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",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
"bootstrap": "^5.2.3",
"check-password-strength": "^2.0.7",
"choices.js": "^10.2.0",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
@ -66,7 +66,6 @@
"markdown-it-sup": "^1.0.0",
"mini-css-extract-plugin": "^2.7.2",
"moment": "^2.29.4",
"node-fetch": "^2.6.1",
"register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0",
"rxjs": "^7.8.0",
@ -90,13 +89,11 @@
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-container": "^2.0.5",
"@types/node": "^18.14.0",
"@types/node-fetch": "^2.6.2",
"@types/sanitize-html": "^2.8.0",
"@types/serialize-javascript": "^5.0.1",
"@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"bootstrap": "^5.2.3",
"bootswatch": "^5.2.3",
"eslint": "^8.34.0",
"eslint-plugin-inferno": "^7.32.1",

View file

@ -3,6 +3,8 @@ import { BrowserRouter } from "inferno-router";
import { App } from "../shared/components/app/app";
import { initializeSite } from "../shared/utils";
import "bootstrap/js/dist/dropdown";
const site = window.isoData.site_res;
initializeSite(site);
@ -12,7 +14,7 @@ const wrapper = (
</BrowserRouter>
);
let root = document.getElementById("root");
const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}

View file

@ -105,51 +105,54 @@ server.get("/*", async (req, res) => {
const context = {} as any;
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
let getSiteForm: GetSite = { auth };
const getSiteForm: GetSite = { auth };
let promises: Promise<any>[] = [];
const promises: Promise<any>[] = [];
let headers = setForwardedHeaders(req.headers);
let initialFetchReq: InitialFetchRequest = {
client: new LemmyHttp(httpBaseInternal, headers),
auth,
path: req.path,
};
const headers = setForwardedHeaders(req.headers);
const client = new LemmyHttp(httpBaseInternal, headers);
// Get site data first
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
let try_site: any = await initialFetchReq.client.getSite(getSiteForm);
let try_site: any = await client.getSite(getSiteForm);
if (try_site.error == "not_logged_in") {
console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
);
getSiteForm.auth = undefined;
initialFetchReq.auth = undefined;
try_site = await initialFetchReq.client.getSite(getSiteForm);
auth = undefined;
try_site = await client.getSite(getSiteForm);
}
let site: GetSiteResponse = try_site;
const site: GetSiteResponse = try_site;
initializeSite(site);
const initialFetchReq: InitialFetchRequest = {
client,
auth,
path: req.path,
query: req.query,
site,
};
if (activeRoute?.fetchInitialData) {
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
}
let routeData = await Promise.all(promises);
const routeData = await Promise.all(promises);
// Redirect to the 404 if there's an API error
if (routeData[0] && routeData[0].error) {
let errCode = routeData[0].error;
console.error(errCode);
if (errCode == "instance_is_private") {
const error = routeData[0].error;
console.error(error);
if (error === "instance_is_private") {
return res.redirect(`/signup`);
} else {
return res.send(`404: ${removeAuthParam(errCode)}`);
return res.send(`404: ${removeAuthParam(error)}`);
}
}
let isoData: IsoData = {
const isoData: IsoData = {
path: req.path,
site_res: site,
routeData,
@ -170,6 +173,7 @@ server.get("/*", async (req, res) => {
<script>eruda.init();</script>
</>
);
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
const root = renderToString(wrapper);
const helmet = Helmet.renderStatic();

View file

@ -40,17 +40,10 @@ export class App extends Component<any, any> {
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
<Switch>
{routes.map(
({ path, exact, component: Component, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={props => <Component {...props} {...rest} />}
/>
)
)}
<Route render={props => <NoMatch {...props} />} />
{routes.map(({ path, component }) => (
<Route key={path} path={path} exact component={component} />
))}
<Route component={NoMatch} />
</Switch>
</div>
<Footer site={siteRes} />

View file

@ -43,7 +43,6 @@ interface NavbarState {
unreadInboxCount: number;
unreadReportCount: number;
unreadApplicationCount: number;
searchParam: string;
showDropdown: boolean;
onSiteBanner?(url: string): any;
}
@ -59,7 +58,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
unreadReportCount: 0,
unreadApplicationCount: 0,
expanded: false,
searchParam: "",
showDropdown: false,
};
subscription: any;
@ -115,20 +113,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
this.unreadApplicationCountSub.unsubscribe();
}
updateUrl() {
const searchParam = this.state.searchParam;
this.setState({ searchParam: "" });
this.setState({ showDropdown: false, expanded: false });
if (searchParam === "") {
this.context.router.history.push(`/search/`);
} else {
const searchParamEncoded = encodeURIComponent(searchParam);
this.context.router.history.push(
`/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
);
}
}
render() {
return this.navbar();
}
@ -488,10 +472,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
i.setState({ expanded: false, showDropdown: false });
}
handleSearchParam(i: Navbar, event: any) {
i.setState({ searchParam: event.target.value });
}
handleLogoutClick(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
UserService.Instance.logout();

View file

@ -1,6 +1,6 @@
// Custom css
@import "../../../../node_modules/tributejs/dist/tribute.css";
@import "../../../../node_modules/toastify-js/src/toastify.css";
@import "../../../../node_modules/choices.js/src/styles/choices.scss";
@import "../../../../node_modules/tippy.js/dist/tippy.css";
@import "../../../../node_modules/bootstrap/dist/css/bootstrap-utilities.min.css";
@import "../../../assets/css/main.css";

View file

@ -430,7 +430,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<button className="btn btn-link btn-animate">
<Link
className="text-muted"
to={`/create_private_message/recipient/${cv.creator.id}`}
to={`/create_private_message/${cv.creator.id}`}
title={i18n.t("message").toLowerCase()}
>
<Icon icon="mail" />

View file

@ -28,7 +28,7 @@ interface CommentNodesProps {
}
export class CommentNodes extends Component<CommentNodesProps, any> {
constructor(props: any, context: any) {
constructor(props: CommentNodesProps, context: any) {
super(props, context);
}

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 { WebSocketService } from "../../services";
import {
getListingTypeFromPropsNoDefault,
getPageFromProps,
getPageFromString,
getQueryParams,
getQueryString,
isBrowser,
myAuth,
numToSI,
QueryParams,
routeListingTypeToEnum,
setIsoData,
showLocal,
toast,
@ -38,16 +41,52 @@ const communityLimit = 50;
interface CommunitiesState {
listCommunitiesResponse?: ListCommunitiesResponse;
page: number;
loading: boolean;
siteRes: GetSiteResponse;
searchText: string;
listingType: ListingType;
}
interface CommunitiesProps {
listingType?: ListingType;
page?: number;
listingType: ListingType;
page: number;
}
function getCommunitiesQueryParams() {
return getQueryParams<CommunitiesProps>({
listingType: getListingTypeFromQuery,
page: getPageFromString,
});
}
function getListingTypeFromQuery(listingType?: string): ListingType {
return routeListingTypeToEnum(listingType ?? "", ListingType.Local);
}
function toggleSubscribe(community_id: number, follow: boolean) {
const auth = myAuth();
if (auth) {
const form: FollowCommunity = {
community_id,
follow,
auth,
};
WebSocketService.Instance.send(wsClient.followCommunity(form));
}
}
function refetch() {
const { listingType, page } = getCommunitiesQueryParams();
const listCommunitiesForm: ListCommunities = {
type_: listingType,
sort: SortType.TopMonth,
limit: communityLimit,
page,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
}
export class Communities extends Component<any, CommunitiesState> {
@ -55,8 +94,6 @@ export class Communities extends Component<any, CommunitiesState> {
private isoData = setIsoData(this.context);
state: CommunitiesState = {
loading: true,
page: getPageFromProps(this.props),
listingType: getListingTypeFromPropsNoDefault(this.props),
siteRes: this.isoData.site_res,
searchText: "",
};
@ -70,15 +107,15 @@ export class Communities extends Component<any, CommunitiesState> {
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
let listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
if (this.isoData.path === this.context.router.route.match.url) {
const listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
this.state = {
...this.state,
listCommunitiesResponse: listRes,
loading: false,
};
} else {
this.refetch();
refetch();
}
}
@ -88,23 +125,6 @@ export class Communities extends Component<any, CommunitiesState> {
}
}
static getDerivedStateFromProps(props: any): CommunitiesProps {
return {
listingType: getListingTypeFromPropsNoDefault(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: CommunitiesState) {
if (
lastState.page !== this.state.page ||
lastState.listingType !== this.state.listingType
) {
this.setState({ loading: true });
this.refetch();
}
}
get documentTitle(): string {
return `${i18n.t("communities")} - ${
this.state.siteRes.site_view.site.name
@ -112,6 +132,8 @@ export class Communities extends Component<any, CommunitiesState> {
}
render() {
const { listingType, page } = getCommunitiesQueryParams();
return (
<div className="container-lg">
<HtmlTags
@ -129,7 +151,7 @@ export class Communities extends Component<any, CommunitiesState> {
<h4>{i18n.t("list_of_communities")}</h4>
<span className="mb-2">
<ListingTypeSelect
type_={this.state.listingType}
type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
@ -192,7 +214,7 @@ export class Communities extends Component<any, CommunitiesState> {
{i18n.t("unsubscribe")}
</button>
)}
{cv.subscribed == SubscribedType.NotSubscribed && (
{cv.subscribed === SubscribedType.NotSubscribed && (
<button
className="btn btn-link d-inline-block"
onClick={linkEvent(
@ -203,7 +225,7 @@ export class Communities extends Component<any, CommunitiesState> {
{i18n.t("subscribe")}
</button>
)}
{cv.subscribed == SubscribedType.Pending && (
{cv.subscribed === SubscribedType.Pending && (
<div className="text-warning d-inline-block">
{i18n.t("subscribe_pending")}
</div>
@ -214,10 +236,7 @@ export class Communities extends Component<any, CommunitiesState> {
</tbody>
</table>
</div>
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
/>
<Paginator page={page} onChange={this.handlePageChange} />
</div>
)}
</div>
@ -250,12 +269,18 @@ export class Communities extends Component<any, CommunitiesState> {
);
}
updateUrl(paramUpdates: CommunitiesProps) {
const page = paramUpdates.page || this.state.page;
const listingTypeStr = paramUpdates.listingType || this.state.listingType;
this.props.history.push(
`/communities/listing_type/${listingTypeStr}/page/${page}`
);
updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
const { listingType: urlListingType, page: urlPage } =
getCommunitiesQueryParams();
const queryParams: QueryParams<CommunitiesProps> = {
listingType: listingType ?? urlListingType,
page: (page ?? urlPage)?.toString(),
};
this.props.history.push(`/communities${getQueryString(queryParams)}`);
refetch();
}
handlePageChange(page: number) {
@ -270,27 +295,11 @@ export class Communities extends Component<any, CommunitiesState> {
}
handleUnsubscribe(communityId: number) {
let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id: communityId,
follow: false,
auth,
};
WebSocketService.Instance.send(wsClient.followCommunity(form));
}
toggleSubscribe(communityId, false);
}
handleSubscribe(communityId: number) {
let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id: communityId,
follow: true,
auth,
};
WebSocketService.Instance.send(wsClient.followCommunity(form));
}
toggleSubscribe(communityId, true);
}
handleSearchChange(i: Communities, event: any) {
@ -299,61 +308,50 @@ export class Communities extends Component<any, CommunitiesState> {
handleSearchSubmit(i: Communities) {
const searchParamEncoded = encodeURIComponent(i.state.searchText);
i.context.router.history.push(
`/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
);
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
}
refetch() {
let listCommunitiesForm: ListCommunities = {
type_: this.state.listingType,
static fetchInitialData({
query: { listingType, page },
client,
auth,
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<any>[] {
const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType),
sort: SortType.TopMonth,
limit: communityLimit,
page: this.state.page,
auth: myAuth(false),
page: getPageFromString(page),
auth: auth,
};
WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm)
);
}
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/");
let type_: ListingType = pathSplit[3]
? ListingType[pathSplit[3]]
: ListingType.Local;
let page = pathSplit[5] ? Number(pathSplit[5]) : 1;
let listCommunitiesForm: ListCommunities = {
type_,
sort: SortType.TopMonth,
limit: communityLimit,
page,
auth: req.auth,
};
return [req.client.listCommunities(listCommunitiesForm)];
return [client.listCommunities(listCommunitiesForm)];
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
const op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
return;
} else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg);
} else if (op === UserOperation.ListCommunities) {
const data = wsJsonToRes<ListCommunitiesResponse>(msg);
this.setState({ listCommunitiesResponse: data, loading: false });
window.scrollTo(0, 0);
} else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg);
let res = this.state.listCommunitiesResponse;
let found = res?.communities.find(
c => c.community.id == data.community_view.community.id
} else if (op === UserOperation.FollowCommunity) {
const {
community_view: {
community,
subscribed,
counts: { subscribers },
},
} = wsJsonToRes<CommunityResponse>(msg);
const res = this.state.listCommunitiesResponse;
const found = res?.communities.find(
({ community: { id } }) => id == community.id
);
if (found) {
found.subscribed = data.community_view.subscribed;
found.counts.subscribers = data.community_view.counts.subscribers;
found.subscribed = subscribed;
found.counts.subscribers = subscribers;
this.setState(this.state);
}
}

View file

@ -1,10 +1,10 @@
import { Component, linkEvent } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AddModToCommunityResponse,
BanFromCommunityResponse,
BlockCommunityResponse,
BlockPersonResponse,
CommentReportResponse,
CommentResponse,
CommentView,
CommunityResponse,
@ -14,7 +14,6 @@ import {
GetCommunityResponse,
GetPosts,
GetPostsResponse,
GetSiteResponse,
ListingType,
PostReportResponse,
PostResponse,
@ -43,16 +42,20 @@ import {
enableDownvotes,
enableNsfw,
fetchLimit,
getDataTypeFromProps,
getPageFromProps,
getSortTypeFromProps,
getDataTypeString,
getPageFromString,
getQueryParams,
getQueryString,
isPostBlocked,
myAuth,
notifyPost,
nsfwCheck,
postToCommentSortType,
QueryParams,
relTags,
restoreScrollPosition,
routeDataTypeToEnum,
routeSortTypeToEnum,
saveCommentRes,
saveScrollPosition,
setIsoData,
@ -78,16 +81,10 @@ import { CommunityLink } from "./community-link";
interface State {
communityRes?: GetCommunityResponse;
siteRes: GetSiteResponse;
communityName: string;
communityLoading: boolean;
postsLoading: boolean;
commentsLoading: boolean;
listingsLoading: boolean;
posts: PostView[];
comments: CommentView[];
dataType: DataType;
sort: SortType;
page: number;
showSidebarMobile: boolean;
}
@ -97,30 +94,43 @@ interface CommunityProps {
page: number;
}
interface UrlParams {
dataType?: string;
sort?: SortType;
page?: number;
function getCommunityQueryParams() {
return getQueryParams<CommunityProps>({
dataType: getDataTypeFromQuery,
page: getPageFromString,
sort: getSortTypeFromQuery,
});
}
export class Community extends Component<any, State> {
const getDataTypeFromQuery = (type?: string): DataType =>
routeDataTypeToEnum(type ?? "", DataType.Post);
function getSortTypeFromQuery(type?: string): SortType {
const mySortType =
UserService.Instance.myUserInfo?.local_user_view.local_user
.default_sort_type;
return routeSortTypeToEnum(
type ?? "",
mySortType ? Object.values(SortType)[mySortType] : SortType.Active
);
}
export class Community extends Component<
RouteComponentProps<{ name: string }>,
State
> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: State = {
communityName: this.props.match.params.name,
communityLoading: true,
postsLoading: true,
commentsLoading: true,
listingsLoading: true,
posts: [],
comments: [],
dataType: getDataTypeFromProps(this.props),
sort: getSortTypeFromProps(this.props),
page: getPageFromProps(this.props),
siteRes: this.isoData.site_res,
showSidebarMobile: false,
};
constructor(props: any, context: any) {
constructor(props: RouteComponentProps<{ name: string }>, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
@ -136,8 +146,10 @@ export class Community extends Component<any, State> {
...this.state,
communityRes: this.isoData.routeData[0] as GetCommunityResponse,
};
let postsRes = this.isoData.routeData[1] as GetPostsResponse | undefined;
let commentsRes = this.isoData.routeData[2] as
const postsRes = this.isoData.routeData[1] as
| GetPostsResponse
| undefined;
const commentsRes = this.isoData.routeData[2] as
| GetCommentsResponse
| undefined;
@ -152,8 +164,7 @@ export class Community extends Component<any, State> {
this.state = {
...this.state,
communityLoading: false,
postsLoading: false,
commentsLoading: false,
listingsLoading: false,
};
} else {
this.fetchCommunity();
@ -162,8 +173,8 @@ export class Community extends Component<any, State> {
}
fetchCommunity() {
let form: GetCommunity = {
name: this.state.communityName,
const form: GetCommunity = {
name: this.props.match.params.name,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.getCommunity(form));
@ -178,95 +189,67 @@ export class Community extends Component<any, State> {
this.subscription?.unsubscribe();
}
static getDerivedStateFromProps(props: any): CommunityProps {
return {
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
static fetchInitialData({
client,
path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
auth,
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<any>[] {
const pathSplit = path.split("/");
const promises: Promise<any>[] = [];
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/");
let promises: Promise<any>[] = [];
let communityName = pathSplit[2];
let communityForm: GetCommunity = {
const communityName = pathSplit[2];
const communityForm: GetCommunity = {
name: communityName,
auth: req.auth,
auth,
};
promises.push(req.client.getCommunity(communityForm));
promises.push(client.getCommunity(communityForm));
let dataType: DataType = pathSplit[4]
? DataType[pathSplit[4]]
: DataType.Post;
const dataType = getDataTypeFromQuery(urlDataType);
let mui = UserService.Instance.myUserInfo;
const sort = getSortTypeFromQuery(urlSort);
let sort: SortType = pathSplit[6]
? SortType[pathSplit[6]]
: mui
? Object.values(SortType)[
mui.local_user_view.local_user.default_sort_type
]
: SortType.Active;
const page = getPageFromString(urlPage);
let page = pathSplit[8] ? Number(pathSplit[8]) : 1;
if (dataType == DataType.Post) {
let getPostsForm: GetPosts = {
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
community_name: communityName,
page,
limit: fetchLimit,
sort,
type_: ListingType.All,
saved_only: false,
auth: req.auth,
auth,
};
promises.push(req.client.getPosts(getPostsForm));
promises.push(client.getPosts(getPostsForm));
promises.push(Promise.resolve());
} else {
let getCommentsForm: GetComments = {
const getCommentsForm: GetComments = {
community_name: communityName,
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
type_: ListingType.All,
saved_only: false,
auth: req.auth,
auth,
};
promises.push(Promise.resolve());
promises.push(req.client.getComments(getCommentsForm));
promises.push(client.getComments(getCommentsForm));
}
return promises;
}
componentDidUpdate(_: any, lastState: State) {
if (
lastState.dataType !== this.state.dataType ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) {
this.setState({ postsLoading: true, commentsLoading: true });
this.fetchData();
}
}
get documentTitle(): string {
let cRes = this.state.communityRes;
const cRes = this.state.communityRes;
return cRes
? `${cRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`
? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
: "";
}
render() {
// For some reason, this returns an empty vec if it matches the site langs
let res = this.state.communityRes;
let communityLangs =
res?.discussion_languages.length == 0
? this.state.siteRes.all_languages.map(l => l.id)
: res?.discussion_languages;
const res = this.state.communityRes;
const { page } = getCommunityQueryParams();
return (
<div className="container-lg">
@ -286,7 +269,7 @@ export class Community extends Component<any, State> {
<div className="row">
<div className="col-12 col-md-8">
{this.communityInfo()}
{this.communityInfo}
<div className="d-block d-md-none">
<button
className="btn btn-secondary d-inline-block mb-2 mr-3"
@ -302,55 +285,14 @@ export class Community extends Component<any, State> {
classes="icon-inline"
/>
</button>
{this.state.showSidebarMobile && (
<>
<Sidebar
community_view={res.community_view}
moderators={res.moderators}
admins={this.state.siteRes.admins}
online={res.online}
enableNsfw={enableNsfw(this.state.siteRes)}
editable
allLanguages={this.state.siteRes.all_languages}
siteLanguages={
this.state.siteRes.discussion_languages
}
communityLanguages={communityLangs}
/>
{!res.community_view.community.local && res.site && (
<SiteSidebar
site={res.site}
showLocal={showLocal(this.isoData)}
/>
)}
</>
)}
{this.state.showSidebarMobile && this.sidebar(res)}
</div>
{this.selects()}
{this.listings()}
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
/>
{this.selects}
{this.listings}
<Paginator page={page} onChange={this.handlePageChange} />
</div>
<div className="d-none d-md-block col-md-4">
<Sidebar
community_view={res.community_view}
moderators={res.moderators}
admins={this.state.siteRes.admins}
online={res.online}
enableNsfw={enableNsfw(this.state.siteRes)}
editable
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
communityLanguages={communityLangs}
/>
{!res.community_view.community.local && res.site && (
<SiteSidebar
site={res.site}
showLocal={showLocal(this.isoData)}
/>
)}
{this.sidebar(res)}
</div>
</div>
</>
@ -360,43 +302,82 @@ export class Community extends Component<any, State> {
);
}
listings() {
return this.state.dataType == DataType.Post ? (
this.state.postsLoading ? (
<h5>
<Spinner large />
</h5>
) : (
<PostListings
posts={this.state.posts}
removeDuplicates
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
sidebar({
community_view,
moderators,
online,
discussion_languages,
site,
}: GetCommunityResponse) {
const { site_res } = this.isoData;
// For some reason, this returns an empty vec if it matches the site langs
const communityLangs =
discussion_languages.length === 0
? site_res.all_languages.map(({ id }) => id)
: discussion_languages;
return (
<>
<Sidebar
community_view={community_view}
moderators={moderators}
admins={site_res.admins}
online={online}
enableNsfw={enableNsfw(site_res)}
editable
allLanguages={site_res.all_languages}
siteLanguages={site_res.discussion_languages}
communityLanguages={communityLangs}
/>
)
) : this.state.commentsLoading ? (
<h5>
<Spinner large />
</h5>
) : (
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
viewType={CommentViewType.Flat}
noIndent
showContext
enableDownvotes={enableDownvotes(this.state.siteRes)}
moderators={this.state.communityRes?.moderators}
admins={this.state.siteRes.admins}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
/>
{!community_view.community.local && site && (
<SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
)}
</>
);
}
communityInfo() {
let community = this.state.communityRes?.community_view.community;
get listings() {
const { dataType } = getCommunityQueryParams();
const { site_res } = this.isoData;
const { listingsLoading, posts, comments, communityRes } = this.state;
if (listingsLoading) {
return (
<h5>
<Spinner large />
</h5>
);
} else if (dataType === DataType.Post) {
return (
<PostListings
posts={posts}
removeDuplicates
enableDownvotes={enableDownvotes(site_res)}
enableNsfw={enableNsfw(site_res)}
allLanguages={site_res.all_languages}
siteLanguages={site_res.discussion_languages}
/>
);
} else {
return (
<CommentNodes
nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat}
noIndent
showContext
enableDownvotes={enableDownvotes(site_res)}
moderators={communityRes?.moderators}
admins={site_res.admins}
allLanguages={site_res.all_languages}
siteLanguages={site_res.discussion_languages}
/>
);
}
}
get communityInfo() {
const community = this.state.communityRes?.community_view.community;
return (
community && (
<div className="mb-2">
@ -414,25 +395,26 @@ export class Community extends Component<any, State> {
);
}
selects() {
get selects() {
// let communityRss = this.state.communityRes.map(r =>
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
// );
let res = this.state.communityRes;
let communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, this.state.sort)
const { dataType, sort } = getCommunityQueryParams();
const res = this.state.communityRes;
const communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, sort)
: undefined;
return (
<div className="mb-3">
<span className="mr-3">
<DataTypeSelect
type_={this.state.dataType}
type_={dataType}
onChange={this.handleDataTypeChange}
/>
</span>
<span className="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
<SortSelect sort={sort} onChange={this.handleSortChange} />
</span>
{communityRss && (
<>
@ -455,66 +437,90 @@ export class Community extends Component<any, State> {
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
this.updateUrl({ sort: val, page: 1 });
handleSortChange(sort: SortType) {
this.updateUrl({ sort, page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
this.updateUrl({ dataType: DataType[val], page: 1 });
handleDataTypeChange(dataType: DataType) {
this.updateUrl({ dataType, page: 1 });
window.scrollTo(0, 0);
}
handleShowSidebarMobile(i: Community) {
i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
i.setState(({ showSidebarMobile }) => ({
showSidebarMobile: !showSidebarMobile,
}));
}
updateUrl(paramUpdates: UrlParams) {
const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
const sortStr = paramUpdates.sort || this.state.sort;
const page = paramUpdates.page || this.state.page;
updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
const {
dataType: urlDataType,
page: urlPage,
sort: urlSort,
} = getCommunityQueryParams();
let typeView = `/c/${this.state.communityName}`;
const queryParams: QueryParams<CommunityProps> = {
dataType: getDataTypeString(dataType ?? urlDataType),
page: (page ?? urlPage).toString(),
sort: sort ?? urlSort,
};
this.props.history.push(
`${typeView}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
`/c/${this.props.match.params.name}${getQueryString(queryParams)}`
);
this.setState({
comments: [],
posts: [],
listingsLoading: true,
});
this.fetchData();
}
fetchData() {
if (this.state.dataType == DataType.Post) {
let form: GetPosts = {
page: this.state.page,
const { dataType, page, sort } = getCommunityQueryParams();
const { name } = this.props.match.params;
let req: string;
if (dataType === DataType.Post) {
const form: GetPosts = {
page,
limit: fetchLimit,
sort: this.state.sort,
sort,
type_: ListingType.All,
community_name: this.state.communityName,
community_name: name,
saved_only: false,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.getPosts(form));
req = wsClient.getPosts(form);
} else {
let form: GetComments = {
page: this.state.page,
const form: GetComments = {
page,
limit: fetchLimit,
sort: postToCommentSortType(this.state.sort),
sort: postToCommentSortType(sort),
type_: ListingType.All,
community_name: this.state.communityName,
community_name: name,
saved_only: false,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.getComments(form));
req = wsClient.getComments(form);
}
WebSocketService.Instance.send(req);
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
const { page } = getCommunityQueryParams();
const op = wsUserOp(msg);
console.log(msg);
let res = this.state.communityRes;
const res = this.state.communityRes;
if (msg.error) {
toast(i18n.t(msg.error), "danger");
this.context.router.history.push("/");
return;
} else if (msg.reconnect) {
if (res) {
WebSocketService.Instance.send(
@ -523,143 +529,225 @@ export class Community extends Component<any, State> {
})
);
}
this.fetchData();
} else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({ communityRes: data, communityLoading: false });
// TODO why is there no auth in this form?
WebSocketService.Instance.send(
wsClient.communityJoin({
community_id: data.community_view.community.id,
})
);
} else if (
op == UserOperation.EditCommunity ||
op == UserOperation.DeleteCommunity ||
op == UserOperation.RemoveCommunity
) {
let data = wsJsonToRes<CommunityResponse>(msg);
if (res) {
res.community_view = data.community_view;
res.discussion_languages = data.discussion_languages;
}
this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg);
if (res) {
res.community_view.subscribed = data.community_view.subscribed;
res.community_view.counts.subscribers =
data.community_view.counts.subscribers;
}
this.setState(this.state);
} else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts: data.posts, postsLoading: false });
restoreScrollPosition(this.context);
setupTippy();
} else if (
op == UserOperation.EditPost ||
op == UserOperation.DeletePost ||
op == UserOperation.RemovePost ||
op == UserOperation.LockPost ||
op == UserOperation.FeaturePost ||
op == UserOperation.SavePost
) {
let data = wsJsonToRes<PostResponse>(msg);
editPostFindRes(data.post_view, this.state.posts);
this.setState(this.state);
} else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg);
} else {
switch (op) {
case UserOperation.GetCommunity: {
const data = wsJsonToRes<GetCommunityResponse>(msg);
let showPostNotifs =
UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs;
this.setState({ communityRes: data, communityLoading: false });
// TODO why is there no auth in this form?
WebSocketService.Instance.send(
wsClient.communityJoin({
community_id: data.community_view.community.id,
})
);
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
//
if (
this.state.page == 1 &&
nsfwCheck(data.post_view) &&
!isPostBlocked(data.post_view)
) {
this.state.posts.unshift(data.post_view);
if (showPostNotifs) {
notifyPost(data.post_view, this.context.router);
break;
}
this.setState(this.state);
}
} else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(data.post_view, this.state.posts);
this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>(msg);
if (res) {
res.moderators = data.moderators;
}
this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) {
let data = wsJsonToRes<BanFromCommunityResponse>(msg);
// TODO this might be incorrect
this.state.posts
.filter(p => p.creator.id == data.person_view.person.id)
.forEach(p => (p.creator_banned_from_community = data.banned));
case UserOperation.EditCommunity:
case UserOperation.DeleteCommunity:
case UserOperation.RemoveCommunity: {
const { community_view, discussion_languages } =
wsJsonToRes<CommunityResponse>(msg);
this.setState(this.state);
} else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg);
this.setState({ comments: data.comments, commentsLoading: false });
} else if (
op == UserOperation.EditComment ||
op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment
) {
let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes(data.comment_view, this.state.comments);
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg);
if (res) {
res.community_view = community_view;
res.discussion_languages = discussion_languages;
this.setState(this.state);
}
// Necessary since it might be a user reply
if (data.form_id) {
this.state.comments.unshift(data.comment_view);
this.setState(this.state);
break;
}
case UserOperation.FollowCommunity: {
const {
community_view: {
subscribed,
counts: { subscribers },
},
} = wsJsonToRes<CommunityResponse>(msg);
if (res) {
res.community_view.subscribed = subscribed;
res.community_view.counts.subscribers = subscribers;
this.setState(this.state);
}
break;
}
case UserOperation.GetPosts: {
const { posts } = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts, listingsLoading: false });
restoreScrollPosition(this.context);
setupTippy();
break;
}
case UserOperation.EditPost:
case UserOperation.DeletePost:
case UserOperation.RemovePost:
case UserOperation.LockPost:
case UserOperation.FeaturePost:
case UserOperation.SavePost: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
editPostFindRes(post_view, this.state.posts);
this.setState(this.state);
break;
}
case UserOperation.CreatePost: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
const showPostNotifs =
UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs;
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
this.state.posts.unshift(post_view);
if (showPostNotifs) {
notifyPost(post_view, this.context.router);
}
this.setState(this.state);
}
break;
}
case UserOperation.CreatePostLike: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(post_view, this.state.posts);
this.setState(this.state);
break;
}
case UserOperation.AddModToCommunity: {
const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
if (res) {
res.moderators = moderators;
this.setState(this.state);
}
break;
}
case UserOperation.BanFromCommunity: {
const {
person_view: {
person: { id: personId },
},
banned,
} = wsJsonToRes<BanFromCommunityResponse>(msg);
// TODO this might be incorrect
this.state.posts
.filter(p => p.creator.id === personId)
.forEach(p => (p.creator_banned_from_community = banned));
this.setState(this.state);
break;
}
case UserOperation.GetComments: {
const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
this.setState({ comments, listingsLoading: false });
break;
}
case UserOperation.EditComment:
case UserOperation.DeleteComment:
case UserOperation.RemoveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
editCommentRes(comment_view, this.state.comments);
this.setState(this.state);
break;
}
case UserOperation.CreateComment: {
const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
// Necessary since it might be a user reply
if (form_id) {
this.setState(({ comments }) => ({
comments: [comment_view].concat(comments),
}));
}
break;
}
case UserOperation.SaveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(comment_view, this.state.comments);
this.setState(this.state);
break;
}
case UserOperation.CreateCommentLike: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(comment_view, this.state.comments);
this.setState(this.state);
break;
}
case UserOperation.BlockPerson: {
const data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data);
break;
}
case UserOperation.CreatePostReport:
case UserOperation.CreateCommentReport: {
const data = wsJsonToRes<PostReportResponse>(msg);
if (data) {
toast(i18n.t("report_created"));
}
break;
}
case UserOperation.PurgeCommunity: {
const { success } = wsJsonToRes<PurgeItemResponse>(msg);
if (success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
break;
}
case UserOperation.BlockCommunity: {
const data = wsJsonToRes<BlockCommunityResponse>(msg);
if (res) {
res.community_view.blocked = data.blocked;
this.setState(this.state);
}
updateCommunityBlock(data);
break;
}
}
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(data.comment_view, this.state.comments);
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(data.comment_view, this.state.comments);
this.setState(this.state);
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg);
if (data) {
toast(i18n.t("report_created"));
}
} else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg);
if (data) {
toast(i18n.t("report_created"));
}
} else if (op == UserOperation.PurgeCommunity) {
let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
} else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes<BlockCommunityResponse>(msg);
if (res) {
res.community_view.blocked = data.blocked;
}
updateCommunityBlock(data);
this.setState(this.state);
}
}
}

View file

@ -280,7 +280,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
className={`btn btn-secondary btn-block mb-2 ${
cv.community.deleted || cv.community.removed ? "no-click" : ""
}`}
to={`/create_post?community_id=${cv.community.id}`}
to={`/create_post?communityId=${cv.community.id}`}
>
{i18n.t("create_a_post")}
</Link>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,8 @@
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AddAdminResponse,
BanPerson,
@ -7,6 +10,8 @@ import {
BlockPerson,
BlockPersonResponse,
CommentResponse,
CommunityModeratorView,
CommunitySafe,
GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
@ -33,12 +38,15 @@ import {
enableNsfw,
fetchLimit,
futureDaysToUnixTime,
getUsernameFromProps,
getPageFromString,
getQueryParams,
getQueryString,
isAdmin,
isBanned,
mdToHtml,
myAuth,
numToSI,
QueryParams,
relTags,
restoreScrollPosition,
routeSortTypeToEnum,
@ -62,10 +70,6 @@ import { PersonListing } from "./person-listing";
interface ProfileState {
personRes?: GetPersonDetailsResponse;
userName: string;
view: PersonDetailsView;
sort: SortType;
page: number;
loading: boolean;
personBlocked: boolean;
banReason?: string;
@ -79,32 +83,84 @@ interface ProfileProps {
view: PersonDetailsView;
sort: SortType;
page: number;
person_id?: number;
username: string;
}
interface UrlParams {
view?: string;
sort?: SortType;
page?: number;
const getProfileQueryParams = () =>
getQueryParams<ProfileProps>({
view: getViewFromProps,
page: getPageFromString,
sort: getSortTypeFromQuery,
});
const getSortTypeFromQuery = (sort?: string): SortType =>
sort ? routeSortTypeToEnum(sort, SortType.New) : SortType.New;
const getViewFromProps = (view?: string): PersonDetailsView =>
view
? PersonDetailsView[view] ?? PersonDetailsView.Overview
: PersonDetailsView.Overview;
function toggleBlockPerson(recipientId: number, block: boolean) {
const auth = myAuth();
if (auth) {
const blockUserForm: BlockPerson = {
person_id: recipientId,
block,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
export class Profile extends Component<any, ProfileState> {
const handleUnblockPerson = (personId: number) =>
toggleBlockPerson(personId, false);
const handleBlockPerson = (personId: number) =>
toggleBlockPerson(personId, true);
const getCommunitiesListing = (
translationKey: NoOptionI18nKeys,
communityViews?: { community: CommunitySafe }[]
) =>
communityViews &&
communityViews.length > 0 && (
<div className="card border-secondary mb-3">
<div className="card-body">
<h5>{i18n.t(translationKey)}</h5>
<ul className="list-unstyled mb-0">
{communityViews.map(({ community }) => (
<li key={community.id}>
<CommunityLink community={community} />
</li>
))}
</ul>
</div>
</div>
);
const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
getCommunitiesListing("moderates", moderates);
const Follows = () =>
getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
export class Profile extends Component<
RouteComponentProps<{ username: string }>,
ProfileState
> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: ProfileState = {
userName: getUsernameFromProps(this.props),
loading: true,
view: Profile.getViewFromProps(this.props.match.view),
sort: Profile.getSortTypeFromProps(this.props.match.sort),
page: Profile.getPageFromProps(this.props.match.page),
personBlocked: false,
siteRes: this.isoData.site_res,
showBanDialog: false,
removeData: false,
};
constructor(props: any, context: any) {
constructor(props: RouteComponentProps<{ username: string }>, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
@ -114,7 +170,7 @@ export class Profile extends Component<any, ProfileState> {
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
if (this.isoData.path === this.context.router.route.match.url) {
this.state = {
...this.state,
personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
@ -126,65 +182,61 @@ export class Profile extends Component<any, ProfileState> {
}
fetchUserData() {
let form: GetPersonDetails = {
username: this.state.userName,
sort: this.state.sort,
saved_only: this.state.view === PersonDetailsView.Saved,
page: this.state.page,
const { page, sort, view } = getProfileQueryParams();
const form: GetPersonDetails = {
username: this.props.match.params.username,
sort,
saved_only: view === PersonDetailsView.Saved,
page,
limit: fetchLimit,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
}
get amCurrentUser() {
return (
UserService.Instance.myUserInfo?.local_user_view.person.id ==
UserService.Instance.myUserInfo?.local_user_view.person.id ===
this.state.personRes?.person_view.person.id
);
}
setPersonBlock() {
let mui = UserService.Instance.myUserInfo;
let res = this.state.personRes;
const mui = UserService.Instance.myUserInfo;
const res = this.state.personRes;
if (mui && res) {
this.setState({
personBlocked: mui.person_blocks
.map(a => a.target.id)
.includes(res.person_view.person.id),
personBlocked: mui.person_blocks.some(
({ target: { id } }) => id === res.person_view.person.id
),
});
}
}
static getViewFromProps(view: string): PersonDetailsView {
return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
}
static fetchInitialData({
client,
path,
query: { page, sort, view: urlView },
auth,
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<any>[] {
const pathSplit = path.split("/");
static getSortTypeFromProps(sort: string): SortType {
return sort ? routeSortTypeToEnum(sort) : SortType.New;
}
const username = pathSplit[2];
const view = getViewFromProps(urlView);
static getPageFromProps(page: number): number {
return page ? Number(page) : 1;
}
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/");
let username = pathSplit[2];
let view = this.getViewFromProps(pathSplit[4]);
let sort = this.getSortTypeFromProps(pathSplit[6]);
let page = this.getPageFromProps(Number(pathSplit[8]));
let form: GetPersonDetails = {
const form: GetPersonDetails = {
username: username,
sort,
sort: getSortTypeFromQuery(sort),
saved_only: view === PersonDetailsView.Saved,
page,
page: getPageFromString(page),
limit: fetchLimit,
auth: req.auth,
auth,
};
return [req.client.getPersonDetails(form)];
return [client.getPersonDetails(form)];
}
componentDidMount() {
@ -197,78 +249,59 @@ export class Profile extends Component<any, ProfileState> {
saveScrollPosition(this.context);
}
static getDerivedStateFromProps(props: any): ProfileProps {
return {
view: this.getViewFromProps(props.match.params.view),
sort: this.getSortTypeFromProps(props.match.params.sort),
page: this.getPageFromProps(props.match.params.page),
person_id: Number(props.match.params.id),
username: props.match.params.username,
};
}
componentDidUpdate(lastProps: any) {
// Necessary if you are on a post and you click another post (same route)
if (
lastProps.location.pathname.split("/")[2] !==
lastProps.history.location.pathname.split("/")[2]
) {
// Couldnt get a refresh working. This does for now.
location.reload();
}
}
get documentTitle(): string {
let res = this.state.personRes;
const res = this.state.personRes;
return res
? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
: "";
}
render() {
let res = this.state.personRes;
const { personRes, loading, siteRes } = this.state;
const { page, sort, view } = getProfileQueryParams();
return (
<div className="container-lg">
{this.state.loading ? (
{loading ? (
<h5>
<Spinner large />
</h5>
) : (
res && (
personRes && (
<div className="row">
<div className="col-12 col-md-8">
<>
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
description={res.person_view.person.bio}
image={res.person_view.person.avatar}
/>
{this.userInfo()}
<hr />
</>
{!this.state.loading && this.selects()}
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
description={personRes.person_view.person.bio}
image={personRes.person_view.person.avatar}
/>
{this.userInfo}
<hr />
{this.selects}
<PersonDetails
personRes={res}
admins={this.state.siteRes.admins}
sort={this.state.sort}
page={this.state.page}
personRes={personRes}
admins={siteRes.admins}
sort={sort}
page={page}
limit={fetchLimit}
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
view={this.state.view}
enableDownvotes={enableDownvotes(siteRes)}
enableNsfw={enableNsfw(siteRes)}
view={view}
onPageChange={this.handlePageChange}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages}
/>
</div>
{!this.state.loading && (
<div className="col-12 col-md-4">
{this.moderates()}
{this.amCurrentUser && this.follows()}
</div>
)}
<div className="col-12 col-md-4">
<Moderates moderates={personRes.moderates} />
{this.amCurrentUser && <Follows />}
</div>
</div>
)
)}
@ -276,73 +309,49 @@ export class Profile extends Component<any, ProfileState> {
);
}
viewRadios() {
get viewRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
<label
className={`btn btn-outline-secondary pointer
${this.state.view == PersonDetailsView.Overview && "active"}
`}
>
<input
type="radio"
value={PersonDetailsView.Overview}
checked={this.state.view === PersonDetailsView.Overview}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t("overview")}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.view == PersonDetailsView.Comments && "active"}
`}
>
<input
type="radio"
value={PersonDetailsView.Comments}
checked={this.state.view == PersonDetailsView.Comments}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t("comments")}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.view == PersonDetailsView.Posts && "active"}
`}
>
<input
type="radio"
value={PersonDetailsView.Posts}
checked={this.state.view == PersonDetailsView.Posts}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t("posts")}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.view == PersonDetailsView.Saved && "active"}
`}
>
<input
type="radio"
value={PersonDetailsView.Saved}
checked={this.state.view == PersonDetailsView.Saved}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t("saved")}
</label>
{this.getRadio(PersonDetailsView.Overview)}
{this.getRadio(PersonDetailsView.Comments)}
{this.getRadio(PersonDetailsView.Posts)}
{this.getRadio(PersonDetailsView.Saved)}
</div>
);
}
selects() {
let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
getRadio(view: PersonDetailsView) {
const { view: urlView } = getProfileQueryParams();
const active = view === urlView;
return (
<label
className={classNames("btn btn-outline-secondary pointer", {
active,
})}
>
<input
type="radio"
value={view}
checked={active}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
</label>
);
}
get selects() {
const { sort } = getProfileQueryParams();
const { username } = this.props.match.params;
const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
return (
<div className="mb-2">
<span className="mr-3">{this.viewRadios()}</span>
<span className="mr-3">{this.viewRadios}</span>
<SortSelect
sort={this.state.sort}
sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
@ -354,33 +363,15 @@ export class Profile extends Component<any, ProfileState> {
</div>
);
}
handleBlockPerson(personId: number) {
let auth = myAuth();
if (auth) {
if (personId != 0) {
let blockUserForm: BlockPerson = {
person_id: personId,
block: true,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
}
handleUnblockPerson(recipientId: number) {
let auth = myAuth();
if (auth) {
let blockUserForm: BlockPerson = {
person_id: recipientId,
block: false,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
userInfo() {
let pv = this.state.personRes?.person_view;
get userInfo() {
const pv = this.state.personRes?.person_view;
const {
personBlocked,
siteRes: { admins },
showBanDialog,
} = this.state;
return (
pv && (
<div>
@ -429,7 +420,7 @@ export class Profile extends Component<any, ProfileState> {
)}
</ul>
</div>
{this.banDialog()}
{this.banDialog}
<div className="flex-grow-1 unselectable pointer mx-2"></div>
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
<>
@ -446,19 +437,16 @@ export class Profile extends Component<any, ProfileState> {
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
to={`/create_private_message/recipient/${pv.person.id}`}
to={`/create_private_message/${pv.person.id}`}
>
{i18n.t("send_message")}
</Link>
{this.state.personBlocked ? (
{personBlocked ? (
<button
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
onClick={linkEvent(
pv.person.id,
this.handleUnblockPerson
)}
onClick={linkEvent(pv.person.id, handleUnblockPerson)}
>
{i18n.t("unblock_user")}
</button>
@ -467,10 +455,7 @@ export class Profile extends Component<any, ProfileState> {
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
onClick={linkEvent(
pv.person.id,
this.handleBlockPerson
)}
onClick={linkEvent(pv.person.id, handleBlockPerson)}
>
{i18n.t("block_user")}
</button>
@ -478,9 +463,9 @@ export class Profile extends Component<any, ProfileState> {
</>
)}
{canMod(pv.person.id, undefined, this.state.siteRes.admins) &&
!isAdmin(pv.person.id, this.state.siteRes.admins) &&
!this.state.showBanDialog &&
{canMod(pv.person.id, undefined, admins) &&
!isAdmin(pv.person.id, admins) &&
!showBanDialog &&
(!isBanned(pv.person) ? (
<button
className={
@ -552,12 +537,14 @@ export class Profile extends Component<any, ProfileState> {
);
}
banDialog() {
let pv = this.state.personRes?.person_view;
get banDialog() {
const pv = this.state.personRes?.person_view;
const { showBanDialog } = this.state;
return (
pv && (
<>
{this.state.showBanDialog && (
{showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<div className="form-group row col-12">
<label className="col-form-label" htmlFor="profile-ban-reason">
@ -630,73 +617,38 @@ export class Profile extends Component<any, ProfileState> {
);
}
moderates() {
let moderates = this.state.personRes?.moderates;
return (
moderates &&
moderates.length > 0 && (
<div className="card border-secondary mb-3">
<div className="card-body">
<h5>{i18n.t("moderates")}</h5>
<ul className="list-unstyled mb-0">
{moderates.map(cmv => (
<li key={cmv.community.id}>
<CommunityLink community={cmv.community} />
</li>
))}
</ul>
</div>
</div>
)
);
}
updateUrl({ page, sort, view }: Partial<ProfileProps>) {
const {
page: urlPage,
sort: urlSort,
view: urlView,
} = getProfileQueryParams();
follows() {
let follows = UserService.Instance.myUserInfo?.follows;
return (
follows &&
follows.length > 0 && (
<div className="card border-secondary mb-3">
<div className="card-body">
<h5>{i18n.t("subscribed")}</h5>
<ul className="list-unstyled mb-0">
{follows.map(cfv => (
<li key={cfv.community.id}>
<CommunityLink community={cfv.community} />
</li>
))}
</ul>
</div>
</div>
)
);
}
const queryParams: QueryParams<ProfileProps> = {
page: (page ?? urlPage).toString(),
sort: sort ?? urlSort,
view: view ?? urlView,
};
updateUrl(paramUpdates: UrlParams) {
const page = paramUpdates.page || this.state.page;
const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
const sortStr = paramUpdates.sort || this.state.sort;
const { username } = this.props.match.params;
let typeView = `/u/${this.state.userName}`;
this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
this.props.history.push(
`${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
);
this.setState({ loading: true });
this.fetchUserData();
}
handlePageChange(page: number) {
this.updateUrl({ page: page });
this.updateUrl({ page });
}
handleSortChange(val: SortType) {
this.updateUrl({ sort: val, page: 1 });
handleSortChange(sort: SortType) {
this.updateUrl({ sort, page: 1 });
}
handleViewChange(i: Profile, event: any) {
i.updateUrl({
view: PersonDetailsView[Number(event.target.value)],
view: PersonDetailsView[event.target.value],
page: 1,
});
}
@ -724,20 +676,25 @@ export class Profile extends Component<any, ProfileState> {
handleModBanSubmit(i: Profile, event?: any) {
if (event) event.preventDefault();
let person = i.state.personRes?.person_view.person;
let auth = myAuth();
const { personRes, removeData, banReason, banExpireDays } = i.state;
const person = personRes?.person_view.person;
const auth = myAuth();
if (person && auth) {
const ban = !person.banned;
// If its an unban, restore all their data
let ban = !person.banned;
if (ban == false) {
if (!ban) {
i.setState({ removeData: false });
}
let form: BanPerson = {
const form: BanPerson = {
person_id: person.id,
ban,
remove_data: i.state.removeData,
reason: i.state.banReason,
expires: futureDaysToUnixTime(i.state.banExpireDays),
remove_data: removeData,
reason: banReason,
expires: futureDaysToUnixTime(banExpireDays),
auth,
};
WebSocketService.Instance.send(wsClient.banPerson(form));
@ -747,94 +704,138 @@ export class Profile extends Component<any, ProfileState> {
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
const op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
if (msg.error == "couldnt_find_that_username_or_email") {
if (msg.error === "couldnt_find_that_username_or_email") {
this.context.router.history.push("/");
}
return;
} else if (msg.reconnect) {
this.fetchUserData();
} else if (op == UserOperation.GetPersonDetails) {
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs
// TODO this might need to get abstracted
let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
this.setState({ personRes: data, loading: false });
this.setPersonBlock();
restoreScrollPosition(this.context);
} else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = data.admins), s));
} else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(data.comment_view, this.state.personRes?.comments);
this.setState(this.state);
} else if (
op == UserOperation.EditComment ||
op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment
) {
let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes(data.comment_view, this.state.personRes?.comments);
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg);
let mui = UserService.Instance.myUserInfo;
if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
toast(i18n.t("reply_sent"));
}
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(data.comment_view, this.state.personRes?.comments);
this.setState(this.state);
} else if (
op == UserOperation.EditPost ||
op == UserOperation.DeletePost ||
op == UserOperation.RemovePost ||
op == UserOperation.LockPost ||
op == UserOperation.FeaturePost ||
op == UserOperation.SavePost
) {
let data = wsJsonToRes<PostResponse>(msg);
editPostFindRes(data.post_view, this.state.personRes?.posts);
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(data.post_view, this.state.personRes?.posts);
this.setState(this.state);
} else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg);
let res = this.state.personRes;
res?.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
res?.posts
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
let pv = res?.person_view;
} else {
switch (op) {
case UserOperation.GetPersonDetails: {
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs
// TODO this might need to get abstracted
const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
this.setState({ personRes: data, loading: false });
this.setPersonBlock();
restoreScrollPosition(this.context);
if (pv?.person.id == data.person_view.person.id) {
pv.person.banned = data.banned;
}
this.setState(this.state);
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data);
this.setPersonBlock();
this.setState(this.state);
} else if (
op == UserOperation.PurgePerson ||
op == UserOperation.PurgePost ||
op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity
) {
let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
break;
}
case UserOperation.AddAdmin: {
const { admins } = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = admins), s));
break;
}
case UserOperation.CreateCommentLike: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(comment_view, this.state.personRes?.comments);
this.setState(this.state);
break;
}
case UserOperation.EditComment:
case UserOperation.DeleteComment:
case UserOperation.RemoveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
editCommentRes(comment_view, this.state.personRes?.comments);
this.setState(this.state);
break;
}
case UserOperation.CreateComment: {
const {
comment_view: {
creator: { id },
},
} = wsJsonToRes<CommentResponse>(msg);
const mui = UserService.Instance.myUserInfo;
if (id === mui?.local_user_view.person.id) {
toast(i18n.t("reply_sent"));
}
break;
}
case UserOperation.SaveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(comment_view, this.state.personRes?.comments);
this.setState(this.state);
break;
}
case UserOperation.EditPost:
case UserOperation.DeletePost:
case UserOperation.RemovePost:
case UserOperation.LockPost:
case UserOperation.FeaturePost:
case UserOperation.SavePost: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
editPostFindRes(post_view, this.state.personRes?.posts);
this.setState(this.state);
break;
}
case UserOperation.CreatePostLike: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(post_view, this.state.personRes?.posts);
this.setState(this.state);
break;
}
case UserOperation.BanPerson: {
const data = wsJsonToRes<BanPersonResponse>(msg);
const res = this.state.personRes;
res?.comments
.filter(c => c.creator.id === data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
res?.posts
.filter(c => c.creator.id === data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned));
const pv = res?.person_view;
if (pv?.person.id === data.person_view.person.id) {
pv.person.banned = data.banned;
}
this.setState(this.state);
break;
}
case UserOperation.BlockPerson: {
const data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data);
this.setPersonBlock();
break;
}
case UserOperation.PurgePerson:
case UserOperation.PurgePost:
case UserOperation.PurgeComment:
case UserOperation.PurgeCommunity: {
const { success } = wsJsonToRes<PurgeItemResponse>(msg);
if (success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
}
}
}

View file

@ -1,3 +1,4 @@
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
BlockCommunity,
@ -6,13 +7,11 @@ import {
BlockPersonResponse,
ChangePassword,
CommunityBlockView,
CommunityView,
DeleteAccount,
GetSiteResponse,
ListingType,
LoginResponse,
PersonBlockView,
PersonViewSafe,
SaveUserSettings,
SortType,
UserOperation,
@ -24,19 +23,17 @@ import { i18n, languages } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import {
capitalizeFirstLetter,
choicesConfig,
communitySelectName,
Choice,
communityToChoice,
debounce,
elementUrl,
emDash,
enableNsfw,
fetchCommunities,
fetchThemeList,
fetchUsers,
getLanguages,
isBrowser,
myAuth,
personSelectName,
personToChoice,
relTags,
setIsoData,
@ -55,15 +52,11 @@ import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
import { SearchableSelect } from "../common/searchable-select";
import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "./person-listing";
var Choices: any;
if (isBrowser()) {
Choices = require("choices.js");
}
interface SettingsState {
// TODO redo these forms
saveUserSettingsForm: {
@ -97,10 +90,7 @@ interface SettingsState {
password?: string;
};
personBlocks: PersonBlockView[];
blockPerson?: PersonViewSafe;
communityBlocks: CommunityBlockView[];
blockCommunityId: number;
blockCommunity?: CommunityView;
currentTab: string;
themeList: string[];
saveUserSettingsLoading: boolean;
@ -108,12 +98,50 @@ interface SettingsState {
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
siteRes: GetSiteResponse;
searchCommunityLoading: boolean;
searchCommunityOptions: Choice[];
searchPersonLoading: boolean;
searchPersonOptions: Choice[];
}
type FilterType = "user" | "community";
const Filter = ({
filterType,
options,
onChange,
onSearch,
loading,
}: {
filterType: FilterType;
options: Choice[];
onSearch: (text: string) => void;
onChange: (choice: Choice) => void;
loading: boolean;
}) => (
<div className="form-group row">
<label
className="col-md-4 col-form-label"
htmlFor={`block-${filterType}-filter`}
>
{i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
</label>
<div className="col-md-8">
<SearchableSelect
id={`block-${filterType}-filter`}
options={[
{ label: emDash, value: "0", disabled: true } as Choice,
].concat(options)}
loading={loading}
onChange={onChange}
onSearch={onSearch}
/>
</div>
</div>
);
export class Settings extends Component<any, SettingsState> {
private isoData = setIsoData(this.context);
private blockPersonChoices: any;
private blockCommunityChoices: any;
private subscription?: Subscription;
state: SettingsState = {
saveUserSettingsForm: {},
@ -125,10 +153,13 @@ export class Settings extends Component<any, SettingsState> {
deleteAccountForm: {},
personBlocks: [],
communityBlocks: [],
blockCommunityId: 0,
currentTab: "settings",
siteRes: this.isoData.site_res,
themeList: [],
searchCommunityLoading: false,
searchCommunityOptions: [],
searchPersonLoading: false,
searchPersonOptions: [],
};
constructor(props: any, context: any) {
@ -149,35 +180,58 @@ export class Settings extends Component<any, SettingsState> {
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
let mui = UserService.Instance.myUserInfo;
const mui = UserService.Instance.myUserInfo;
if (mui) {
let luv = mui.local_user_view;
const {
local_user: {
show_nsfw,
theme,
default_sort_type,
default_listing_type,
interface_language,
show_avatars,
show_bot_accounts,
show_scores,
show_read_posts,
show_new_post_notifs,
send_notifications_to_email,
email,
},
person: {
avatar,
banner,
display_name,
bot_account,
bio,
matrix_user_id,
},
} = mui.local_user_view;
this.state = {
...this.state,
personBlocks: mui.person_blocks,
communityBlocks: mui.community_blocks,
saveUserSettingsForm: {
...this.state.saveUserSettingsForm,
show_nsfw: luv.local_user.show_nsfw,
theme: luv.local_user.theme ? luv.local_user.theme : "browser",
default_sort_type: luv.local_user.default_sort_type,
default_listing_type: luv.local_user.default_listing_type,
interface_language: luv.local_user.interface_language,
show_nsfw,
theme: theme ?? "browser",
default_sort_type,
default_listing_type,
interface_language,
discussion_languages: mui.discussion_languages,
avatar: luv.person.avatar,
banner: luv.person.banner,
display_name: luv.person.display_name,
show_avatars: luv.local_user.show_avatars,
bot_account: luv.person.bot_account,
show_bot_accounts: luv.local_user.show_bot_accounts,
show_scores: luv.local_user.show_scores,
show_read_posts: luv.local_user.show_read_posts,
show_new_post_notifs: luv.local_user.show_new_post_notifs,
email: luv.local_user.email,
bio: luv.person.bio,
send_notifications_to_email:
luv.local_user.send_notifications_to_email,
matrix_user_id: luv.person.matrix_user_id,
avatar,
banner,
display_name,
show_avatars,
bot_account,
show_bot_accounts,
show_scores,
show_read_posts,
show_new_post_notifs,
email,
bio,
send_notifications_to_email,
matrix_user_id,
},
};
}
@ -349,9 +403,17 @@ export class Settings extends Component<any, SettingsState> {
}
blockUserCard() {
const { searchPersonLoading, searchPersonOptions } = this.state;
return (
<div>
{this.blockUserForm()}
<Filter
filterType="user"
loading={searchPersonLoading}
onChange={this.handleBlockPerson}
onSearch={this.handlePersonSearch}
options={searchPersonOptions}
/>
{this.blockedUsersList()}
</div>
);
@ -384,38 +446,18 @@ export class Settings extends Component<any, SettingsState> {
);
}
blockUserForm() {
let blockPerson = this.state.blockPerson;
return (
<div className="form-group row">
<label
className="col-md-4 col-form-label"
htmlFor="block-person-filter"
>
{i18n.t("block_user")}
</label>
<div className="col-md-8">
<select
className="form-control"
id="block-person-filter"
value={blockPerson?.person.id ?? 0}
>
<option value="0"></option>
{blockPerson && (
<option value={blockPerson.person.id}>
{personSelectName(blockPerson)}
</option>
)}
</select>
</div>
</div>
);
}
blockCommunityCard() {
const { searchCommunityLoading, searchCommunityOptions } = this.state;
return (
<div>
{this.blockCommunityForm()}
<Filter
filterType="community"
loading={searchCommunityLoading}
onChange={this.handleBlockCommunity}
onSearch={this.handleCommunitySearch}
options={searchCommunityOptions}
/>
{this.blockedCommunitiesList()}
</div>
);
@ -448,33 +490,6 @@ export class Settings extends Component<any, SettingsState> {
);
}
blockCommunityForm() {
return (
<div className="form-group row">
<label
className="col-md-4 col-form-label"
htmlFor="block-community-filter"
>
{i18n.t("block_community")}
</label>
<div className="col-md-8">
<select
className="form-control"
id="block-community-filter"
value={this.state.blockCommunityId}
>
<option value="0"></option>
{this.state.blockCommunity && (
<option value={this.state.blockCommunity.community.id}>
{communitySelectName(this.state.blockCommunity)}
</option>
)}
</select>
</div>
</div>
);
}
saveUserSettingsHtmlForm() {
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
@ -907,91 +922,57 @@ export class Settings extends Component<any, SettingsState> {
);
}
setupBlockPersonChoices() {
if (isBrowser()) {
let selectId: any = document.getElementById("block-person-filter");
if (selectId) {
this.blockPersonChoices = new Choices(selectId, choicesConfig);
this.blockPersonChoices.passedElement.element.addEventListener(
"choice",
(e: any) => {
this.handleBlockPerson(Number(e.detail.choice.value));
},
false
);
this.blockPersonChoices.passedElement.element.addEventListener(
"search",
debounce(async (e: any) => {
try {
let persons = (await fetchUsers(e.detail.value)).users;
let choices = persons.map(pvs => personToChoice(pvs));
this.blockPersonChoices.setChoices(
choices,
"value",
"label",
true
);
} catch (err) {
console.error(err);
}
}),
false
);
}
}
}
handlePersonSearch = debounce(async (text: string) => {
this.setState({ searchPersonLoading: true });
setupBlockCommunityChoices() {
if (isBrowser()) {
let selectId: any = document.getElementById("block-community-filter");
if (selectId) {
this.blockCommunityChoices = new Choices(selectId, choicesConfig);
this.blockCommunityChoices.passedElement.element.addEventListener(
"choice",
(e: any) => {
this.handleBlockCommunity(Number(e.detail.choice.value));
},
false
);
this.blockCommunityChoices.passedElement.element.addEventListener(
"search",
debounce(async (e: any) => {
try {
let communities = (await fetchCommunities(e.detail.value))
.communities;
let choices = communities.map(cv => communityToChoice(cv));
this.blockCommunityChoices.setChoices(
choices,
"value",
"label",
true
);
} catch (err) {
console.log(err);
}
}),
false
);
}
}
}
const searchPersonOptions: Choice[] = [];
handleBlockPerson(personId: number) {
let auth = myAuth();
if (auth && personId != 0) {
let blockUserForm: BlockPerson = {
person_id: personId,
if (text.length > 0) {
searchPersonOptions.push(
...(await fetchUsers(text)).users.map(personToChoice)
);
}
this.setState({
searchPersonLoading: false,
searchPersonOptions,
});
});
handleCommunitySearch = debounce(async (text: string) => {
this.setState({ searchCommunityLoading: true });
const searchCommunityOptions: Choice[] = [];
if (text.length > 0) {
searchCommunityOptions.push(
...(await fetchCommunities(text)).communities.map(communityToChoice)
);
}
this.setState({
searchCommunityLoading: false,
searchCommunityOptions,
});
});
handleBlockPerson({ value }: Choice) {
const auth = myAuth();
if (auth && value !== "0") {
const blockUserForm: BlockPerson = {
person_id: Number(value),
block: true,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
let auth = myAuth();
const auth = myAuth();
if (auth) {
let blockUserForm: BlockPerson = {
const blockUserForm: BlockPerson = {
person_id: i.recipientId,
block: false,
auth,
@ -1000,11 +981,11 @@ export class Settings extends Component<any, SettingsState> {
}
}
handleBlockCommunity(community_id: number) {
let auth = myAuth();
if (auth && community_id != 0) {
let blockCommunityForm: BlockCommunity = {
community_id,
handleBlockCommunity({ value }: Choice) {
const auth = myAuth();
if (auth && value !== "0") {
const blockCommunityForm: BlockCommunity = {
community_id: Number(value),
block: true,
auth,
};
@ -1015,9 +996,9 @@ export class Settings extends Component<any, SettingsState> {
}
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
let auth = myAuth();
const auth = myAuth();
if (auth) {
let blockCommunityForm: BlockCommunity = {
const blockCommunityForm: BlockCommunity = {
community_id: i.communityId,
block: false,
auth,
@ -1249,11 +1230,6 @@ export class Settings extends Component<any, SettingsState> {
handleSwitchTab(i: { ctx: Settings; tab: string }) {
i.ctx.setState({ currentTab: i.tab });
if (i.ctx.state.currentTab == "blocks") {
i.ctx.setupBlockPersonChoices();
i.ctx.setupBlockCommunityChoices();
}
}
parseMessage(msg: any) {

View file

@ -1,13 +1,10 @@
import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
GetCommunity,
GetCommunityResponse,
GetSiteResponse,
ListCommunities,
ListCommunitiesResponse,
ListingType,
PostView,
SortType,
UserOperation,
wsJsonToRes,
wsUserOp,
@ -17,11 +14,15 @@ import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import {
Choice,
enableDownvotes,
enableNsfw,
fetchLimit,
getIdFromString,
getQueryParams,
getQueryString,
isBrowser,
myAuth,
QueryParams,
setIsoData,
toast,
wsClient,
@ -31,13 +32,26 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { PostForm } from "./post-form";
interface CreatePostState {
listCommunitiesResponse?: ListCommunitiesResponse;
siteRes: GetSiteResponse;
loading: boolean;
export interface CreatePostProps {
communityId?: number;
}
export class CreatePost extends Component<any, CreatePostState> {
function getCreatePostQueryParams() {
return getQueryParams<CreatePostProps>({
communityId: getIdFromString,
});
}
interface CreatePostState {
siteRes: GetSiteResponse;
loading: boolean;
selectedCommunityChoice?: Choice;
}
export class CreatePost extends Component<
RouteComponentProps<Record<string, never>>,
CreatePostState
> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: CreatePostState = {
@ -45,10 +59,12 @@ export class CreatePost extends Component<any, CreatePostState> {
loading: true,
};
constructor(props: any, context: any) {
constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this);
this.handleSelectedCommunityChange =
this.handleSelectedCommunityChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
@ -59,45 +75,56 @@ export class CreatePost extends Component<any, CreatePostState> {
}
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
if (this.isoData.path === this.context.router.route.match.url) {
const communityRes = this.isoData.routeData[0] as
| GetCommunityResponse
| undefined;
if (communityRes) {
const communityChoice: Choice = {
label: communityRes.community_view.community.name,
value: communityRes.community_view.community.id.toString(),
};
this.state = {
...this.state,
selectedCommunityChoice: communityChoice,
};
}
this.state = {
...this.state,
listCommunitiesResponse: this.isoData
.routeData[0] as ListCommunitiesResponse,
loading: false,
};
} else {
this.refetch();
this.fetchCommunity();
}
}
refetch() {
let nameOrId = this.params.nameOrId;
let auth = myAuth(false);
if (nameOrId) {
if (typeof nameOrId === "string") {
let form: GetCommunity = {
name: nameOrId,
auth,
};
WebSocketService.Instance.send(wsClient.getCommunity(form));
} else {
let form: GetCommunity = {
id: nameOrId,
auth,
};
WebSocketService.Instance.send(wsClient.getCommunity(form));
}
} else {
let listCommunitiesForm: ListCommunities = {
type_: ListingType.All,
sort: SortType.TopAll,
limit: fetchLimit,
fetchCommunity() {
const { communityId } = getCreatePostQueryParams();
const auth = myAuth(false);
if (communityId) {
const form: GetCommunity = {
id: communityId,
auth,
};
WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm)
);
WebSocketService.Instance.send(wsClient.getCommunity(form));
}
}
componentDidMount(): void {
const { communityId } = getCreatePostQueryParams();
if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
this.fetchCommunity();
} else if (!communityId) {
this.setState({
selectedCommunityChoice: undefined,
loading: false,
});
}
}
@ -114,7 +141,12 @@ export class CreatePost extends Component<any, CreatePostState> {
}
render() {
let res = this.state.listCommunitiesResponse;
const { selectedCommunityChoice } = this.state;
const locationState = this.props.history.location.state as
| PostFormParams
| undefined;
return (
<div className="container-lg">
<HtmlTags
@ -126,96 +158,93 @@ export class CreatePost extends Component<any, CreatePostState> {
<Spinner large />
</h5>
) : (
res && (
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_post")}</h5>
<PostForm
communities={res.communities}
onCreate={this.handlePostCreate}
params={this.params}
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
/>
</div>
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_post")}</h5>
<PostForm
onCreate={this.handlePostCreate}
params={locationState}
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
selectedCommunityChoice={selectedCommunityChoice}
onSelectCommunity={this.handleSelectedCommunityChange}
/>
</div>
)
</div>
)}
</div>
);
}
get params(): PostFormParams {
let urlParams = new URLSearchParams(this.props.location.search);
let name = urlParams.get("community_name") ?? this.prevCommunityName;
let communityIdParam = urlParams.get("community_id");
let id = communityIdParam ? Number(communityIdParam) : this.prevCommunityId;
let nameOrId: string | number | undefined;
if (name) {
nameOrId = name;
} else if (id) {
nameOrId = id;
}
updateUrl({ communityId }: Partial<CreatePostProps>) {
const { communityId: urlCommunityId } = getCreatePostQueryParams();
let params: PostFormParams = {
name: urlParams.get("title") ?? undefined,
nameOrId,
body: urlParams.get("body") ?? undefined,
url: urlParams.get("url") ?? undefined,
const queryParams: QueryParams<CreatePostProps> = {
communityId: (communityId ?? urlCommunityId)?.toString(),
};
return params;
const locationState = this.props.history.location.state as
| PostFormParams
| undefined;
this.props.history.push(
`/create_post${getQueryString(queryParams)}`,
locationState
);
this.fetchCommunity();
}
get prevCommunityName(): string | undefined {
if (this.props.match.params.name) {
return this.props.match.params.name;
} else if (this.props.location.state) {
let lastLocation = this.props.location.state.prevPath;
if (lastLocation.includes("/c/")) {
return lastLocation.split("/c/").at(1);
}
}
return undefined;
}
get prevCommunityId(): number | undefined {
// TODO is this actually a number? Whats the real return type
let id = this.props.match.params.id;
return id ?? undefined;
handleSelectedCommunityChange(choice: Choice) {
this.updateUrl({
communityId: getIdFromString(choice?.value),
});
}
handlePostCreate(post_view: PostView) {
this.props.history.push(`/post/${post_view.post.id}`);
this.props.history.replace(`/post/${post_view.post.id}`);
}
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let listCommunitiesForm: ListCommunities = {
type_: ListingType.All,
sort: SortType.TopAll,
limit: fetchLimit,
auth: req.auth,
};
return [req.client.listCommunities(listCommunitiesForm)];
static fetchInitialData({
client,
query: { communityId },
auth,
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<any>[] {
const promises: Promise<any>[] = [];
if (communityId) {
const form: GetCommunity = {
auth,
id: getIdFromString(communityId),
};
promises.push(client.getCommunity(form));
} else {
promises.push(Promise.resolve());
}
return promises;
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
const op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
return;
} else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg);
this.setState({ listCommunitiesResponse: data, loading: false });
} else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({
listCommunitiesResponse: {
communities: [data.community_view],
}
if (op === UserOperation.GetCommunity) {
const {
community_view: {
community: { name, id },
},
} = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({
selectedCommunityChoice: { label: name, value: id.toString() },
loading: false,
});
}

View file

@ -2,7 +2,6 @@ import autosize from "autosize";
import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import {
CommunityView,
CreatePost,
EditPost,
Language,
@ -24,14 +23,13 @@ import { UserService, WebSocketService } from "../../services";
import {
archiveTodayUrl,
capitalizeFirstLetter,
choicesConfig,
communitySelectName,
Choice,
communityToChoice,
debounce,
fetchCommunities,
getIdFromString,
getSiteMetadata,
ghostArchiveUrl,
isBrowser,
isImage,
myAuth,
myFirstDiscussionLanguageId,
@ -50,26 +48,23 @@ import {
import { Icon, Spinner } from "../common/icon";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
import { SearchableSelect } from "../common/searchable-select";
import { PostListings } from "./post-listings";
var Choices: any;
if (isBrowser()) {
Choices = require("choices.js");
}
const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps {
post_view?: PostView; // If a post is given, that means this is an edit
allLanguages: Language[];
siteLanguages: number[];
communities?: CommunityView[];
params?: PostFormParams;
onCancel?(): any;
onCreate?(post: PostView): any;
onEdit?(post: PostView): any;
enableNsfw?: boolean;
enableDownvotes?: boolean;
selectedCommunityChoice?: Choice;
onSelectCommunity?: (choice: Choice) => void;
}
interface PostFormState {
@ -88,32 +83,34 @@ interface PostFormState {
loading: boolean;
imageLoading: boolean;
communitySearchLoading: boolean;
communitySearchOptions: Choice[];
previewMode: boolean;
}
export class PostForm extends Component<PostFormProps, PostFormState> {
private subscription?: Subscription;
private choices: any;
state: PostFormState = {
form: {},
loading: false,
imageLoading: false,
communitySearchLoading: false,
previewMode: false,
communitySearchOptions: [],
};
constructor(props: any, context: any) {
constructor(props: PostFormProps, context: any) {
super(props, context);
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.handleCommunitySelect = this.handleCommunitySelect.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Means its an edit
let pv = this.props.post_view;
const pv = this.props.post_view;
if (pv) {
this.state = {
...this.state,
@ -128,15 +125,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
};
}
let params = this.props.params;
const selectedCommunityChoice = this.props.selectedCommunityChoice;
if (selectedCommunityChoice) {
this.state = {
...this.state,
form: {
...this.state.form,
community_id: getIdFromString(selectedCommunityChoice.value),
},
communitySearchOptions: [selectedCommunityChoice],
};
}
const params = this.props.params;
if (params) {
this.state = {
...this.state,
form: {
...this.state.form,
name: params.name,
url: params.url,
body: params.body,
...params,
},
};
}
@ -144,8 +152,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
componentDidMount() {
setupTippy();
this.setupCommunities();
let textarea: any = document.getElementById("post-title");
const textarea: any = document.getElementById("post-title");
if (textarea) {
autosize(textarea);
}
@ -168,6 +176,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
window.onbeforeunload = null;
}
static getDerivedStateFromProps(
{ selectedCommunityChoice }: PostFormProps,
{ form, ...restState }: PostFormState
) {
return {
...restState,
form: {
...form,
community_id: getIdFromString(selectedCommunityChoice?.value),
},
};
}
render() {
let firstLang =
this.state.form.language_id ??
@ -342,26 +363,23 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
className="col-sm-2 col-form-label"
htmlFor="post-community"
>
{this.state.communitySearchLoading ? (
<Spinner />
) : (
i18n.t("community")
)}
{i18n.t("community")}
</label>
<div className="col-sm-10">
<select
className="form-control"
<SearchableSelect
id="post-community"
value={this.state.form.community_id}
onInput={linkEvent(this, this.handlePostCommunityChange)}
>
<option>{i18n.t("select_a_community")}</option>
{this.props.communities?.map(cv => (
<option key={cv.community.id} value={cv.community.id}>
{communitySelectName(cv)}
</option>
))}
</select>
options={[
{
label: i18n.t("select_a_community"),
value: "",
disabled: true,
} as Choice,
].concat(this.state.communitySearchOptions)}
loading={this.state.communitySearchLoading}
onChange={this.handleCommunitySelect}
onSearch={this.handleCommunitySearch}
/>
</div>
</div>
)}
@ -609,67 +627,41 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
});
}
setupCommunities() {
// Set up select searching
if (isBrowser()) {
let selectId: any = document.getElementById("post-community");
if (selectId) {
this.choices = new Choices(selectId, choicesConfig);
this.choices.passedElement.element.addEventListener(
"choice",
(e: any) => {
this.setState(
s => ((s.form.community_id = Number(e.detail.choice.value)), s)
);
},
false
);
this.choices.passedElement.element.addEventListener("search", () => {
this.setState({ communitySearchLoading: true });
});
this.choices.passedElement.element.addEventListener(
"search",
debounce(async (e: any) => {
try {
let communities = (await fetchCommunities(e.detail.value))
.communities;
this.choices.setChoices(
communities.map(cv => communityToChoice(cv)),
"value",
"label",
true
);
this.setState({ communitySearchLoading: false });
} catch (err) {
console.log(err);
}
}),
false
);
}
handleCommunitySearch = debounce(async (text: string) => {
const { selectedCommunityChoice } = this.props;
this.setState({ communitySearchLoading: true });
const newOptions: Choice[] = [];
if (selectedCommunityChoice) {
newOptions.push(selectedCommunityChoice);
}
let pv = this.props.post_view;
this.setState(s => ((s.form.community_id = pv?.community.id), s));
if (text.length > 0) {
newOptions.push(
...(await fetchCommunities(text)).communities.map(communityToChoice)
);
let nameOrId = this.props.params?.nameOrId;
if (nameOrId) {
if (typeof nameOrId === "string") {
let name_ = nameOrId;
let foundCommunityId = this.props.communities?.find(
r => r.community.name == name_
)?.community.id;
this.setState(s => ((s.form.community_id = foundCommunityId), s));
} else {
let id = nameOrId;
this.setState(s => ((s.form.community_id = id), s));
}
this.setState({
communitySearchOptions: newOptions,
});
}
if (isBrowser() && this.state.form.community_id) {
this.choices.setChoiceByValue(this.state.form.community_id.toString());
this.setState({
communitySearchLoading: false,
});
});
handleCommunitySelect(choice: Choice) {
if (this.props.onSelectCommunity) {
this.setState({
loading: true,
});
this.props.onSelectCommunity(choice);
this.setState({ loading: false });
}
this.setState(this.state);
}
parseMessage(msg: any) {

View file

@ -25,7 +25,7 @@ import {
} from "lemmy-js-client";
import { externalHost } from "../../env";
import { i18n } from "../../i18next";
import { BanType, PurgeType } from "../../interfaces";
import { BanType, PostFormParams, PurgeType } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
amAdmin,
@ -147,7 +147,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
render() {
let post = this.props.post_view.post;
const post = this.props.post_view.post;
return (
<div className="post-listing">
{!this.state.showEdit ? (
@ -734,7 +735,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return (
<Link
className="btn btn-link btn-animate text-muted py-0"
to={`/create_post${this.crossPostParams}`}
to={{
/* Empty string properties are required to satisfy type*/
pathname: "/create_post",
state: { ...this.crossPostParams },
hash: "",
key: "",
search: "",
}}
title={i18n.t("cross_post")}
>
<Icon icon="copy" inline />
@ -1461,18 +1469,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
}
get crossPostParams(): string {
let post = this.props.post_view.post;
let params = `?title=${encodeURIComponent(post.name)}`;
get crossPostParams(): PostFormParams {
const queryParams: PostFormParams = {};
const { name, url } = this.props.post_view.post;
if (post.url) {
params += `&url=${encodeURIComponent(post.url)}`;
queryParams.name = name;
if (url) {
queryParams.url = url;
}
let crossPostBody = this.crossPostBody();
const crossPostBody = this.crossPostBody();
if (crossPostBody) {
params += `&body=${encodeURIComponent(crossPostBody)}`;
queryParams.body = crossPostBody;
}
return params;
return queryParams;
}
crossPostBody(): string | undefined {

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import type { ParsedQs } from "qs";
/**
* This contains serialized data, it needs to be deserialized before use.
@ -20,17 +21,18 @@ declare global {
}
}
export interface InitialFetchRequest {
export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
auth?: string;
client: LemmyHttp;
path: string;
query: T;
site: GetSiteResponse;
}
export interface PostFormParams {
name?: string;
url?: string;
body?: string;
nameOrId?: string | number;
}
export enum CommentViewType {
@ -49,10 +51,10 @@ export enum BanType {
}
export enum PersonDetailsView {
Overview,
Comments,
Posts,
Saved,
Overview = "Overview",
Comments = "Comments",
Posts = "Posts",
Saved = "Saved",
}
export enum PurgeType {

View file

@ -1,4 +1,3 @@
import { Inferno } from "inferno";
import { IRouteProps } from "inferno-router/dist/Route";
import { Communities } from "./components/community/communities";
import { Community } from "./components/community/community";
@ -26,21 +25,15 @@ import { InitialFetchRequest } from "./interfaces";
interface IRoutePropsWithFetch extends IRouteProps {
// TODO Make sure this one is good.
component: Inferno.ComponentClass;
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
}
export const routes: IRoutePropsWithFetch[] = [
{
path: `/`,
component: Home,
fetchInitialData: Home.fetchInitialData,
exact: true,
component: Home,
fetchInitialData: req => Home.fetchInitialData(req),
},
{
path: `/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`,
component: Home,
fetchInitialData: req => Home.fetchInitialData(req),
},
{
path: `/login`,
@ -53,101 +46,81 @@ export const routes: IRoutePropsWithFetch[] = [
{
path: `/create_post`,
component: CreatePost,
fetchInitialData: req => CreatePost.fetchInitialData(req),
fetchInitialData: CreatePost.fetchInitialData,
},
{
path: `/create_community`,
component: CreateCommunity,
},
{
path: `/create_private_message/recipient/:recipient_id`,
path: `/create_private_message/:recipient_id`,
component: CreatePrivateMessage,
fetchInitialData: req => CreatePrivateMessage.fetchInitialData(req),
},
{
path: `/communities/listing_type/:listing_type/page/:page`,
component: Communities,
fetchInitialData: req => Communities.fetchInitialData(req),
fetchInitialData: CreatePrivateMessage.fetchInitialData,
},
{
path: `/communities`,
component: Communities,
fetchInitialData: req => Communities.fetchInitialData(req),
fetchInitialData: Communities.fetchInitialData,
},
{
path: `/post/:post_id`,
component: Post,
fetchInitialData: req => Post.fetchInitialData(req),
fetchInitialData: Post.fetchInitialData,
},
{
path: `/comment/:comment_id`,
component: Post,
fetchInitialData: req => Post.fetchInitialData(req),
},
{
path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
component: Community,
fetchInitialData: req => Community.fetchInitialData(req),
fetchInitialData: Post.fetchInitialData,
},
{
path: `/c/:name`,
component: Community,
fetchInitialData: req => Community.fetchInitialData(req),
},
{
path: `/u/:username/view/:view/sort/:sort/page/:page`,
component: Profile,
fetchInitialData: req => Profile.fetchInitialData(req),
fetchInitialData: Community.fetchInitialData,
},
{
path: `/u/:username`,
component: Profile,
fetchInitialData: req => Profile.fetchInitialData(req),
fetchInitialData: Profile.fetchInitialData,
},
{
path: `/inbox`,
component: Inbox,
fetchInitialData: req => Inbox.fetchInitialData(req),
fetchInitialData: Inbox.fetchInitialData,
},
{
path: `/settings`,
component: Settings,
},
{
path: `/modlog/community/:community_id`,
component: Modlog,
fetchInitialData: req => Modlog.fetchInitialData(req),
},
{
path: `/modlog`,
component: Modlog,
fetchInitialData: req => Modlog.fetchInitialData(req),
fetchInitialData: Modlog.fetchInitialData,
},
{
path: `/modlog/:communityId`,
component: Modlog,
fetchInitialData: Modlog.fetchInitialData,
},
{ path: `/setup`, component: Setup },
{
path: `/admin`,
component: AdminSettings,
fetchInitialData: req => AdminSettings.fetchInitialData(req),
fetchInitialData: AdminSettings.fetchInitialData,
},
{
path: `/reports`,
component: Reports,
fetchInitialData: req => Reports.fetchInitialData(req),
fetchInitialData: Reports.fetchInitialData,
},
{
path: `/registration_applications`,
component: RegistrationApplications,
fetchInitialData: req => RegistrationApplications.fetchInitialData(req),
},
{
path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
component: Search,
fetchInitialData: req => Search.fetchInitialData(req),
fetchInitialData: RegistrationApplications.fetchInitialData,
},
{
path: `/search`,
component: Search,
fetchInitialData: req => Search.fetchInitialData(req),
fetchInitialData: Search.fetchInitialData,
},
{
path: `/password_change/:token`,

View file

@ -82,6 +82,8 @@ export const concurrentImageUpload = 4;
export const relTags = "noopener nofollow";
export const emDash = "\u2014";
export type ThemeColor =
| "primary"
| "secondary"
@ -118,6 +120,14 @@ function getRandomCharFromAlphabet(alphabet: string): string {
return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
export function getIdFromString(id?: string): number | undefined {
return id && id !== "0" && !Number.isNaN(Number(id)) ? Number(id) : undefined;
}
export function getPageFromString(page?: string): number {
return page && !Number.isNaN(Number(page)) ? Number(page) : 1;
}
export function randomStr(
idDesiredLength = 20,
alphabet = DEFAULT_ALPHABET
@ -332,8 +342,11 @@ export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function routeSortTypeToEnum(sort: string): SortType {
return SortType[sort];
export function routeSortTypeToEnum(
sort: string,
defaultValue: SortType
): SortType {
return SortType[sort] ?? defaultValue;
}
export function listingTypeFromNum(type_: number): ListingType {
@ -344,16 +357,25 @@ export function sortTypeFromNum(type_: number): SortType {
return Object.values(SortType)[type_];
}
export function routeListingTypeToEnum(type: string): ListingType {
return ListingType[type];
export function routeListingTypeToEnum(
type: string,
defaultValue: ListingType
): ListingType {
return ListingType[type] ?? defaultValue;
}
export function routeDataTypeToEnum(type: string): DataType {
return DataType[capitalizeFirstLetter(type)];
export function routeDataTypeToEnum(
type: string,
defaultValue: DataType
): DataType {
return DataType[type] ?? defaultValue;
}
export function routeSearchTypeToEnum(type: string): SearchType {
return SearchType[type];
export function routeSearchTypeToEnum(
type: string,
defaultValue: SearchType
): SearchType {
return SearchType[type] ?? defaultValue;
}
export async function getSiteMetadata(url: string) {
@ -362,26 +384,34 @@ export async function getSiteMetadata(url: string) {
return client.getSiteMetadata(form);
}
export function debounce(func: any, wait = 1000, immediate = false) {
export function getDataTypeString(dt: DataType) {
return dt === DataType.Post ? "Post" : "Comment";
}
export function debounce<T extends any[], R>(
func: (...e: T) => R,
wait = 1000,
immediate = false
) {
// 'private' variable for instance
// The returned function will be able to reference this due to closure.
// Each call to the returned function will share this common timer.
let timeout: any;
let timeout: NodeJS.Timeout | null;
// Calling debounce returns a new anonymous function
return function () {
// reference the context and args for the setTimeout function
var args = arguments;
const args = arguments;
// Should the function be called now? If immediate is true
// and not already in a timeout then the answer is: Yes
var callNow = immediate && !timeout;
const callNow = immediate && !timeout;
// This is the basic debounce behaviour where you can call this
// This is the basic debounce behavior where you can call this
// function several times, but it will only execute once
// [before or after imposing a delay].
// Each time the returned function is called, the timer starts over.
clearTimeout(timeout);
clearTimeout(timeout ?? undefined);
// Set the new timeout
timeout = setTimeout(function () {
@ -400,7 +430,7 @@ export function debounce(func: any, wait = 1000, immediate = false) {
// Immediate mode and no wait timer? Execute the function..
if (callNow) func.apply(this, args);
};
} as (...e: T) => R;
}
export function getLanguages(
@ -903,47 +933,6 @@ async function communitySearch(text: string): Promise<CommunityTribute[]> {
return communities;
}
export function getListingTypeFromProps(
props: any,
defaultListingType: ListingType,
myUserInfo = UserService.Instance.myUserInfo
): ListingType {
let myLt = myUserInfo?.local_user_view.local_user.default_listing_type;
return props.match.params.listing_type
? routeListingTypeToEnum(props.match.params.listing_type)
: myLt
? Object.values(ListingType)[myLt]
: defaultListingType;
}
export function getListingTypeFromPropsNoDefault(props: any): ListingType {
return props.match.params.listing_type
? routeListingTypeToEnum(props.match.params.listing_type)
: ListingType.Local;
}
export function getDataTypeFromProps(props: any): DataType {
return props.match.params.data_type
? routeDataTypeToEnum(props.match.params.data_type)
: DataType.Post;
}
export function getSortTypeFromProps(
props: any,
myUserInfo = UserService.Instance.myUserInfo
): SortType {
let mySortType = myUserInfo?.local_user_view.local_user.default_sort_type;
return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort)
: mySortType
? Object.values(SortType)[mySortType]
: SortType.Active;
}
export function getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
}
export function getRecipientIdFromProps(props: any): number {
return props.match.params.recipient_id
? Number(props.match.params.recipient_id)
@ -960,10 +949,6 @@ export function getCommentIdFromProps(props: any): number | undefined {
return id ? Number(id) : undefined;
}
export function getUsernameFromProps(props: any): string {
return props.match.params.username;
}
export function editCommentRes(data: CommentView, comments?: CommentView[]) {
let found = comments?.find(c => c.comment.id == data.comment.id);
if (found) {
@ -1378,25 +1363,30 @@ export function showLocal(isoData: IsoData): boolean {
return linked ? linked.length > 0 : false;
}
export interface ChoicesValue {
export interface Choice {
value: string;
label: string;
disabled?: boolean;
}
export function communityToChoice(cv: CommunityView): ChoicesValue {
let choice: ChoicesValue = {
export function getUpdatedSearchId(id?: number | null, urlId?: number | null) {
return id === null
? undefined
: ((id ?? urlId) === 0 ? undefined : id ?? urlId)?.toString();
}
export function communityToChoice(cv: CommunityView): Choice {
return {
value: cv.community.id.toString(),
label: communitySelectName(cv),
};
return choice;
}
export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
let choice: ChoicesValue = {
export function personToChoice(pvs: PersonViewSafe): Choice {
return {
value: pvs.person.id.toString(),
label: personSelectName(pvs),
};
return choice;
}
export async function fetchCommunities(q: string) {
@ -1427,49 +1417,17 @@ export async function fetchUsers(q: string) {
return client.search(form);
}
export const choicesConfig = {
shouldSort: false,
searchResultLimit: fetchLimit,
classNames: {
containerOuter: "choices custom-select px-0",
containerInner:
"choices__inner bg-secondary border-0 py-0 modlog-choices-font-size",
input: "form-control",
inputCloned: "choices__input--cloned",
list: "choices__list",
listItems: "choices__list--multiple",
listSingle: "choices__list--single py-0",
listDropdown: "choices__list--dropdown",
item: "choices__item bg-secondary",
itemSelectable: "choices__item--selectable",
itemDisabled: "choices__item--disabled",
itemChoice: "choices__item--choice",
placeholder: "choices__placeholder",
group: "choices__group",
groupHeading: "choices__heading",
button: "choices__button",
activeState: "is-active",
focusState: "is-focused",
openState: "is-open",
disabledState: "is-disabled",
highlightedState: "text-info",
selectedState: "text-info",
flippedState: "is-flipped",
loadingState: "is-loading",
noResults: "has-no-results",
noChoices: "has-no-choices",
},
};
export function communitySelectName(cv: CommunityView): string {
return cv.community.local
? cv.community.title
: `${hostname(cv.community.actor_id)}/${cv.community.title}`;
}
export function personSelectName(pvs: PersonViewSafe): string {
let pName = pvs.person.display_name ?? pvs.person.name;
return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
export function personSelectName({
person: { display_name, name, local, actor_id },
}: PersonViewSafe): string {
const pName = display_name ?? name;
return local ? pName : `${hostname(actor_id)}/${pName}`;
}
export function initializeSite(site: GetSiteResponse) {
@ -1505,12 +1463,6 @@ export function isBanned(ps: PersonSafe): boolean {
}
}
export function pushNotNull(array: any[], new_item?: any) {
if (new_item) {
array.push(...new_item);
}
}
export function myAuth(throwErr = true): string | undefined {
return UserService.Instance.auth(throwErr);
}
@ -1524,14 +1476,17 @@ export function enableNsfw(siteRes: GetSiteResponse): boolean {
}
export function postToCommentSortType(sort: SortType): CommentSortType {
if ([SortType.Active, SortType.Hot].includes(sort)) {
return CommentSortType.Hot;
} else if ([SortType.New, SortType.NewComments].includes(sort)) {
return CommentSortType.New;
} else if (sort == SortType.Old) {
return CommentSortType.Old;
} else {
return CommentSortType.Top;
switch (sort) {
case SortType.Active:
case SortType.Hot:
return CommentSortType.Hot;
case SortType.New:
case SortType.NewComments:
return CommentSortType.New;
case SortType.Old:
return CommentSortType.Old;
default:
return CommentSortType.Top;
}
}
@ -1553,7 +1508,8 @@ export function canCreateCommunity(
siteRes: GetSiteResponse,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
let adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
const adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
// TODO: Make this check if user is logged on as well
return !adminOnly || amAdmin(myUserInfo);
}
@ -1651,3 +1607,36 @@ const groupBy = <T>(
(acc[predicate(value, index, array)] ||= []).push(value);
return acc;
}, {} as { [key: string]: T[] });
export type QueryParams<T extends Record<string, any>> = {
[key in keyof T]?: string;
};
export function getQueryParams<T extends Record<string, any>>(processors: {
[K in keyof T]: (param: string) => T[K];
}): T {
if (isBrowser()) {
const searchParams = new URLSearchParams(window.location.search);
return Array.from(Object.entries(processors)).reduce(
(acc, [key, process]) => ({
...acc,
[key]: process(searchParams.get(key)),
}),
{} as T
);
}
return {} as T;
}
export function getQueryString<T extends Record<string, string | undefined>>(
obj: T
) {
return Object.entries(obj)
.filter(([, val]) => val !== undefined && val !== null)
.reduce(
(acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`,
"?"
);
}

View file

@ -1115,7 +1115,7 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
@ -1534,14 +1534,6 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/node-fetch@^2.6.2":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "18.7.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f"
@ -2530,15 +2522,6 @@ check-password-strength@^2.0.7:
resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.7.tgz#d8fd6c1a274267c7ddd9cd15c71a3cfb6ad35baa"
integrity sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==
choices.js@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-10.2.0.tgz#3fe915a12b469a87b9552cd7158e413c8f65ab4f"
integrity sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==
dependencies:
deepmerge "^4.2.2"
fuse.js "^6.6.2"
redux "^4.2.0"
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -3980,15 +3963,6 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@ -4104,11 +4078,6 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -6089,7 +6058,7 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-fetch@2.6.7, node-fetch@^2.6.1:
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -7386,13 +7355,6 @@ rechoir@^0.8.0:
dependencies:
resolve "^1.20.0"
redux@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"