Removing monads. Fixes #884 (#886)

* Removing monads. Fixes #884

* Fixing post fetching.

* Dont show not_logged_in error for navbar.

* Adding the lemmy-js-client RC.

* Fixing registration application mode
This commit is contained in:
Dessalines 2023-01-04 11:56:24 -05:00 committed by GitHub
parent 37c200571b
commit b64f47cfe9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 5186 additions and 6250 deletions

@ -1 +1 @@
Subproject commit c97005696d132acf2cad3506df4f3c2256142349 Subproject commit 975c922271f27920aef51a2c3ae9f24714c44004

View file

@ -23,13 +23,11 @@
"@babel/preset-env": "7.19.3", "@babel/preset-env": "7.19.3",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.18.9", "@babel/runtime": "^7.18.9",
"@sniptt/monads": "^0.5.10",
"autosize": "^5.0.1", "autosize": "^5.0.1",
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
"babel-plugin-inferno": "^6.5.0", "babel-plugin-inferno": "^6.5.0",
"check-password-strength": "^2.0.7", "check-password-strength": "^2.0.7",
"choices.js": "^10.1.0", "choices.js": "^10.1.0",
"class-transformer": "^0.5.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
@ -47,7 +45,7 @@
"inferno-server": "^8.0.5", "inferno-server": "^8.0.5",
"isomorphic-cookie": "^1.2.4", "isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lemmy-js-client": "0.17.0-rc.57", "lemmy-js-client": "0.17.0-rc.61",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
"markdown-it-footnote": "^3.0.3", "markdown-it-footnote": "^3.0.3",
@ -57,7 +55,6 @@
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0", "run-node-webpack-plugin": "^1.3.0",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",

View file

@ -1,10 +1,9 @@
import { hydrate } from "inferno-hydrate"; import { hydrate } from "inferno-hydrate";
import { BrowserRouter } from "inferno-router"; import { BrowserRouter } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { App } from "../shared/components/app/app"; import { App } from "../shared/components/app/app";
import { convertWindowJson, initializeSite } from "../shared/utils"; import { initializeSite } from "../shared/utils";
const site = convertWindowJson(GetSiteResponse, window.isoData.site_res); const site = window.isoData.site_res;
initializeSite(site); initializeSite(site);
const wrapper = ( const wrapper = (
@ -13,4 +12,7 @@ const wrapper = (
</BrowserRouter> </BrowserRouter>
); );
hydrate(wrapper, document.getElementById("root")); let root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}

View file

@ -1,5 +1,3 @@
import { None, Option } from "@sniptt/monads";
import { serialize as serializeO } from "class-transformer";
import express from "express"; import express from "express";
import fs from "fs"; import fs from "fs";
import { IncomingHttpHeaders } from "http"; import { IncomingHttpHeaders } from "http";
@ -7,7 +5,7 @@ import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router"; import { matchPath, StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server"; import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie"; import IsomorphicCookie from "isomorphic-cookie";
import { GetSite, GetSiteResponse, LemmyHttp, toOption } from "lemmy-js-client"; import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import path from "path"; import path from "path";
import process from "process"; import process from "process";
import serialize from "serialize-javascript"; import serialize from "serialize-javascript";
@ -114,11 +112,11 @@ server.get("/css/themelist", async (_req, res) => {
// server.use(cookieParser()); // server.use(cookieParser());
server.get("/*", async (req, res) => { server.get("/*", async (req, res) => {
try { try {
const activeRoute = routes.find(route => matchPath(req.path, route)) || {}; const activeRoute = routes.find(route => matchPath(req.path, route));
const context = {} as any; const context = {} as any;
let auth: Option<string> = toOption(IsomorphicCookie.load("jwt", req)); let auth: string | undefined = IsomorphicCookie.load("jwt", req);
let getSiteForm = new GetSite({ auth }); let getSiteForm: GetSite = { auth };
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
@ -138,14 +136,14 @@ server.get("/*", async (req, res) => {
console.error( console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie" "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
); );
getSiteForm.auth = None; getSiteForm.auth = undefined;
initialFetchReq.auth = None; initialFetchReq.auth = undefined;
try_site = await initialFetchReq.client.getSite(getSiteForm); try_site = await initialFetchReq.client.getSite(getSiteForm);
} }
let site: GetSiteResponse = try_site; let site: GetSiteResponse = try_site;
initializeSite(site); initializeSite(site);
if (activeRoute.fetchInitialData) { if (activeRoute?.fetchInitialData) {
promises.push(...activeRoute.fetchInitialData(initialFetchReq)); promises.push(...activeRoute.fetchInitialData(initialFetchReq));
} }
@ -193,7 +191,7 @@ server.get("/*", async (req, res) => {
<!DOCTYPE html> <!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()} lang="en"> <html ${helmet.htmlAttributes.toString()} lang="en">
<head> <head>
<script>window.isoData = ${serializeO(isoData)}</script> <script>window.isoData = ${JSON.stringify(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script> <script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile --> <!-- A remote debugging utility for mobile -->
@ -246,14 +244,17 @@ server.listen(Number(port), hostname, () => {
function setForwardedHeaders(headers: IncomingHttpHeaders): { function setForwardedHeaders(headers: IncomingHttpHeaders): {
[key: string]: string; [key: string]: string;
} { } {
let out = { let out: { [key: string]: string } = {};
host: headers.host, if (headers.host) {
}; out.host = headers.host;
if (headers["x-real-ip"]) {
out["x-real-ip"] = headers["x-real-ip"];
} }
if (headers["x-forwarded-for"]) { let realIp = headers["x-real-ip"];
out["x-forwarded-for"] = headers["x-forwarded-for"]; if (realIp) {
out["x-real-ip"] = realIp as string;
}
let forwardedFor = headers["x-forwarded-for"];
if (forwardedFor) {
out["x-forwarded-for"] = forwardedFor as string;
} }
return out; return out;

View file

@ -19,14 +19,14 @@ export class App extends Component<any, any> {
render() { render() {
let siteRes = this.isoData.site_res; let siteRes = this.isoData.site_res;
let siteView = siteRes.site_view; let siteView = siteRes.site_view;
let icon = siteView.site.icon;
return ( return (
<> <>
<Provider i18next={i18n}> <Provider i18next={i18n}>
<div id="app"> <div id="app">
<Theme defaultTheme={siteView.local_site.default_theme} /> <Theme defaultTheme={siteView.local_site.default_theme} />
{siteView.site.icon.match({ {icon && (
some: icon => (
<Helmet> <Helmet>
<link <link
id="favicon" id="favicon"
@ -36,20 +36,20 @@ export class App extends Component<any, any> {
/> />
<link rel="apple-touch-icon" href={icon || favIconPngUrl} /> <link rel="apple-touch-icon" href={icon || favIconPngUrl} />
</Helmet> </Helmet>
), )}
none: <></>,
})}
<Navbar siteRes={siteRes} /> <Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1"> <div className="mt-4 p-0 fl-1">
<Switch> <Switch>
{routes.map(({ path, exact, component: C, ...rest }) => ( {routes.map(
({ path, exact, component: Component, ...rest }) => (
<Route <Route
key={path} key={path}
path={path} path={path}
exact={exact} exact={exact}
render={props => <C {...props} {...rest} />} render={props => <Component {...props} {...rest} />}
/> />
))} )
)}
<Route render={props => <NoMatch {...props} />} /> <Route render={props => <NoMatch {...props} />} />
</Switch> </Switch>
</div> </div>

View file

@ -32,7 +32,7 @@ export class Footer extends Component<FooterProps, any> {
{i18n.t("modlog")} {i18n.t("modlog")}
</NavLink> </NavLink>
</li> </li>
{this.props.site.site_view.local_site.legal_information.isSome() && ( {this.props.site.site_view.local_site.legal_information && (
<li className="nav-item"> <li className="nav-item">
<NavLink className="nav-link" to="/legal"> <NavLink className="nav-link" to="/legal">
{i18n.t("legal_information")} {i18n.t("legal_information")}

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Component, createRef, linkEvent, RefObject } from "inferno";
import { NavLink } from "inferno-router"; import { NavLink } from "inferno-router";
import { import {
@ -20,10 +19,10 @@ import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amAdmin, amAdmin,
auth,
canCreateCommunity, canCreateCommunity,
donateLemmyUrl, donateLemmyUrl,
isBrowser, isBrowser,
myAuth,
notifyComment, notifyComment,
notifyPrivateMessage, notifyPrivateMessage,
numToSI, numToSI,
@ -57,7 +56,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
private unreadReportCountSub: Subscription; private unreadReportCountSub: Subscription;
private unreadApplicationCountSub: Subscription; private unreadApplicationCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>; private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = { state: NavbarState = {
unreadInboxCount: 0, unreadInboxCount: 0,
unreadReportCount: 0, unreadReportCount: 0,
unreadApplicationCount: 0, unreadApplicationCount: 0,
@ -70,7 +69,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
@ -82,11 +80,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
this.searchTextField = createRef(); this.searchTextField = createRef();
// On the first load, check the unreads // On the first load, check the unreads
if (UserService.Instance.myUserInfo.isSome()) { let auth = myAuth(false);
if (auth && UserService.Instance.myUserInfo) {
this.requestNotificationPermission(); this.requestNotificationPermission();
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.userJoin({ wsClient.userJoin({
auth: auth().unwrap(), auth,
}) })
); );
@ -143,22 +142,22 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
// TODO class active corresponding to current page // TODO class active corresponding to current page
navbar() { navbar() {
let siteView = this.props.siteRes.site_view; let siteView = this.props.siteRes.site_view;
let person = UserService.Instance.myUserInfo?.local_user_view.person;
return ( return (
<nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3"> <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
<div className="container-lg"> <div className="container-lg">
<NavLink <NavLink
to="/" to="/"
onMouseUp={linkEvent(this, this.handleHideExpandNavbar)} onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
title={siteView.site.description.unwrapOr(siteView.site.name)} title={siteView.site.description ?? siteView.site.name}
className="d-flex align-items-center navbar-brand mr-md-3" className="d-flex align-items-center navbar-brand mr-md-3"
> >
{siteView.site.icon.match({ {siteView.site.icon && showAvatars() && (
some: icon => showAvatars() && <PictrsImage src={icon} icon />, <PictrsImage src={siteView.site.icon} icon />
none: <></>, )}
})}
{siteView.site.name} {siteView.site.name}
</NavLink> </NavLink>
{UserService.Instance.myUserInfo.isSome() && ( {UserService.Instance.myUserInfo && (
<> <>
<ul className="navbar-nav ml-auto"> <ul className="navbar-nav ml-auto">
<li className="nav-item"> <li className="nav-item">
@ -341,7 +340,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{UserService.Instance.myUserInfo.isSome() ? ( {UserService.Instance.myUserInfo ? (
<> <>
<ul className="navbar-nav my-2"> <ul className="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
@ -409,10 +408,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{UserService.Instance.myUserInfo {person && (
.map(m => m.local_user_view.person)
.match({
some: person => (
<ul className="navbar-nav"> <ul className="navbar-nav">
<li className="nav-item dropdown"> <li className="nav-item dropdown">
<button <button
@ -423,14 +419,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
aria-expanded="false" aria-expanded="false"
> >
<span> <span>
{showAvatars() && {showAvatars() && person.avatar && (
person.avatar.match({ <PictrsImage src={person.avatar} icon />
some: avatar => ( )}
<PictrsImage src={avatar} icon /> {person.display_name ?? person.name}
),
none: <></>,
})}
{person.display_name.unwrapOr(person.name)}
</span> </span>
</button> </button>
{this.state.showDropdown && ( {this.state.showDropdown && (
@ -467,10 +459,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<li className="nav-item"> <li className="nav-item">
<button <button
className="nav-link btn btn-link" className="nav-link btn btn-link"
onClick={linkEvent( onClick={linkEvent(this, this.handleLogoutClick)}
this,
this.handleLogoutClick
)}
title="test" title="test"
> >
<Icon icon="log-out" classes="mr-1" /> <Icon icon="log-out" classes="mr-1" />
@ -481,9 +470,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
)} )}
</li> </li>
</ul> </ul>
), )}
none: <></>,
})}
</> </>
) : ( ) : (
<ul className="navbar-nav my-2"> <ul className="navbar-nav my-2">
@ -516,11 +503,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
get moderatesSomething(): boolean { get moderatesSomething(): boolean {
return ( let mods = UserService.Instance.myUserInfo?.moderates;
amAdmin() || let moderatesS = (mods && mods.length > 0) || false;
UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([]) return amAdmin() || moderatesS;
.length > 0
);
} }
handleToggleExpandNavbar(i: Navbar) { handleToggleExpandNavbar(i: Navbar) {
@ -544,9 +529,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
event.preventDefault(); event.preventDefault();
i.setState({ toggleSearch: true }); i.setState({ toggleSearch: true });
i.searchTextField.current.focus(); i.searchTextField.current?.focus();
const offsetWidth = i.searchTextField.current.offsetWidth; const offsetWidth = i.searchTextField.current?.offsetWidth;
if (i.state.searchParam && offsetWidth > 100) { if (i.state.searchParam && offsetWidth && offsetWidth > 100) {
i.updateUrl(); i.updateUrl();
} }
} }
@ -576,111 +561,93 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
console.log(i18n.t("websocket_reconnected")); console.log(i18n.t("websocket_reconnected"));
if (UserService.Instance.myUserInfo.isSome()) { let auth = myAuth(false);
if (UserService.Instance.myUserInfo && auth) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.userJoin({ wsClient.userJoin({
auth: auth().unwrap(), auth,
}) })
); );
this.fetchUnreads(); this.fetchUnreads();
} }
} else if (op == UserOperation.GetUnreadCount) { } else if (op == UserOperation.GetUnreadCount) {
let data = wsJsonToRes<GetUnreadCountResponse>( let data = wsJsonToRes<GetUnreadCountResponse>(msg);
msg,
GetUnreadCountResponse
);
this.setState({ this.setState({
unreadInboxCount: data.replies + data.mentions + data.private_messages, unreadInboxCount: data.replies + data.mentions + data.private_messages,
}); });
this.sendUnreadCount(); this.sendUnreadCount();
} else if (op == UserOperation.GetReportCount) { } else if (op == UserOperation.GetReportCount) {
let data = wsJsonToRes<GetReportCountResponse>( let data = wsJsonToRes<GetReportCountResponse>(msg);
msg,
GetReportCountResponse
);
this.setState({ this.setState({
unreadReportCount: unreadReportCount:
data.post_reports + data.post_reports +
data.comment_reports + data.comment_reports +
data.private_message_reports.unwrapOr(0), (data.private_message_reports ?? 0),
}); });
this.sendReportUnread(); this.sendReportUnread();
} else if (op == UserOperation.GetUnreadRegistrationApplicationCount) { } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>( let data =
msg, wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
GetUnreadRegistrationApplicationCountResponse
);
this.setState({ unreadApplicationCount: data.registration_applications }); this.setState({ unreadApplicationCount: data.registration_applications });
this.sendApplicationUnread(); this.sendApplicationUnread();
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
let mui = UserService.Instance.myUserInfo;
UserService.Instance.myUserInfo.match({ if (
some: mui => { mui &&
if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) { data.recipient_ids.includes(mui.local_user_view.local_user.id)
) {
this.setState({ this.setState({
unreadInboxCount: this.state.unreadInboxCount + 1, unreadInboxCount: this.state.unreadInboxCount + 1,
}); });
this.sendUnreadCount(); this.sendUnreadCount();
notifyComment(data.comment_view, this.context.router); notifyComment(data.comment_view, this.context.router);
} }
},
none: void 0,
});
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg,
PrivateMessageResponse
);
UserService.Instance.myUserInfo.match({
some: mui => {
if ( if (
data.private_message_view.recipient.id == data.private_message_view.recipient.id ==
mui.local_user_view.person.id UserService.Instance.myUserInfo?.local_user_view.person.id
) { ) {
this.setState({ this.setState({
unreadInboxCount: this.state.unreadInboxCount + 1, unreadInboxCount: this.state.unreadInboxCount + 1,
}); });
this.sendUnreadCount(); this.sendUnreadCount();
notifyPrivateMessage( notifyPrivateMessage(data.private_message_view, this.context.router);
data.private_message_view,
this.context.router
);
} }
},
none: void 0,
});
} }
} }
fetchUnreads() { fetchUnreads() {
console.log("Fetching inbox unreads..."); console.log("Fetching inbox unreads...");
let unreadForm = new GetUnreadCount({ let auth = myAuth();
auth: auth().unwrap(), if (auth) {
}); let unreadForm: GetUnreadCount = {
auth,
};
WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm)); WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
console.log("Fetching reports..."); console.log("Fetching reports...");
let reportCountForm = new GetReportCount({ let reportCountForm: GetReportCount = {
community_id: None, auth,
auth: auth().unwrap(), };
});
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm)); WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
if (amAdmin()) { if (amAdmin()) {
console.log("Fetching applications..."); console.log("Fetching applications...");
let applicationCountForm = new GetUnreadRegistrationApplicationCount({ let applicationCountForm: GetUnreadRegistrationApplicationCount = {
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getUnreadRegistrationApplicationCount(applicationCountForm) wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
); );
} }
} }
}
get currentLocation() { get currentLocation() {
return this.context.router.history.location.pathname; return this.context.router.history.location.pathname;
@ -703,7 +670,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
requestNotificationPermission() { requestNotificationPermission() {
if (UserService.Instance.myUserInfo.isSome()) { if (UserService.Instance.myUserInfo) {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
if (!Notification) { if (!Notification) {
toast(i18n.t("notifications_error"), "danger"); toast(i18n.t("notifications_error"), "danger");

View file

@ -9,19 +9,15 @@ interface Props {
export class Theme extends Component<Props> { export class Theme extends Component<Props> {
render() { render() {
let user = UserService.Instance.myUserInfo; let user = UserService.Instance.myUserInfo;
let hasTheme = user let hasTheme = user?.local_user_view.local_user.theme !== "browser";
.map(m => m.local_user_view.local_user.theme !== "browser")
.unwrapOr(false);
if (hasTheme) { if (user && hasTheme) {
return ( return (
<Helmet> <Helmet>
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href={`/css/themes/${ href={`/css/themes/${user.local_user_view.local_user.theme}.css`}
user.unwrap().local_user_view.local_user.theme
}.css`}
/> />
</Helmet> </Helmet>
); );

View file

@ -1,4 +1,3 @@
import { Either, None, Option, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -16,8 +15,8 @@ import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
myAuth,
myFirstDiscussionLanguageId, myFirstDiscussionLanguageId,
wsClient, wsClient,
wsSubscribe, wsSubscribe,
@ -29,7 +28,7 @@ interface CommentFormProps {
/** /**
* Can either be the parent, or the editable comment. The right side is a postId. * Can either be the parent, or the editable comment. The right side is a postId.
*/ */
node: Either<CommentNodeI, number>; node: CommentNodeI | number;
edit?: boolean; edit?: boolean;
disabled?: boolean; disabled?: boolean;
focus?: boolean; focus?: boolean;
@ -41,19 +40,19 @@ interface CommentFormProps {
interface CommentFormState { interface CommentFormState {
buttonTitle: string; buttonTitle: string;
finished: boolean; finished: boolean;
formId: Option<string>; formId?: string;
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private subscription: Subscription; private subscription?: Subscription;
private emptyState: CommentFormState = { state: CommentFormState = {
buttonTitle: this.props.node.isRight() buttonTitle:
typeof this.props.node === "number"
? capitalizeFirstLetter(i18n.t("post")) ? capitalizeFirstLetter(i18n.t("post"))
: this.props.edit : this.props.edit
? capitalizeFirstLetter(i18n.t("save")) ? capitalizeFirstLetter(i18n.t("save"))
: capitalizeFirstLetter(i18n.t("reply")), : capitalizeFirstLetter(i18n.t("reply")),
finished: false, finished: false,
formId: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -62,50 +61,46 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
this.handleCommentSubmit = this.handleCommentSubmit.bind(this); this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
render() { render() {
let initialContent = this.props.node.match({ let initialContent =
left: node => typeof this.props.node !== "number"
this.props.edit ? Some(node.comment_view.comment.content) : None, ? this.props.edit
right: () => None, ? this.props.node.comment_view.comment.content
}); : undefined
: undefined;
let selectedLang = this.props.node let selectedLang =
.left() typeof this.props.node !== "number"
.map(n => n.comment_view.comment.language_id) ? this.props.node.comment_view.comment.language_id
.or( : myFirstDiscussionLanguageId(
myFirstDiscussionLanguageId(
this.props.allLanguages, this.props.allLanguages,
this.props.siteLanguages, this.props.siteLanguages,
UserService.Instance.myUserInfo UserService.Instance.myUserInfo
)
); );
return ( return (
<div className="mb-3"> <div className="mb-3">
{UserService.Instance.myUserInfo.isSome() ? ( {UserService.Instance.myUserInfo ? (
<MarkdownTextArea <MarkdownTextArea
initialContent={initialContent} initialContent={initialContent}
initialLanguageId={selectedLang} initialLanguageId={selectedLang}
showLanguage showLanguage
buttonTitle={Some(this.state.buttonTitle)} buttonTitle={this.state.buttonTitle}
maxLength={None}
finished={this.state.finished} finished={this.state.finished}
replyType={this.props.node.isLeft()} replyType={typeof this.props.node !== "number"}
focus={this.props.focus} focus={this.props.focus}
disabled={this.props.disabled} disabled={this.props.disabled}
onSubmit={this.handleCommentSubmit} onSubmit={this.handleCommentSubmit}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
placeholder={Some(i18n.t("comment_here"))} placeholder={i18n.t("comment_here")}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
/> />
@ -125,55 +120,55 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
handleCommentSubmit(msg: { handleCommentSubmit(msg: {
val: Option<string>; val: string;
formId: string; formId: string;
languageId: Option<number>; languageId?: number;
}) { }) {
let content = msg.val; let content = msg.val;
let language_id = msg.languageId; let language_id = msg.languageId;
this.setState({ formId: Some(msg.formId) }); let node = this.props.node;
this.props.node.match({ this.setState({ formId: msg.formId });
left: node => {
if (this.props.edit) { let auth = myAuth();
let form = new EditComment({ if (auth) {
if (typeof node === "number") {
let postId = node;
let form: CreateComment = {
content,
form_id: this.state.formId,
post_id: postId,
language_id,
auth,
};
WebSocketService.Instance.send(wsClient.createComment(form));
} else {
if (this.props.edit) {
let form: EditComment = {
content, content,
distinguished: None,
form_id: this.state.formId, form_id: this.state.formId,
comment_id: node.comment_view.comment.id, comment_id: node.comment_view.comment.id,
language_id, language_id,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.editComment(form)); WebSocketService.Instance.send(wsClient.editComment(form));
} else { } else {
let form = new CreateComment({ let form: CreateComment = {
content: content.unwrap(), content,
form_id: this.state.formId, form_id: this.state.formId,
post_id: node.comment_view.post.id, post_id: node.comment_view.post.id,
parent_id: Some(node.comment_view.comment.id), parent_id: node.comment_view.comment.id,
language_id, language_id,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.createComment(form)); WebSocketService.Instance.send(wsClient.createComment(form));
} }
}, }
right: postId => { }
let form = new CreateComment({
content: content.unwrap(),
form_id: this.state.formId,
post_id: postId,
parent_id: None,
language_id,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.createComment(form));
},
});
this.setState(this.state);
} }
handleReplyCancel() { handleReplyCancel() {
this.props.onReplyCancel(); this.props.onReplyCancel?.();
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -181,15 +176,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
console.log(msg); console.log(msg);
// Only do the showing and hiding if logged in // Only do the showing and hiding if logged in
if (UserService.Instance.myUserInfo.isSome()) { if (UserService.Instance.myUserInfo) {
if ( if (
op == UserOperation.CreateComment || op == UserOperation.CreateComment ||
op == UserOperation.EditComment op == UserOperation.EditComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
// This only finishes this form, if the randomly generated form_id matches the one received // This only finishes this form, if the randomly generated form_id matches the one received
if (this.state.formId.unwrapOr("") == data.form_id.unwrapOr("")) { if (this.state.formId == data.form_id) {
this.setState({ finished: true }); this.setState({ finished: true });
// Necessary because it broke tribute for some reason // Necessary because it broke tribute for some reason

View file

@ -1,4 +1,3 @@
import { Left, None, Option, Some } from "@sniptt/monads";
import classNames from "classnames"; import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -27,7 +26,6 @@ import {
PurgePerson, PurgePerson,
RemoveComment, RemoveComment,
SaveComment, SaveComment,
toUndefined,
TransferCommunity, TransferCommunity,
} from "lemmy-js-client"; } from "lemmy-js-client";
import moment from "moment"; import moment from "moment";
@ -36,7 +34,6 @@ import { BanType, CommentViewType, PurgeType } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amCommunityCreator, amCommunityCreator,
auth,
canAdmin, canAdmin,
canMod, canMod,
colorList, colorList,
@ -47,6 +44,7 @@ import {
isMod, isMod,
mdToHtml, mdToHtml,
mdToHtmlNoImages, mdToHtmlNoImages,
myAuth,
numToSI, numToSI,
setupTippy, setupTippy,
showScores, showScores,
@ -63,14 +61,14 @@ interface CommentNodeState {
showReply: boolean; showReply: boolean;
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean; showRemoveDialog: boolean;
removeReason: Option<string>; removeReason?: string;
showBanDialog: boolean; showBanDialog: boolean;
removeData: boolean; removeData: boolean;
banReason: Option<string>; banReason?: string;
banExpireDays: Option<number>; banExpireDays?: number;
banType: BanType; banType: BanType;
showPurgeDialog: boolean; showPurgeDialog: boolean;
purgeReason: Option<string>; purgeReason?: string;
purgeType: PurgeType; purgeType: PurgeType;
purgeLoading: boolean; purgeLoading: boolean;
showConfirmTransferSite: boolean; showConfirmTransferSite: boolean;
@ -81,8 +79,8 @@ interface CommentNodeState {
viewSource: boolean; viewSource: boolean;
showAdvanced: boolean; showAdvanced: boolean;
showReportDialog: boolean; showReportDialog: boolean;
reportReason: string; reportReason?: string;
my_vote: Option<number>; my_vote?: number;
score: number; score: number;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;
@ -92,8 +90,8 @@ interface CommentNodeState {
interface CommentNodeProps { interface CommentNodeProps {
node: CommentNodeI; node: CommentNodeI;
moderators: Option<CommunityModeratorView[]>; moderators?: CommunityModeratorView[];
admins: Option<PersonViewSafe[]>; admins?: PersonViewSafe[];
noBorder?: boolean; noBorder?: boolean;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
@ -101,7 +99,7 @@ interface CommentNodeProps {
markable?: boolean; markable?: boolean;
showContext?: boolean; showContext?: boolean;
showCommunity?: boolean; showCommunity?: boolean;
enableDownvotes: boolean; enableDownvotes?: boolean;
viewType: CommentViewType; viewType: CommentViewType;
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
@ -109,19 +107,15 @@ interface CommentNodeProps {
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
private emptyState: CommentNodeState = { state: CommentNodeState = {
showReply: false, showReply: false,
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
removeReason: None,
showBanDialog: false, showBanDialog: false,
removeData: false, removeData: false,
banReason: None,
banExpireDays: None,
banType: BanType.Community, banType: BanType.Community,
showPurgeDialog: false, showPurgeDialog: false,
purgeLoading: false, purgeLoading: false,
purgeReason: None,
purgeType: PurgeType.Person, purgeType: PurgeType.Person,
collapsed: false, collapsed: false,
viewSource: false, viewSource: false,
@ -131,7 +125,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showConfirmAppointAsMod: false, showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false, showConfirmAppointAsAdmin: false,
showReportDialog: false, showReportDialog: false,
reportReason: null,
my_vote: this.props.node.comment_view.my_vote, my_vote: this.props.node.comment_view.my_vote,
score: this.props.node.comment_view.counts.score, score: this.props.node.comment_view.counts.score,
upvotes: this.props.node.comment_view.counts.upvotes, upvotes: this.props.node.comment_view.counts.upvotes,
@ -143,7 +136,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handleCommentUpvote = this.handleCommentUpvote.bind(this); this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
this.handleCommentDownvote = this.handleCommentDownvote.bind(this); this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
@ -166,37 +158,35 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let node = this.props.node; let node = this.props.node;
let cv = this.props.node.comment_view; let cv = this.props.node.comment_view;
let purgeTypeText: string; let purgeTypeText =
if (this.state.purgeType == PurgeType.Comment) { this.state.purgeType == PurgeType.Comment
purgeTypeText = i18n.t("purge_comment"); ? i18n.t("purge_comment")
} else if (this.state.purgeType == PurgeType.Person) { : `${i18n.t("purge")} ${cv.creator.name}`;
purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
}
let canMod_ = canMod( let canMod_ = canMod(
cv.creator.id,
this.props.moderators, this.props.moderators,
this.props.admins, this.props.admins
cv.creator.id
); );
let canModOnSelf = canMod( let canModOnSelf = canMod(
cv.creator.id,
this.props.moderators, this.props.moderators,
this.props.admins, this.props.admins,
cv.creator.id,
UserService.Instance.myUserInfo, UserService.Instance.myUserInfo,
true true
); );
let canAdmin_ = canAdmin(this.props.admins, cv.creator.id); let canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
let canAdminOnSelf = canAdmin( let canAdminOnSelf = canAdmin(
this.props.admins,
cv.creator.id, cv.creator.id,
this.props.admins,
UserService.Instance.myUserInfo, UserService.Instance.myUserInfo,
true true
); );
let isMod_ = isMod(this.props.moderators, cv.creator.id); let isMod_ = isMod(cv.creator.id, this.props.moderators);
let isAdmin_ = isAdmin(this.props.admins, cv.creator.id); let isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
let amCommunityCreator_ = amCommunityCreator( let amCommunityCreator_ = amCommunityCreator(
this.props.moderators, cv.creator.id,
cv.creator.id this.props.moderators
); );
let borderColor = this.props.node.depth let borderColor = this.props.node.depth
@ -227,15 +217,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.props.node.comment_view.comment.distinguished, this.props.node.comment_view.comment.distinguished,
})} })}
style={ style={
!this.props.noIndent && !this.props.noIndent && this.props.node.depth
this.props.node.depth && ? `border-left: 2px ${borderColor} solid !important`
`border-left: 2px ${borderColor} solid !important` : ""
} }
> >
<div <div
className={`${ className={classNames({
!this.props.noIndent && this.props.node.depth && "ml-2" "ml-2": !this.props.noIndent && this.props.node.depth,
}`} })}
> >
<div className="d-flex flex-wrap align-items-center text-muted small"> <div className="d-flex flex-wrap align-items-center text-muted small">
<span className="mr-2"> <span className="mr-2">
@ -324,7 +314,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* end of user row */} {/* end of user row */}
{this.state.showEdit && ( {this.state.showEdit && (
<CommentForm <CommentForm
node={Left(node)} node={node}
edit edit
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
@ -376,14 +366,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
</button> </button>
)} )}
{UserService.Instance.myUserInfo.isSome() && {UserService.Instance.myUserInfo && !this.props.viewOnly && (
!this.props.viewOnly && (
<> <>
<button <button
className={`btn btn-link btn-animate ${ className={`btn btn-link btn-animate ${
this.state.my_vote.unwrapOr(0) == 1 this.state.my_vote == 1 ? "text-info" : "text-muted"
? "text-info"
: "text-muted"
}`} }`}
onClick={this.handleCommentUpvote} onClick={this.handleCommentUpvote}
data-tippy-content={i18n.t("upvote")} data-tippy-content={i18n.t("upvote")}
@ -400,7 +387,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.props.enableDownvotes && ( {this.props.enableDownvotes && (
<button <button
className={`btn btn-link btn-animate ${ className={`btn btn-link btn-animate ${
this.state.my_vote.unwrapOr(0) == -1 this.state.my_vote == -1
? "text-danger" ? "text-danger"
: "text-muted" : "text-muted"
}`} }`}
@ -514,10 +501,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
<button <button
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(this, this.handleEditClick)}
this,
this.handleEditClick
)}
data-tippy-content={i18n.t("edit")} data-tippy-content={i18n.t("edit")}
aria-label={i18n.t("edit")} aria-label={i18n.t("edit")}
> >
@ -569,8 +553,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon <Icon
icon="shield" icon="shield"
classes={`icon-inline ${ classes={`icon-inline ${
cv.comment.distinguished && cv.comment.distinguished && "text-danger"
"text-danger"
}`} }`}
/> />
</button> </button>
@ -873,7 +856,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id={`mod-remove-reason-${cv.comment.id}`} id={`mod-remove-reason-${cv.comment.id}`}
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.removeReason)} value={this.state.removeReason}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/> />
<button <button
@ -928,7 +911,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id={`mod-ban-reason-${cv.comment.id}`} id={`mod-ban-reason-${cv.comment.id}`}
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.banReason)} value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)} onInput={linkEvent(this, this.handleModBanReasonChange)}
/> />
<label <label
@ -942,7 +925,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id={`mod-ban-expires-${cv.comment.id}`} id={`mod-ban-expires-${cv.comment.id}`}
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("number_of_days")} placeholder={i18n.t("number_of_days")}
value={toUndefined(this.state.banExpireDays)} value={this.state.banExpireDays}
onInput={linkEvent(this, this.handleModBanExpireDaysChange)} onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
/> />
<div className="form-group"> <div className="form-group">
@ -992,7 +975,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id="purge-reason" id="purge-reason"
className="form-control my-3" className="form-control my-3"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)} value={this.state.purgeReason}
onInput={linkEvent(this, this.handlePurgeReasonChange)} onInput={linkEvent(this, this.handlePurgeReasonChange)}
/> />
<div className="form-group row col-12"> <div className="form-group row col-12">
@ -1012,7 +995,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
{this.state.showReply && ( {this.state.showReply && (
<CommentForm <CommentForm
node={Left(node)} node={node}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
focus focus
@ -1026,7 +1009,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
locked={this.props.locked} locked={this.props.locked}
moderators={this.props.moderators} moderators={this.props.moderators}
admins={this.props.admins} admins={this.props.admins}
maxCommentsShown={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType} viewType={this.props.viewType}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
@ -1085,12 +1067,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
get myComment(): boolean { get myComment(): boolean {
return UserService.Instance.myUserInfo return (
.map( UserService.Instance.myUserInfo?.local_user_view.person.id ==
m => this.props.node.comment_view.creator.id
m.local_user_view.person.id == this.props.node.comment_view.creator.id );
)
.unwrapOr(false);
} }
get isPostCreator(): boolean { get isPostCreator(): boolean {
@ -1118,37 +1098,46 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleBlockUserClick(i: CommentNode) { handleBlockUserClick(i: CommentNode) {
let blockUserForm = new BlockPerson({ let auth = myAuth();
if (auth) {
let blockUserForm: BlockPerson = {
person_id: i.props.node.comment_view.creator.id, person_id: i.props.node.comment_view.creator.id,
block: true, block: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
}
handleDeleteClick(i: CommentNode) { handleDeleteClick(i: CommentNode) {
let comment = i.props.node.comment_view.comment; let comment = i.props.node.comment_view.comment;
let deleteForm = new DeleteComment({ let auth = myAuth();
if (auth) {
let deleteForm: DeleteComment = {
comment_id: comment.id, comment_id: comment.id,
deleted: !comment.deleted, deleted: !comment.deleted,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.deleteComment(deleteForm)); WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
} }
}
handleSaveCommentClick(i: CommentNode) { handleSaveCommentClick(i: CommentNode) {
let cv = i.props.node.comment_view; let cv = i.props.node.comment_view;
let save = cv.saved == undefined ? true : !cv.saved; let save = cv.saved == undefined ? true : !cv.saved;
let form = new SaveComment({ let auth = myAuth();
if (auth) {
let form: SaveComment = {
comment_id: cv.comment.id, comment_id: cv.comment.id,
save, save,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.saveComment(form)); WebSocketService.Instance.send(wsClient.saveComment(form));
i.setState({ saveLoading: true }); i.setState({ saveLoading: true });
} }
}
handleReplyCancel() { handleReplyCancel() {
this.setState({ showReply: false, showEdit: false }); this.setState({ showReply: false, showEdit: false });
@ -1156,7 +1145,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleCommentUpvote(event: any) { handleCommentUpvote(event: any) {
event.preventDefault(); event.preventDefault();
let myVote = this.state.my_vote.unwrapOr(0); let myVote = this.state.my_vote;
let newVote = myVote == 1 ? 0 : 1; let newVote = myVote == 1 ? 0 : 1;
if (myVote == 1) { if (myVote == 1) {
@ -1177,20 +1166,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}); });
} }
this.setState({ my_vote: Some(newVote) }); this.setState({ my_vote: newVote });
let form = new CreateCommentLike({ let auth = myAuth();
if (auth) {
let form: CreateCommentLike = {
comment_id: this.props.node.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.likeComment(form)); WebSocketService.Instance.send(wsClient.likeComment(form));
setupTippy(); setupTippy();
} }
}
handleCommentDownvote(event: any) { handleCommentDownvote(event: any) {
event.preventDefault(); event.preventDefault();
let myVote = this.state.my_vote.unwrapOr(0); let myVote = this.state.my_vote;
let newVote = myVote == -1 ? 0 : -1; let newVote = myVote == -1 ? 0 : -1;
if (myVote == 1) { if (myVote == 1) {
@ -1211,17 +1203,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}); });
} }
this.setState({ my_vote: Some(newVote) }); this.setState({ my_vote: newVote });
let form = new CreateCommentLike({ let auth = myAuth();
if (auth) {
let form: CreateCommentLike = {
comment_id: this.props.node.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
score: newVote, score: newVote,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.likeComment(form)); WebSocketService.Instance.send(wsClient.likeComment(form));
setupTippy(); setupTippy();
} }
}
handleShowReportDialog(i: CommentNode) { handleShowReportDialog(i: CommentNode) {
i.setState({ showReportDialog: !i.state.showReportDialog }); i.setState({ showReportDialog: !i.state.showReportDialog });
@ -1233,15 +1228,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleReportSubmit(i: CommentNode) { handleReportSubmit(i: CommentNode) {
let comment = i.props.node.comment_view.comment; let comment = i.props.node.comment_view.comment;
let form = new CreateCommentReport({ let reason = i.state.reportReason;
let auth = myAuth();
if (reason && auth) {
let form: CreateCommentReport = {
comment_id: comment.id, comment_id: comment.id,
reason: i.state.reportReason, reason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.createCommentReport(form)); WebSocketService.Instance.send(wsClient.createCommentReport(form));
i.setState({ showReportDialog: false }); i.setState({ showReportDialog: false });
} }
}
handleModRemoveShow(i: CommentNode) { handleModRemoveShow(i: CommentNode) {
i.setState({ i.setState({
@ -1251,7 +1249,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleModRemoveReasonChange(i: CommentNode, event: any) { handleModRemoveReasonChange(i: CommentNode, event: any) {
i.setState({ removeReason: Some(event.target.value) }); i.setState({ removeReason: event.target.value });
} }
handleModRemoveDataChange(i: CommentNode, event: any) { handleModRemoveDataChange(i: CommentNode, event: any) {
@ -1260,30 +1258,33 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModRemoveSubmit(i: CommentNode) { handleModRemoveSubmit(i: CommentNode) {
let comment = i.props.node.comment_view.comment; let comment = i.props.node.comment_view.comment;
let form = new RemoveComment({ let auth = myAuth();
if (auth) {
let form: RemoveComment = {
comment_id: comment.id, comment_id: comment.id,
removed: !comment.removed, removed: !comment.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.removeComment(form)); WebSocketService.Instance.send(wsClient.removeComment(form));
i.setState({ showRemoveDialog: false }); i.setState({ showRemoveDialog: false });
} }
}
handleDistinguishClick(i: CommentNode) { handleDistinguishClick(i: CommentNode) {
let comment = i.props.node.comment_view.comment; let comment = i.props.node.comment_view.comment;
let form = new EditComment({ let auth = myAuth();
if (auth) {
let form: EditComment = {
comment_id: comment.id, comment_id: comment.id,
form_id: None, // TODO not sure about this distinguished: !comment.distinguished,
content: None, auth,
distinguished: Some(!comment.distinguished), };
language_id: Some(comment.language_id),
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.editComment(form)); WebSocketService.Instance.send(wsClient.editComment(form));
i.setState(i.state); i.setState(i.state);
} }
}
isPersonMentionType( isPersonMentionType(
item: CommentView | PersonMentionView | CommentReplyView item: CommentView | PersonMentionView | CommentReplyView
@ -1298,24 +1299,27 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleMarkRead(i: CommentNode) { handleMarkRead(i: CommentNode) {
let auth = myAuth();
if (auth) {
if (i.isPersonMentionType(i.props.node.comment_view)) { if (i.isPersonMentionType(i.props.node.comment_view)) {
let form = new MarkPersonMentionAsRead({ let form: MarkPersonMentionAsRead = {
person_mention_id: i.props.node.comment_view.person_mention.id, person_mention_id: i.props.node.comment_view.person_mention.id,
read: !i.props.node.comment_view.person_mention.read, read: !i.props.node.comment_view.person_mention.read,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form)); WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
} else if (i.isCommentReplyType(i.props.node.comment_view)) { } else if (i.isCommentReplyType(i.props.node.comment_view)) {
let form = new MarkCommentReplyAsRead({ let form: MarkCommentReplyAsRead = {
comment_reply_id: i.props.node.comment_view.comment_reply.id, comment_reply_id: i.props.node.comment_view.comment_reply.id,
read: !i.props.node.comment_view.comment_reply.read, read: !i.props.node.comment_view.comment_reply.read,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form)); WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
} }
i.setState({ readLoading: true }); i.setState({ readLoading: true });
} }
}
handleModBanFromCommunityShow(i: CommentNode) { handleModBanFromCommunityShow(i: CommentNode) {
i.setState({ i.setState({
@ -1334,11 +1338,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleModBanReasonChange(i: CommentNode, event: any) { handleModBanReasonChange(i: CommentNode, event: any) {
i.setState({ banReason: Some(event.target.value) }); i.setState({ banReason: event.target.value });
} }
handleModBanExpireDaysChange(i: CommentNode, event: any) { handleModBanExpireDaysChange(i: CommentNode, event: any) {
i.setState({ banExpireDays: Some(event.target.value) }); i.setState({ banExpireDays: event.target.value });
} }
handleModBanFromCommunitySubmit(i: CommentNode) { handleModBanFromCommunitySubmit(i: CommentNode) {
@ -1353,22 +1357,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModBanBothSubmit(i: CommentNode) { handleModBanBothSubmit(i: CommentNode) {
let cv = i.props.node.comment_view; let cv = i.props.node.comment_view;
let auth = myAuth();
if (auth) {
if (i.state.banType == BanType.Community) { if (i.state.banType == BanType.Community) {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !cv.creator_banned_from_community; let ban = !cv.creator_banned_from_community;
if (ban == false) { if (ban == false) {
i.setState({ removeData: false }); i.setState({ removeData: false });
} }
let form = new BanFromCommunity({ let form: BanFromCommunity = {
person_id: cv.creator.id, person_id: cv.creator.id,
community_id: cv.community.id, community_id: cv.community.id,
ban, ban,
remove_data: Some(i.state.removeData), remove_data: i.state.removeData,
reason: i.state.banReason, reason: i.state.banReason,
expires: i.state.banExpireDays.map(futureDaysToUnixTime), expires: futureDaysToUnixTime(i.state.banExpireDays),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.banFromCommunity(form)); WebSocketService.Instance.send(wsClient.banFromCommunity(form));
} else { } else {
// If its an unban, restore all their data // If its an unban, restore all their data
@ -1376,19 +1381,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
if (ban == false) { if (ban == false) {
i.setState({ removeData: false }); i.setState({ removeData: false });
} }
let form = new BanPerson({ let form: BanPerson = {
person_id: cv.creator.id, person_id: cv.creator.id,
ban, ban,
remove_data: Some(i.state.removeData), remove_data: i.state.removeData,
reason: i.state.banReason, reason: i.state.banReason,
expires: i.state.banExpireDays.map(futureDaysToUnixTime), expires: futureDaysToUnixTime(i.state.banExpireDays),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.banPerson(form)); WebSocketService.Instance.send(wsClient.banPerson(form));
} }
i.setState({ showBanDialog: false }); i.setState({ showBanDialog: false });
} }
}
handlePurgePersonShow(i: CommentNode) { handlePurgePersonShow(i: CommentNode) {
i.setState({ i.setState({
@ -1407,30 +1413,32 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handlePurgeReasonChange(i: CommentNode, event: any) { handlePurgeReasonChange(i: CommentNode, event: any) {
i.setState({ purgeReason: Some(event.target.value) }); i.setState({ purgeReason: event.target.value });
} }
handlePurgeSubmit(i: CommentNode, event: any) { handlePurgeSubmit(i: CommentNode, event: any) {
event.preventDefault(); event.preventDefault();
let auth = myAuth();
if (auth) {
if (i.state.purgeType == PurgeType.Person) { if (i.state.purgeType == PurgeType.Person) {
let form = new PurgePerson({ let form: PurgePerson = {
person_id: i.props.node.comment_view.creator.id, person_id: i.props.node.comment_view.creator.id,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.purgePerson(form)); WebSocketService.Instance.send(wsClient.purgePerson(form));
} else if (i.state.purgeType == PurgeType.Comment) { } else if (i.state.purgeType == PurgeType.Comment) {
let form = new PurgeComment({ let form: PurgeComment = {
comment_id: i.props.node.comment_view.comment.id, comment_id: i.props.node.comment_view.comment.id,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.purgeComment(form)); WebSocketService.Instance.send(wsClient.purgeComment(form));
} }
i.setState({ purgeLoading: true }); i.setState({ purgeLoading: true });
} }
}
handleShowConfirmAppointAsMod(i: CommentNode) { handleShowConfirmAppointAsMod(i: CommentNode) {
i.setState({ showConfirmAppointAsMod: true }); i.setState({ showConfirmAppointAsMod: true });
@ -1442,15 +1450,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleAddModToCommunity(i: CommentNode) { handleAddModToCommunity(i: CommentNode) {
let cv = i.props.node.comment_view; let cv = i.props.node.comment_view;
let form = new AddModToCommunity({ let auth = myAuth();
if (auth) {
let form: AddModToCommunity = {
person_id: cv.creator.id, person_id: cv.creator.id,
community_id: cv.community.id, community_id: cv.community.id,
added: !isMod(i.props.moderators, cv.creator.id), added: !isMod(cv.creator.id, i.props.moderators),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.addModToCommunity(form)); WebSocketService.Instance.send(wsClient.addModToCommunity(form));
i.setState({ showConfirmAppointAsMod: false }); i.setState({ showConfirmAppointAsMod: false });
} }
}
handleShowConfirmAppointAsAdmin(i: CommentNode) { handleShowConfirmAppointAsAdmin(i: CommentNode) {
i.setState({ showConfirmAppointAsAdmin: true }); i.setState({ showConfirmAppointAsAdmin: true });
@ -1461,15 +1472,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleAddAdmin(i: CommentNode) { handleAddAdmin(i: CommentNode) {
let auth = myAuth();
if (auth) {
let creatorId = i.props.node.comment_view.creator.id; let creatorId = i.props.node.comment_view.creator.id;
let form = new AddAdmin({ let form: AddAdmin = {
person_id: creatorId, person_id: creatorId,
added: !isAdmin(i.props.admins, creatorId), added: !isAdmin(creatorId, i.props.admins),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.addAdmin(form)); WebSocketService.Instance.send(wsClient.addAdmin(form));
i.setState({ showConfirmAppointAsAdmin: false }); i.setState({ showConfirmAppointAsAdmin: false });
} }
}
handleShowConfirmTransferCommunity(i: CommentNode) { handleShowConfirmTransferCommunity(i: CommentNode) {
i.setState({ showConfirmTransferCommunity: true }); i.setState({ showConfirmTransferCommunity: true });
@ -1481,14 +1495,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleTransferCommunity(i: CommentNode) { handleTransferCommunity(i: CommentNode) {
let cv = i.props.node.comment_view; let cv = i.props.node.comment_view;
let form = new TransferCommunity({ let auth = myAuth();
if (auth) {
let form: TransferCommunity = {
community_id: cv.community.id, community_id: cv.community.id,
person_id: cv.creator.id, person_id: cv.creator.id,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.transferCommunity(form)); WebSocketService.Instance.send(wsClient.transferCommunity(form));
i.setState({ showConfirmTransferCommunity: false }); i.setState({ showConfirmTransferCommunity: false });
} }
}
handleShowConfirmTransferSite(i: CommentNode) { handleShowConfirmTransferSite(i: CommentNode) {
i.setState({ showConfirmTransferSite: true }); i.setState({ showConfirmTransferSite: true });
@ -1519,27 +1536,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleFetchChildren(i: CommentNode) { handleFetchChildren(i: CommentNode) {
let form = new GetComments({ let form: GetComments = {
post_id: Some(i.props.node.comment_view.post.id), post_id: i.props.node.comment_view.post.id,
parent_id: Some(i.props.node.comment_view.comment.id), parent_id: i.props.node.comment_view.comment.id,
max_depth: Some(commentTreeMaxDepth), max_depth: commentTreeMaxDepth,
page: None, limit: 999, // TODO
sort: None, type_: ListingType.All,
limit: Some(999), saved_only: false,
type_: Some(ListingType.All), auth: myAuth(false),
community_name: None, };
community_id: None,
saved_only: Some(false),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(form)); WebSocketService.Instance.send(wsClient.getComments(form));
} }
get scoreColor() { get scoreColor() {
if (this.state.my_vote.unwrapOr(0) == 1) { if (this.state.my_vote == 1) {
return "text-info"; return "text-info";
} else if (this.state.my_vote.unwrapOr(0) == -1) { } else if (this.state.my_vote == -1) {
return "text-danger"; return "text-danger";
} else { } else {
return "text-muted"; return "text-muted";

View file

@ -1,4 +1,3 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
@ -11,9 +10,9 @@ import { CommentNode } from "./comment-node";
interface CommentNodesProps { interface CommentNodesProps {
nodes: CommentNodeI[]; nodes: CommentNodeI[];
moderators: Option<CommunityModeratorView[]>; moderators?: CommunityModeratorView[];
admins: Option<PersonViewSafe[]>; admins?: PersonViewSafe[];
maxCommentsShown: Option<number>; maxCommentsShown?: number;
noBorder?: boolean; noBorder?: boolean;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
@ -34,9 +33,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
} }
render() { render() {
let maxComments = this.props.maxCommentsShown.unwrapOr( let maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
this.props.nodes.length
);
return ( return (
<div className="comments"> <div className="comments">

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -11,7 +10,7 @@ import {
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { CommentViewType } from "../../interfaces"; import { CommentViewType } from "../../interfaces";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { auth, wsClient } from "../../utils"; import { myAuth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node"; import { CommentNode } from "./comment-node";
@ -59,8 +58,6 @@ export class CommentReport extends Component<CommentReportProps, any> {
<CommentNode <CommentNode
node={node} node={node}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
enableDownvotes={true} enableDownvotes={true}
viewOnly={true} viewOnly={true}
showCommunity={true} showCommunity={true}
@ -74,24 +71,21 @@ export class CommentReport extends Component<CommentReportProps, any> {
<div> <div>
{i18n.t("reason")}: {r.comment_report.reason} {i18n.t("reason")}: {r.comment_report.reason}
</div> </div>
{r.resolver.match({ {r.resolver && (
some: resolver => (
<div> <div>
{r.comment_report.resolved ? ( {r.comment_report.resolved ? (
<T i18nKey="resolved_by"> <T i18nKey="resolved_by">
# #
<PersonListing person={resolver} /> <PersonListing person={r.resolver} />
</T> </T>
) : ( ) : (
<T i18nKey="unresolved_by"> <T i18nKey="unresolved_by">
# #
<PersonListing person={resolver} /> <PersonListing person={r.resolver} />
</T> </T>
)} )}
</div> </div>
), )}
none: <></>,
})}
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)} onClick={linkEvent(this, this.handleResolveReport)}
@ -110,11 +104,14 @@ export class CommentReport extends Component<CommentReportProps, any> {
} }
handleResolveReport(i: CommentReport) { handleResolveReport(i: CommentReport) {
let form = new ResolveCommentReport({ let auth = myAuth();
if (auth) {
let form: ResolveCommentReport = {
report_id: i.props.report.comment_report.id, report_id: i.props.report.comment_report.id,
resolved: !i.props.report.comment_report.resolved, resolved: !i.props.report.comment_report.resolved,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.resolveCommentReport(form)); WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
} }
}
} }

View file

@ -1,10 +1,9 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { PictrsImage } from "./pictrs-image"; import { PictrsImage } from "./pictrs-image";
interface BannerIconHeaderProps { interface BannerIconHeaderProps {
banner: Option<string>; banner?: string;
icon: Option<string>; icon?: string;
} }
export class BannerIconHeader extends Component<BannerIconHeaderProps, any> { export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
@ -13,23 +12,19 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
} }
render() { render() {
let banner = this.props.banner;
let icon = this.props.icon;
return ( return (
<div className="position-relative mb-2"> <div className="position-relative mb-2">
{this.props.banner.match({ {banner && <PictrsImage src={banner} banner alt="" />}
some: banner => <PictrsImage src={banner} banner alt="" />, {icon && (
none: <></>,
})}
{this.props.icon.match({
some: icon => (
<PictrsImage <PictrsImage
src={icon} src={icon}
iconOverlay iconOverlay
pushup={this.props.banner.isSome()} pushup={!!this.props.banner}
alt="" alt=""
/> />
), )}
none: <></>,
})}
</div> </div>
); );
} }

View file

@ -18,13 +18,12 @@ export class CommentSortSelect extends Component<
CommentSortSelectState CommentSortSelectState
> { > {
private id = `sort-select-${randomStr()}`; private id = `sort-select-${randomStr()}`;
private emptyState: CommentSortSelectState = { state: CommentSortSelectState = {
sort: this.props.sort, sort: this.props.sort,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): CommentSortSelectState { static getDerivedStateFromProps(props: any): CommentSortSelectState {
@ -65,6 +64,6 @@ export class CommentSortSelect extends Component<
} }
handleSortChange(i: CommentSortSelect, event: any) { handleSortChange(i: CommentSortSelect, event: any) {
i.props.onChange(event.target.value); i.props.onChange?.(event.target.value);
} }
} }

View file

@ -15,13 +15,12 @@ export class DataTypeSelect extends Component<
DataTypeSelectProps, DataTypeSelectProps,
DataTypeSelectState DataTypeSelectState
> { > {
private emptyState: DataTypeSelectState = { state: DataTypeSelectState = {
type_: this.props.type_, type_: this.props.type_,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): DataTypeSelectProps { static getDerivedStateFromProps(props: any): DataTypeSelectProps {
@ -64,6 +63,6 @@ export class DataTypeSelect extends Component<
} }
handleTypeChange(i: DataTypeSelect, event: any) { handleTypeChange(i: DataTypeSelect, event: any) {
i.props.onChange(Number(event.target.value)); i.props.onChange?.(Number(event.target.value));
} }
} }

View file

@ -1,4 +1,3 @@
import { Option } from "@sniptt/monads";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { Component } from "inferno"; import { Component } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
@ -8,14 +7,16 @@ import { md } from "../../utils";
interface HtmlTagsProps { interface HtmlTagsProps {
title: string; title: string;
path: string; path: string;
description: Option<string>; description?: string;
image: Option<string>; image?: string;
} }
/// Taken from https://metatags.io/ /// Taken from https://metatags.io/
export class HtmlTags extends Component<HtmlTagsProps, any> { export class HtmlTags extends Component<HtmlTagsProps, any> {
render() { render() {
let url = httpExternalPath(this.props.path); let url = httpExternalPath(this.props.path);
let desc = this.props.description;
let image = this.props.image;
return ( return (
<Helmet title={this.props.title}> <Helmet title={this.props.title}>
@ -33,21 +34,19 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
{/* Optional desc and images */} {/* Optional desc and images */}
{this.props.description.isSome() && {["description", "og:description", "twitter:description"].map(
["description", "og:description", "twitter:description"].map(n => ( n =>
desc && (
<meta <meta
key={n} key={n}
name={n} name={n}
content={htmlToText( content={htmlToText(md.renderInline(desc))}
md.renderInline(this.props.description.unwrap())
)}
/> />
))} )
)}
{this.props.image.isSome() && {["og:image", "twitter:image"].map(
["og:image", "twitter:image"].map(p => ( p => image && <meta key={p} property={p} content={image} />
<meta key={p} property={p} content={this.props.image.unwrap()} /> )}
))}
</Helmet> </Helmet>
); );
} }

View file

@ -1,4 +1,3 @@
import { Option } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -8,7 +7,7 @@ import { Icon } from "./icon";
interface ImageUploadFormProps { interface ImageUploadFormProps {
uploadTitle: string; uploadTitle: string;
imageSrc: Option<string>; imageSrc?: string;
onUpload(url: string): any; onUpload(url: string): any;
onRemove(): any; onRemove(): any;
rounded?: boolean; rounded?: boolean;
@ -39,11 +38,10 @@ export class ImageUploadForm extends Component<
htmlFor={this.id} htmlFor={this.id}
className="pointer text-muted small font-weight-bold" className="pointer text-muted small font-weight-bold"
> >
{this.props.imageSrc.match({ {this.props.imageSrc ? (
some: imageSrc => (
<span className="d-inline-block position-relative"> <span className="d-inline-block position-relative">
<img <img
src={imageSrc} src={this.props.imageSrc}
height={this.props.rounded ? 60 : ""} height={this.props.rounded ? 60 : ""}
width={this.props.rounded ? 60 : ""} width={this.props.rounded ? 60 : ""}
className={`img-fluid ${ className={`img-fluid ${
@ -57,13 +55,9 @@ export class ImageUploadForm extends Component<
<Icon icon="x" classes="mini-overlay" /> <Icon icon="x" classes="mini-overlay" />
</a> </a>
</span> </span>
), ) : (
none: ( <span className="btn btn-secondary">{this.props.uploadTitle}</span>
<span className="btn btn-secondary"> )}
{this.props.uploadTitle}
</span>
),
})}
</label> </label>
<input <input
id={this.id} id={this.id}
@ -71,7 +65,7 @@ export class ImageUploadForm extends Component<
accept="image/*,video/*" accept="image/*,video/*"
name={this.id} name={this.id}
className="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>

View file

@ -1,4 +1,3 @@
import { Option } from "@sniptt/monads";
import classNames from "classnames"; import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Language } from "lemmy-js-client"; import { Language } from "lemmy-js-client";
@ -10,7 +9,7 @@ import { Icon } from "./icon";
interface LanguageSelectProps { interface LanguageSelectProps {
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
selectedLanguageIds: Option<number[]>; selectedLanguageIds?: number[];
multiple: boolean; multiple: boolean;
onChange(val: number[]): any; onChange(val: number[]): any;
showAll?: boolean; showAll?: boolean;
@ -31,9 +30,9 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
// Necessary because there is no HTML way to set selected for multiple in value= // Necessary because there is no HTML way to set selected for multiple in value=
setSelectedValues() { setSelectedValues() {
this.props.selectedLanguageIds.map(toString).match({ let ids = this.props.selectedLanguageIds?.map(toString);
some: ids => { if (ids) {
var select = (document.getElementById(this.id) as HTMLSelectElement) let select = (document.getElementById(this.id) as HTMLSelectElement)
.options; .options;
for (let i = 0; i < select.length; i++) { for (let i = 0; i < select.length; i++) {
let o = select[i]; let o = select[i];
@ -41,9 +40,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
o.selected = true; o.selected = true;
} }
} }
}, }
none: void 0,
});
} }
render() { render() {
@ -107,7 +104,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
<option <option
key={l.id} key={l.id}
value={l.id} value={l.id}
selected={selectedLangs.unwrapOr([]).includes(l.id)} selected={selectedLangs?.includes(l.id)}
> >
{l.name} {l.name}
</option> </option>

View file

@ -21,13 +21,12 @@ export class ListingTypeSelect extends Component<
> { > {
private id = `listing-type-input-${randomStr()}`; private id = `listing-type-input-${randomStr()}`;
private emptyState: ListingTypeSelectState = { state: ListingTypeSelectState = {
type_: this.props.type_, type_: this.props.type_,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): ListingTypeSelectProps { static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
@ -46,7 +45,7 @@ export class ListingTypeSelect extends Component<
title={i18n.t("subscribed_description")} title={i18n.t("subscribed_description")}
className={`btn btn-outline-secondary className={`btn btn-outline-secondary
${this.state.type_ == ListingType.Subscribed && "active"} ${this.state.type_ == ListingType.Subscribed && "active"}
${UserService.Instance.myUserInfo.isNone() ? "disabled" : "pointer"} ${!UserService.Instance.myUserInfo ? "disabled" : "pointer"}
`} `}
> >
<input <input
@ -55,7 +54,7 @@ export class ListingTypeSelect extends Component<
value={ListingType.Subscribed} value={ListingType.Subscribed}
checked={this.state.type_ == ListingType.Subscribed} checked={this.state.type_ == ListingType.Subscribed}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
disabled={UserService.Instance.myUserInfo.isNone()} disabled={!UserService.Instance.myUserInfo}
/> />
{i18n.t("subscribed")} {i18n.t("subscribed")}
</label> </label>
@ -100,6 +99,6 @@ export class ListingTypeSelect extends Component<
} }
handleTypeChange(i: ListingTypeSelect, event: any) { handleTypeChange(i: ListingTypeSelect, event: any) {
i.props.onChange(event.target.value); i.props.onChange?.(event.target.value);
} }
} }

View file

@ -1,8 +1,7 @@
import { None, Option, Some } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { Language, toUndefined } from "lemmy-js-client"; import { Language } from "lemmy-js-client";
import { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService } from "../../services"; import { UserService } from "../../services";
@ -22,11 +21,11 @@ import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select"; import { LanguageSelect } from "./language-select";
interface MarkdownTextAreaProps { interface MarkdownTextAreaProps {
initialContent: Option<string>; initialContent?: string;
initialLanguageId: Option<number>; initialLanguageId?: number;
placeholder: Option<string>; placeholder?: string;
buttonTitle: Option<string>; buttonTitle?: string;
maxLength: Option<number>; maxLength?: number;
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
@ -35,18 +34,14 @@ interface MarkdownTextAreaProps {
hideNavigationWarnings?: boolean; hideNavigationWarnings?: boolean;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
onSubmit?(msg: { onSubmit?(msg: { val?: string; formId: string; languageId?: number }): any;
val: Option<string>; allLanguages: Language[]; // TODO should probably be nullable
formId: string; siteLanguages: number[]; // TODO same
languageId: Option<number>;
}): any;
allLanguages: Language[];
siteLanguages: number[];
} }
interface MarkdownTextAreaState { interface MarkdownTextAreaState {
content: Option<string>; content?: string;
languageId: Option<number>; languageId?: number;
previewMode: boolean; previewMode: boolean;
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
@ -59,7 +54,7 @@ export class MarkdownTextArea extends Component<
private id = `comment-textarea-${randomStr()}`; private id = `comment-textarea-${randomStr()}`;
private formId = `comment-form-${randomStr()}`; private formId = `comment-form-${randomStr()}`;
private tribute: any; private tribute: any;
private emptyState: MarkdownTextAreaState = { state: MarkdownTextAreaState = {
content: this.props.initialContent, content: this.props.initialContent,
languageId: this.props.initialLanguageId, languageId: this.props.initialLanguageId,
previewMode: false, previewMode: false,
@ -75,7 +70,6 @@ export class MarkdownTextArea extends Component<
if (isBrowser()) { if (isBrowser()) {
this.tribute = setupTribute(); this.tribute = setupTribute();
} }
this.state = this.emptyState;
} }
componentDidMount() { componentDidMount() {
@ -84,7 +78,7 @@ export class MarkdownTextArea extends Component<
autosize(textarea); autosize(textarea);
this.tribute.attach(textarea); this.tribute.attach(textarea);
textarea.addEventListener("tribute-replaced", () => { textarea.addEventListener("tribute-replaced", () => {
this.setState({ content: Some(textarea.value) }); this.setState({ content: textarea.value });
autosize.update(textarea); autosize.update(textarea);
}); });
@ -100,18 +94,18 @@ export class MarkdownTextArea extends Component<
} }
componentDidUpdate() { componentDidUpdate() {
if (!this.props.hideNavigationWarnings && this.state.content.isSome()) { if (!this.props.hideNavigationWarnings && this.state.content) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
window.onbeforeunload = undefined; window.onbeforeunload = null;
} }
} }
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) { componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
if (nextProps.finished) { if (nextProps.finished) {
this.setState({ previewMode: false, loading: false, content: None }); this.setState({ previewMode: false, loading: false, content: undefined });
if (this.props.replyType) { if (this.props.replyType) {
this.props.onReplyCancel(); this.props.onReplyCancel?.();
} }
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
@ -126,12 +120,12 @@ export class MarkdownTextArea extends Component<
} }
render() { render() {
let languageId = this.state.languageId;
return ( return (
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}> <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
<Prompt <Prompt
when={ when={!this.props.hideNavigationWarnings && this.state.content}
!this.props.hideNavigationWarnings && this.state.content.isSome()
}
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<div className="form-group row"> <div className="form-group row">
@ -139,27 +133,21 @@ export class MarkdownTextArea extends Component<
<textarea <textarea
id={this.id} id={this.id}
className={`form-control ${this.state.previewMode && "d-none"}`} className={`form-control ${this.state.previewMode && "d-none"}`}
value={toUndefined(this.state.content)} value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)} onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} onPaste={linkEvent(this, this.handleImageUploadPaste)}
required required
disabled={this.props.disabled} disabled={this.props.disabled}
rows={2} rows={2}
maxLength={this.props.maxLength.unwrapOr( maxLength={this.props.maxLength ?? markdownFieldCharacterLimit}
markdownFieldCharacterLimit placeholder={this.props.placeholder}
)}
placeholder={toUndefined(this.props.placeholder)}
/> />
{this.state.previewMode && {this.state.previewMode && this.state.content && (
this.state.content.match({
some: content => (
<div <div
className="card border-secondary card-body md-div" className="card border-secondary card-body md-div"
dangerouslySetInnerHTML={mdToHtml(content)} dangerouslySetInnerHTML={mdToHtml(this.state.content)}
/> />
), )}
none: <></>,
})}
</div> </div>
<label className="sr-only" htmlFor={this.id}> <label className="sr-only" htmlFor={this.id}>
{i18n.t("body")} {i18n.t("body")}
@ -167,8 +155,7 @@ export class MarkdownTextArea extends Component<
</div> </div>
<div className="row"> <div className="row">
<div className="col-sm-12 d-flex flex-wrap"> <div className="col-sm-12 d-flex flex-wrap">
{this.props.buttonTitle.match({ {this.props.buttonTitle && (
some: buttonTitle => (
<button <button
type="submit" type="submit"
className="btn btn-sm btn-secondary mr-2" className="btn btn-sm btn-secondary mr-2"
@ -177,12 +164,10 @@ export class MarkdownTextArea extends Component<
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : ( ) : (
<span>{buttonTitle}</span> <span>{this.props.buttonTitle}</span>
)} )}
</button> </button>
), )}
none: <></>,
})}
{this.props.replyType && ( {this.props.replyType && (
<button <button
type="button" type="button"
@ -192,7 +177,7 @@ export class MarkdownTextArea extends Component<
{i18n.t("cancel")} {i18n.t("cancel")}
</button> </button>
)} )}
{this.state.content.isSome() && ( {this.state.content && (
<button <button
className={`btn btn-sm btn-secondary mr-2 ${ className={`btn btn-sm btn-secondary mr-2 ${
this.state.previewMode && "active" this.state.previewMode && "active"
@ -209,7 +194,9 @@ export class MarkdownTextArea extends Component<
<LanguageSelect <LanguageSelect
iconVersion iconVersion
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
selectedLanguageIds={this.state.languageId.map(Array.of)} selectedLanguageIds={
languageId ? Array.of(languageId) : undefined
}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
multiple={false} multiple={false}
onChange={this.handleLanguageChange} onChange={this.handleLanguageChange}
@ -243,7 +230,7 @@ export class MarkdownTextArea extends Component<
<label <label
htmlFor={`file-upload-${this.id}`} htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${ className={`mb-0 ${
UserService.Instance.myUserInfo.isSome() && "pointer" UserService.Instance.myUserInfo && "pointer"
}`} }`}
data-tippy-content={i18n.t("upload_image")} data-tippy-content={i18n.t("upload_image")}
> >
@ -259,7 +246,7 @@ export class MarkdownTextArea extends Component<
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
className="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>
@ -376,13 +363,9 @@ export class MarkdownTextArea extends Component<
let deleteToken = res.files[0].delete_token; let deleteToken = res.files[0].delete_token;
let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`; let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
let imageMarkdown = `![](${url})`; let imageMarkdown = `![](${url})`;
let content = i.state.content;
i.setState({ i.setState({
content: Some( content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
i.state.content.match({
some: content => `${content}\n${imageMarkdown}`,
none: imageMarkdown,
})
),
imageLoading: false, imageLoading: false,
}); });
i.contentChange(); i.contentChange();
@ -407,13 +390,13 @@ export class MarkdownTextArea extends Component<
} }
contentChange() { contentChange() {
if (this.props.onContentChange) { if (this.state.content) {
this.props.onContentChange(toUndefined(this.state.content)); this.props.onContentChange?.(this.state.content);
} }
} }
handleContentChange(i: MarkdownTextArea, event: any) { handleContentChange(i: MarkdownTextArea, event: any) {
i.setState({ content: Some(event.target.value) }); i.setState({ content: event.target.value });
i.contentChange(); i.contentChange();
} }
@ -423,7 +406,7 @@ export class MarkdownTextArea extends Component<
} }
handleLanguageChange(val: number[]) { handleLanguageChange(val: number[]) {
this.setState({ languageId: Some(val[0]) }); this.setState({ languageId: val[0] });
} }
handleSubmit(i: MarkdownTextArea, event: any) { handleSubmit(i: MarkdownTextArea, event: any) {
@ -434,11 +417,11 @@ export class MarkdownTextArea extends Component<
formId: i.formId, formId: i.formId,
languageId: i.state.languageId, languageId: i.state.languageId,
}; };
i.props.onSubmit(msg); i.props.onSubmit?.(msg);
} }
handleReplyCancel(i: MarkdownTextArea) { handleReplyCancel(i: MarkdownTextArea) {
i.props.onReplyCancel(); i.props.onReplyCancel?.();
} }
handleInsertLink(i: MarkdownTextArea, event: any) { handleInsertLink(i: MarkdownTextArea, event: any) {
@ -448,25 +431,24 @@ export class MarkdownTextArea extends Component<
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
if (i.state.content.isNone()) { let content = i.state.content;
i.setState({ content: Some("") });
if (!i.state.content) {
i.setState({ content: "" });
} }
let content = i.state.content.unwrap();
if (start !== end) { if (start !== end) {
let selectedText = content.substring(start, end); let selectedText = content?.substring(start, end);
i.setState({ i.setState({
content: Some( content: `${content?.substring(
`${content.substring(0, start)}[${selectedText}]()${content.substring( 0,
end start
)}` )}[${selectedText}]()${content?.substring(end)}`,
),
}); });
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd = end + 3), 10); setTimeout(() => (textarea.selectionEnd = end + 3), 10);
} else { } else {
i.setState({ content: Some(`${content} []()`) }); i.setState({ content: `${content} []()` });
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd -= 1), 10); setTimeout(() => (textarea.selectionEnd -= 1), 10);
} }
@ -486,28 +468,25 @@ export class MarkdownTextArea extends Component<
afterChars: string, afterChars: string,
emptyChars = "___" emptyChars = "___"
) { ) {
if (this.state.content.isNone()) { let content = this.state.content;
this.setState({ content: Some("") }); if (!this.state.content) {
this.setState({ content: "" });
} }
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
let content = this.state.content.unwrap();
if (start !== end) { if (start !== end) {
let selectedText = content.substring(start, end); let selectedText = content?.substring(start, end);
this.setState({ this.setState({
content: Some( content: `${content?.substring(
`${content.substring(
0, 0,
start start
)}${beforeChars}${selectedText}${afterChars}${content.substring(end)}` )}${beforeChars}${selectedText}${afterChars}${content?.substring(end)}`,
),
}); });
} else { } else {
this.setState({ this.setState({
content: Some(`${content}${beforeChars}${emptyChars}${afterChars}`), content: `${content}${beforeChars}${emptyChars}${afterChars}`,
}); });
} }
this.contentChange(); this.contentChange();
@ -581,11 +560,12 @@ export class MarkdownTextArea extends Component<
} }
simpleInsert(chars: string) { simpleInsert(chars: string) {
if (this.state.content.isNone()) { let content = this.state.content;
this.setState({ content: Some(`${chars} `) }); if (!content) {
this.setState({ content: `${chars} ` });
} else { } else {
this.setState({ this.setState({
content: Some(`${this.state.content.unwrap()}\n${chars} `), content: `${content}\n${chars} `,
}); });
} }
@ -606,20 +586,21 @@ export class MarkdownTextArea extends Component<
quoteInsert() { quoteInsert() {
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
let selectedText = window.getSelection().toString(); let selectedText = window.getSelection()?.toString();
let content = this.state.content;
if (selectedText) { if (selectedText) {
let quotedText = let quotedText =
selectedText selectedText
.split("\n") .split("\n")
.map(t => `> ${t}`) .map(t => `> ${t}`)
.join("\n") + "\n\n"; .join("\n") + "\n\n";
if (this.state.content.isNone()) { if (!content) {
this.setState({ content: Some("") }); this.setState({ content: "" });
} else { } else {
this.setState({ content: Some(`${this.state.content.unwrap()}\n`) }); this.setState({ content: `${content}\n` });
} }
this.setState({ this.setState({
content: Some(`${this.state.content.unwrap()}${quotedText}`), content: `${content}${quotedText}`,
}); });
this.contentChange(); this.contentChange();
// Not sure why this needs a delay // Not sure why this needs a delay
@ -631,8 +612,6 @@ export class MarkdownTextArea extends Component<
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
return start !== end return start !== end ? this.state.content?.substring(start, end) ?? "" : "";
? this.state.content.unwrap().substring(start, end)
: "";
} }
} }

View file

@ -1,4 +1,3 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import moment from "moment"; import moment from "moment";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -7,7 +6,7 @@ import { Icon } from "./icon";
interface MomentTimeProps { interface MomentTimeProps {
published: string; published: string;
updated: Option<string>; updated?: string;
showAgo?: boolean; showAgo?: boolean;
ignoreUpdated?: boolean; ignoreUpdated?: boolean;
} }
@ -22,22 +21,27 @@ export class MomentTime extends Component<MomentTimeProps, any> {
} }
createdAndModifiedTimes() { createdAndModifiedTimes() {
return `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format( let updated = this.props.updated;
let line = `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
this.props.published this.props.published
)}\n\n\n${ )}`;
this.props.updated.isSome() && capitalizeFirstLetter(i18n.t("modified")) if (updated) {
} ${this.format(this.props.updated.unwrap())}`; line += `\n\n\n${capitalizeFirstLetter(i18n.t("modified"))} ${this.format(
updated
)}`;
}
return line;
} }
render() { render() {
if (!this.props.ignoreUpdated && this.props.updated.isSome()) { if (!this.props.ignoreUpdated && this.props.updated) {
return ( return (
<span <span
data-tippy-content={this.createdAndModifiedTimes()} data-tippy-content={this.createdAndModifiedTimes()}
className="font-italics pointer unselectable" className="font-italics pointer unselectable"
> >
<Icon icon="edit-2" classes="icon-inline mr-1" /> <Icon icon="edit-2" classes="icon-inline mr-1" />
{moment.utc(this.props.updated.unwrap()).fromNow(!this.props.showAgo)} {moment.utc(this.props.updated).fromNow(!this.props.showAgo)}
</span> </span>
); );
} else { } else {

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -7,7 +6,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { auth, mdToHtml, wsClient } from "../../utils"; import { mdToHtml, myAuth, wsClient } from "../../utils";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { MarkdownTextArea } from "./markdown-textarea"; import { MarkdownTextArea } from "./markdown-textarea";
import { MomentTime } from "./moment-time"; import { MomentTime } from "./moment-time";
@ -17,7 +16,7 @@ interface RegistrationApplicationProps {
} }
interface RegistrationApplicationState { interface RegistrationApplicationState {
denyReason: Option<string>; denyReason?: string;
denyExpanded: boolean; denyExpanded: boolean;
} }
@ -25,15 +24,13 @@ export class RegistrationApplication extends Component<
RegistrationApplicationProps, RegistrationApplicationProps,
RegistrationApplicationState RegistrationApplicationState
> { > {
private emptyState: RegistrationApplicationState = { state: RegistrationApplicationState = {
denyReason: this.props.application.registration_application.deny_reason, denyReason: this.props.application.registration_application.deny_reason,
denyExpanded: false, denyExpanded: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this); this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
} }
@ -48,44 +45,37 @@ export class RegistrationApplication extends Component<
{i18n.t("applicant")}: <PersonListing person={a.creator} /> {i18n.t("applicant")}: <PersonListing person={a.creator} />
</div> </div>
<div> <div>
{i18n.t("created")}:{" "} {i18n.t("created")}: <MomentTime showAgo published={ra.published} />
<MomentTime showAgo published={ra.published} updated={None} />
</div> </div>
<div>{i18n.t("answer")}:</div> <div>{i18n.t("answer")}:</div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} />
{a.admin.match({ {a.admin && (
some: admin => (
<div> <div>
{accepted ? ( {accepted ? (
<T i18nKey="approved_by"> <T i18nKey="approved_by">
# #
<PersonListing person={admin} /> <PersonListing person={a.admin} />
</T> </T>
) : ( ) : (
<div> <div>
<T i18nKey="denied_by"> <T i18nKey="denied_by">
# #
<PersonListing person={admin} /> <PersonListing person={a.admin} />
</T> </T>
{ra.deny_reason.match({ {ra.deny_reason && (
some: deny_reason => (
<div> <div>
{i18n.t("deny_reason")}:{" "} {i18n.t("deny_reason")}:{" "}
<div <div
className="md-div d-inline-flex" className="md-div d-inline-flex"
dangerouslySetInnerHTML={mdToHtml(deny_reason)} dangerouslySetInnerHTML={mdToHtml(ra.deny_reason)}
/> />
</div> </div>
),
none: <></>,
})}
</div>
)} )}
</div> </div>
), )}
none: <></>, </div>
})} )}
{this.state.denyExpanded && ( {this.state.denyExpanded && (
<div className="form-group row"> <div className="form-group row">
@ -95,11 +85,7 @@ export class RegistrationApplication extends Component<
<div className="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.denyReason} initialContent={this.state.denyReason}
initialLanguageId={None}
onContentChange={this.handleDenyReasonChange} onContentChange={this.handleDenyReasonChange}
placeholder={None}
buttonTitle={None}
maxLength={None}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]} allLanguages={[]}
siteLanguages={[]} siteLanguages={[]}
@ -107,7 +93,7 @@ export class RegistrationApplication extends Component<
</div> </div>
</div> </div>
)} )}
{(ra.admin_id.isNone() || (ra.admin_id.isSome() && !accepted)) && ( {(!ra.admin_id || (ra.admin_id && !accepted)) && (
<button <button
className="btn btn-secondary mr-2 my-2" className="btn btn-secondary mr-2 my-2"
onClick={linkEvent(this, this.handleApprove)} onClick={linkEvent(this, this.handleApprove)}
@ -116,7 +102,7 @@ export class RegistrationApplication extends Component<
{i18n.t("approve")} {i18n.t("approve")}
</button> </button>
)} )}
{(ra.admin_id.isNone() || (ra.admin_id.isSome() && accepted)) && ( {(!ra.admin_id || (ra.admin_id && accepted)) && (
<button <button
className="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
onClick={linkEvent(this, this.handleDeny)} onClick={linkEvent(this, this.handleDeny)}
@ -130,36 +116,41 @@ export class RegistrationApplication extends Component<
} }
handleApprove(i: RegistrationApplication) { handleApprove(i: RegistrationApplication) {
let auth = myAuth();
if (auth) {
i.setState({ denyExpanded: false }); i.setState({ denyExpanded: false });
let form = new ApproveRegistrationApplication({ let form: ApproveRegistrationApplication = {
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
deny_reason: None,
approve: true, approve: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.approveRegistrationApplication(form) wsClient.approveRegistrationApplication(form)
); );
} }
}
handleDeny(i: RegistrationApplication) { handleDeny(i: RegistrationApplication) {
if (i.state.denyExpanded) { if (i.state.denyExpanded) {
i.setState({ denyExpanded: false }); i.setState({ denyExpanded: false });
let form = new ApproveRegistrationApplication({ let auth = myAuth();
if (auth) {
let form: ApproveRegistrationApplication = {
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
approve: false, approve: false,
deny_reason: i.state.denyReason, deny_reason: i.state.denyReason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.approveRegistrationApplication(form) wsClient.approveRegistrationApplication(form)
); );
}
} else { } else {
i.setState({ denyExpanded: true }); i.setState({ denyExpanded: true });
} }
} }
handleDenyReasonChange(val: string) { handleDenyReasonChange(val: string) {
this.setState({ denyReason: Some(val) }); this.setState({ denyReason: val });
} }
} }

View file

@ -17,13 +17,12 @@ interface SortSelectState {
export class SortSelect extends Component<SortSelectProps, SortSelectState> { export class SortSelect extends Component<SortSelectProps, SortSelectState> {
private id = `sort-select-${randomStr()}`; private id = `sort-select-${randomStr()}`;
private emptyState: SortSelectState = { state: SortSelectState = {
sort: this.props.sort, sort: this.props.sort,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): SortSelectState { static getDerivedStateFromProps(props: any): SortSelectState {
@ -86,6 +85,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
} }
handleSortChange(i: SortSelect, event: any) { handleSortChange(i: SortSelect, event: any) {
i.props.onChange(event.target.value); i.props.onChange?.(event.target.value);
} }
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommunityResponse, CommunityResponse,
@ -18,10 +17,10 @@ import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
auth,
getListingTypeFromPropsNoDefault, getListingTypeFromPropsNoDefault,
getPageFromProps, getPageFromProps,
isBrowser, isBrowser,
myAuth,
numToSI, numToSI,
setIsoData, setIsoData,
showLocal, showLocal,
@ -38,7 +37,7 @@ import { CommunityLink } from "./community-link";
const communityLimit = 50; const communityLimit = 50;
interface CommunitiesState { interface CommunitiesState {
listCommunitiesResponse: Option<ListCommunitiesResponse>; listCommunitiesResponse?: ListCommunitiesResponse;
page: number; page: number;
loading: boolean; loading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -52,10 +51,9 @@ interface CommunitiesProps {
} }
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription; private subscription?: Subscription;
private isoData = setIsoData(this.context, ListCommunitiesResponse); private isoData = setIsoData(this.context);
private emptyState: CommunitiesState = { state: CommunitiesState = {
listCommunitiesResponse: None,
loading: true, loading: true,
page: getPageFromProps(this.props), page: getPageFromProps(this.props),
listingType: getListingTypeFromPropsNoDefault(this.props), listingType: getListingTypeFromPropsNoDefault(this.props),
@ -65,7 +63,6 @@ export class Communities extends Component<any, CommunitiesState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
@ -74,7 +71,7 @@ export class Communities extends Component<any, CommunitiesState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse); let listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
this.state = { this.state = {
...this.state, ...this.state,
listCommunitiesResponse: listRes, listCommunitiesResponse: listRes,
@ -87,7 +84,7 @@ export class Communities extends Component<any, CommunitiesState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -120,8 +117,6 @@ export class Communities extends Component<any, CommunitiesState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -168,10 +163,7 @@ export class Communities extends Component<any, CommunitiesState> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.state.listCommunitiesResponse {this.state.listCommunitiesResponse?.communities.map(cv => (
.map(l => l.communities)
.unwrapOr([])
.map(cv => (
<tr key={cv.community.id}> <tr key={cv.community.id}>
<td> <td>
<CommunityLink community={cv.community} /> <CommunityLink community={cv.community} />
@ -278,22 +270,28 @@ export class Communities extends Component<any, CommunitiesState> {
} }
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {
let form = new FollowCommunity({ let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id: communityId, community_id: communityId,
follow: false, follow: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
} }
}
handleSubscribe(communityId: number) { handleSubscribe(communityId: number) {
let form = new FollowCommunity({ let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id: communityId, community_id: communityId,
follow: true, follow: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
} }
}
handleSearchChange(i: Communities, event: any) { handleSearchChange(i: Communities, event: any) {
i.setState({ searchText: event.target.value }); i.setState({ searchText: event.target.value });
@ -307,13 +305,13 @@ export class Communities extends Component<any, CommunitiesState> {
} }
refetch() { refetch() {
let listCommunitiesForm = new ListCommunities({ let listCommunitiesForm: ListCommunities = {
type_: Some(this.state.listingType), type_: this.state.listingType,
sort: Some(SortType.TopMonth), sort: SortType.TopMonth,
limit: Some(communityLimit), limit: communityLimit,
page: Some(this.state.page), page: this.state.page,
auth: auth(false).ok(), auth: myAuth(false),
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
@ -322,17 +320,17 @@ export class Communities extends Component<any, CommunitiesState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let type_: Option<ListingType> = Some( let type_: ListingType = pathSplit[3]
pathSplit[3] ? ListingType[pathSplit[3]] : ListingType.Local ? ListingType[pathSplit[3]]
); : ListingType.Local;
let page = Some(pathSplit[5] ? Number(pathSplit[5]) : 1); let page = pathSplit[5] ? Number(pathSplit[5]) : 1;
let listCommunitiesForm = new ListCommunities({ let listCommunitiesForm: ListCommunities = {
type_, type_,
sort: Some(SortType.TopMonth), sort: SortType.TopMonth,
limit: Some(communityLimit), limit: communityLimit,
page, page,
auth: req.auth, auth: req.auth,
}); };
return [req.client.listCommunities(listCommunitiesForm)]; return [req.client.listCommunities(listCommunitiesForm)];
} }
@ -344,25 +342,20 @@ export class Communities extends Component<any, CommunitiesState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>( let data = wsJsonToRes<ListCommunitiesResponse>(msg);
msg, this.setState({ listCommunitiesResponse: data, loading: false });
ListCommunitiesResponse
);
this.setState({ listCommunitiesResponse: Some(data), loading: false });
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.state.listCommunitiesResponse.match({ let res = this.state.listCommunitiesResponse;
some: res => { let found = res?.communities.find(
let found = res.communities.find(
c => c.community.id == data.community_view.community.id c => c.community.id == data.community_view.community.id
); );
if (found) {
found.subscribed = data.community_view.subscribed; found.subscribed = data.community_view.subscribed;
found.counts.subscribers = data.community_view.counts.subscribers; found.counts.subscribers = data.community_view.counts.subscribers;
},
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} }
} }
}
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { import {
@ -7,7 +6,6 @@ import {
CreateCommunity, CreateCommunity,
EditCommunity, EditCommunity,
Language, Language,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -16,8 +14,8 @@ import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
myAuth,
randomStr, randomStr,
wsClient, wsClient,
wsSubscribe, wsSubscribe,
@ -28,10 +26,10 @@ import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
interface CommunityFormProps { interface CommunityFormProps {
community_view: Option<CommunityView>; // If a community is given, that means this is an edit community_view?: CommunityView; // If a community is given, that means this is an edit
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
communityLanguages: Option<number[]>; communityLanguages?: number[];
onCancel?(): any; onCancel?(): any;
onCreate?(community: CommunityView): any; onCreate?(community: CommunityView): any;
onEdit?(community: CommunityView): any; onEdit?(community: CommunityView): any;
@ -39,7 +37,16 @@ interface CommunityFormProps {
} }
interface CommunityFormState { interface CommunityFormState {
communityForm: CreateCommunity; form: {
name?: string;
title?: string;
description?: string;
icon?: string;
banner?: string;
nsfw?: boolean;
posting_restricted_to_mods?: boolean;
discussion_languages?: number[];
};
loading: boolean; loading: boolean;
} }
@ -48,28 +55,16 @@ export class CommunityForm extends Component<
CommunityFormState CommunityFormState
> { > {
private id = `community-form-${randomStr()}`; private id = `community-form-${randomStr()}`;
private subscription: Subscription; private subscription?: Subscription;
private emptyState: CommunityFormState = { state: CommunityFormState = {
communityForm: new CreateCommunity({ form: {},
name: undefined,
title: undefined,
description: None,
discussion_languages: this.props.communityLanguages,
nsfw: None,
icon: None,
banner: None,
posting_restricted_to_mods: None,
auth: undefined,
}),
loading: false, loading: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange =
this.handleCommunityDescriptionChange.bind(this); this.handleCommunityDescriptionChange.bind(this);
@ -84,24 +79,21 @@ export class CommunityForm extends Component<
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
let cv = this.props.community_view;
if (this.props.community_view.isSome()) { if (cv) {
let cv = this.props.community_view.unwrap();
this.state = { this.state = {
...this.state, form: {
communityForm: new CreateCommunity({
name: cv.community.name, name: cv.community.name,
title: cv.community.title, title: cv.community.title,
description: cv.community.description, description: cv.community.description,
nsfw: Some(cv.community.nsfw), nsfw: cv.community.nsfw,
icon: cv.community.icon, icon: cv.community.icon,
banner: cv.community.banner, banner: cv.community.banner,
posting_restricted_to_mods: Some( posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
cv.community.posting_restricted_to_mods
),
discussion_languages: this.props.communityLanguages, discussion_languages: this.props.communityLanguages,
auth: undefined, },
}), loading: false,
}; };
} }
} }
@ -109,18 +101,18 @@ export class CommunityForm extends Component<
componentDidUpdate() { componentDidUpdate() {
if ( if (
!this.state.loading && !this.state.loading &&
(this.state.communityForm.name || (this.state.form.name ||
this.state.communityForm.title || this.state.form.title ||
this.state.communityForm.description.isSome()) this.state.form.description)
) { ) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
window.onbeforeunload = undefined; window.onbeforeunload = null;
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
window.onbeforeunload = null; window.onbeforeunload = null;
} }
@ -130,14 +122,14 @@ export class CommunityForm extends Component<
<Prompt <Prompt
when={ when={
!this.state.loading && !this.state.loading &&
(this.state.communityForm.name || (this.state.form.name ||
this.state.communityForm.title || this.state.form.title ||
this.state.communityForm.description.isSome()) this.state.form.description)
} }
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
{this.props.community_view.isNone() && ( {!this.props.community_view && (
<div className="form-group row"> <div className="form-group row">
<label <label
className="col-12 col-sm-2 col-form-label" className="col-12 col-sm-2 col-form-label"
@ -156,7 +148,7 @@ export class CommunityForm extends Component<
type="text" type="text"
id="community-name" id="community-name"
className="form-control" className="form-control"
value={this.state.communityForm.name} value={this.state.form.name}
onInput={linkEvent(this, this.handleCommunityNameChange)} onInput={linkEvent(this, this.handleCommunityNameChange)}
required required
minLength={3} minLength={3}
@ -183,7 +175,7 @@ export class CommunityForm extends Component<
<input <input
type="text" type="text"
id="community-title" id="community-title"
value={this.state.communityForm.title} value={this.state.form.title}
onInput={linkEvent(this, this.handleCommunityTitleChange)} onInput={linkEvent(this, this.handleCommunityTitleChange)}
className="form-control" className="form-control"
required required
@ -197,7 +189,7 @@ export class CommunityForm extends Component<
<div className="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_icon")} uploadTitle={i18n.t("upload_icon")}
imageSrc={this.state.communityForm.icon} imageSrc={this.state.form.icon}
onUpload={this.handleIconUpload} onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove} onRemove={this.handleIconRemove}
rounded rounded
@ -209,7 +201,7 @@ export class CommunityForm extends Component<
<div className="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_banner")} uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.communityForm.banner} imageSrc={this.state.form.banner}
onUpload={this.handleBannerUpload} onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove} onRemove={this.handleBannerRemove}
/> />
@ -221,11 +213,8 @@ export class CommunityForm extends Component<
</label> </label>
<div className="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.communityForm.description} initialContent={this.state.form.description}
initialLanguageId={None} placeholder={i18n.t("description")}
placeholder={Some("description")}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleCommunityDescriptionChange} onContentChange={this.handleCommunityDescriptionChange}
allLanguages={[]} allLanguages={[]}
siteLanguages={[]} siteLanguages={[]}
@ -244,7 +233,7 @@ export class CommunityForm extends Component<
className="form-check-input position-static" className="form-check-input position-static"
id="community-nsfw" id="community-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.communityForm.nsfw)} checked={this.state.form.nsfw}
onChange={linkEvent(this, this.handleCommunityNsfwChange)} onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/> />
</div> </div>
@ -261,9 +250,7 @@ export class CommunityForm extends Component<
className="form-check-input position-static" className="form-check-input position-static"
id="community-only-mods-can-post" id="community-only-mods-can-post"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.form.posting_restricted_to_mods}
this.state.communityForm.posting_restricted_to_mods
)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleCommunityPostingRestrictedToMods this.handleCommunityPostingRestrictedToMods
@ -276,7 +263,7 @@ export class CommunityForm extends Component<
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
showSite showSite
selectedLanguageIds={this.state.communityForm.discussion_languages} selectedLanguageIds={this.state.form.discussion_languages}
multiple={true} multiple={true}
onChange={this.handleDiscussionLanguageChange} onChange={this.handleDiscussionLanguageChange}
/> />
@ -289,13 +276,13 @@ export class CommunityForm extends Component<
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.community_view.isSome() ? ( ) : this.props.community_view ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("create")) capitalizeFirstLetter(i18n.t("create"))
)} )}
</button> </button>
{this.props.community_view.isSome() && ( {this.props.community_view && (
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
@ -314,82 +301,92 @@ export class CommunityForm extends Component<
handleCreateCommunitySubmit(i: CommunityForm, event: any) { handleCreateCommunitySubmit(i: CommunityForm, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ loading: true }); i.setState({ loading: true });
let cForm = i.state.communityForm; let cForm = i.state.form;
cForm.auth = auth().unwrap(); let auth = myAuth();
i.props.community_view.match({ let cv = i.props.community_view;
some: cv => {
let form = new EditCommunity({ if (auth) {
if (cv) {
let form: EditCommunity = {
community_id: cv.community.id, community_id: cv.community.id,
title: Some(cForm.title), title: cForm.title,
description: cForm.description, description: cForm.description,
icon: cForm.icon, icon: cForm.icon,
banner: cForm.banner, banner: cForm.banner,
nsfw: cForm.nsfw, nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods, posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages, discussion_languages: cForm.discussion_languages,
auth: cForm.auth, auth,
}); };
WebSocketService.Instance.send(wsClient.editCommunity(form)); WebSocketService.Instance.send(wsClient.editCommunity(form));
}, } else {
none: () => { if (cForm.title && cForm.name) {
WebSocketService.Instance.send( let form: CreateCommunity = {
wsClient.createCommunity(i.state.communityForm) name: cForm.name,
); title: cForm.title,
}, description: cForm.description,
}); icon: cForm.icon,
banner: cForm.banner,
nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages,
auth,
};
WebSocketService.Instance.send(wsClient.createCommunity(form));
}
}
}
i.setState(i.state); i.setState(i.state);
} }
handleCommunityNameChange(i: CommunityForm, event: any) { handleCommunityNameChange(i: CommunityForm, event: any) {
i.state.communityForm.name = event.target.value; i.state.form.name = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleCommunityTitleChange(i: CommunityForm, event: any) { handleCommunityTitleChange(i: CommunityForm, event: any) {
i.state.communityForm.title = event.target.value; i.state.form.title = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleCommunityDescriptionChange(val: string) { handleCommunityDescriptionChange(val: string) {
this.setState(s => ((s.communityForm.description = Some(val)), s)); this.setState(s => ((s.form.description = val), s));
} }
handleCommunityNsfwChange(i: CommunityForm, event: any) { handleCommunityNsfwChange(i: CommunityForm, event: any) {
i.state.communityForm.nsfw = Some(event.target.checked); i.state.form.nsfw = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) { handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
i.state.communityForm.posting_restricted_to_mods = Some( i.state.form.posting_restricted_to_mods = event.target.checked;
event.target.checked
);
i.setState(i.state); i.setState(i.state);
} }
handleCancel(i: CommunityForm) { handleCancel(i: CommunityForm) {
i.props.onCancel(); i.props.onCancel?.();
} }
handleIconUpload(url: string) { handleIconUpload(url: string) {
this.setState(s => ((s.communityForm.icon = Some(url)), s)); this.setState(s => ((s.form.icon = url), s));
} }
handleIconRemove() { handleIconRemove() {
this.setState(s => ((s.communityForm.icon = Some("")), s)); this.setState(s => ((s.form.icon = ""), s));
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.setState(s => ((s.communityForm.banner = Some(url)), s)); this.setState(s => ((s.form.banner = url), s));
} }
handleBannerRemove() { handleBannerRemove() {
this.setState(s => ((s.communityForm.banner = Some("")), s)); this.setState(s => ((s.form.banner = ""), s));
} }
handleDiscussionLanguageChange(val: number[]) { handleDiscussionLanguageChange(val: number[]) {
this.setState(s => ((s.communityForm.discussion_languages = Some(val)), s)); this.setState(s => ((s.form.discussion_languages = val), s));
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -401,14 +398,14 @@ export class CommunityForm extends Component<
this.setState({ loading: false }); this.setState({ loading: false });
return; return;
} else if (op == UserOperation.CreateCommunity) { } else if (op == UserOperation.CreateCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.props.onCreate(data.community_view); this.props.onCreate?.(data.community_view);
// Update myUserInfo // Update myUserInfo
let community = data.community_view.community; let community = data.community_view.community;
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => { if (mui) {
let person = mui.local_user_view.person; let person = mui.local_user_view.person;
mui.follows.push({ mui.follows.push({
community, community,
@ -418,17 +415,15 @@ export class CommunityForm extends Component<
community, community,
moderator: person, moderator: person,
}); });
}, }
none: void 0,
});
} else if (op == UserOperation.EditCommunity) { } else if (op == UserOperation.EditCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.setState({ loading: false }); this.setState({ loading: false });
this.props.onEdit(data.community_view); this.props.onEdit?.(data.community_view);
let community = data.community_view.community; let community = data.community_view.community;
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => { if (mui) {
let followFound = mui.follows.findIndex( let followFound = mui.follows.findIndex(
f => f.community.id == community.id f => f.community.id == community.id
); );
@ -442,9 +437,7 @@ export class CommunityForm extends Component<
if (moderatesFound) { if (moderatesFound) {
mui.moderates[moderatesFound].community = community; mui.moderates[moderatesFound].community = community;
} }
}, }
none: void 0,
});
} }
} }
} }

View file

@ -5,7 +5,6 @@ import { hostname, relTags, showAvatars } from "../../utils";
import { PictrsImage } from "../common/pictrs-image"; import { PictrsImage } from "../common/pictrs-image";
interface CommunityLinkProps { interface CommunityLinkProps {
// TODO figure this out better
community: CommunitySafe; community: CommunitySafe;
realLink?: boolean; realLink?: boolean;
useApubName?: boolean; useApubName?: boolean;
@ -56,14 +55,12 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
} }
avatarAndName(displayName: string) { avatarAndName(displayName: string) {
let icon = this.props.community.icon;
return ( return (
<> <>
{!this.props.hideAvatar && {!this.props.hideAvatar && showAvatars() && icon && (
showAvatars() && <PictrsImage src={icon} icon />
this.props.community.icon.match({ )}
some: icon => <PictrsImage src={icon} icon />,
none: <></>,
})}
<span className="overflow-wrap-anywhere">{displayName}</span> <span className="overflow-wrap-anywhere">{displayName}</span>
</> </>
); );

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
AddModToCommunityResponse, AddModToCommunityResponse,
@ -22,7 +21,6 @@ import {
PostView, PostView,
PurgeItemResponse, PurgeItemResponse,
SortType, SortType,
toOption,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -36,7 +34,6 @@ import {
} from "../../interfaces"; } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
commentsToFlatNodes, commentsToFlatNodes,
communityRSSUrl, communityRSSUrl,
createCommentLikeRes, createCommentLikeRes,
@ -50,6 +47,7 @@ import {
getPageFromProps, getPageFromProps,
getSortTypeFromProps, getSortTypeFromProps,
isPostBlocked, isPostBlocked,
myAuth,
notifyPost, notifyPost,
nsfwCheck, nsfwCheck,
postToCommentSortType, postToCommentSortType,
@ -79,7 +77,7 @@ import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link"; import { CommunityLink } from "./community-link";
interface State { interface State {
communityRes: Option<GetCommunityResponse>; communityRes?: GetCommunityResponse;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
communityName: string; communityName: string;
communityLoading: boolean; communityLoading: boolean;
@ -106,15 +104,9 @@ interface UrlParams {
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
GetCommunityResponse, state: State = {
GetPostsResponse,
GetCommentsResponse
);
private subscription: Subscription;
private emptyState: State = {
communityRes: None,
communityName: this.props.match.params.name, communityName: this.props.match.params.name,
communityLoading: true, communityLoading: true,
postsLoading: true, postsLoading: true,
@ -131,7 +123,6 @@ export class Community extends Component<any, State> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
@ -143,17 +134,19 @@ export class Community extends Component<any, State> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
communityRes: Some(this.isoData.routeData[0] as GetCommunityResponse), communityRes: this.isoData.routeData[0] as GetCommunityResponse,
}; };
let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse); let postsRes = this.isoData.routeData[1] as GetPostsResponse | undefined;
let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse); let commentsRes = this.isoData.routeData[2] as
| GetCommentsResponse
| undefined;
if (postsRes.isSome()) { if (postsRes) {
this.state = { ...this.state, posts: postsRes.unwrap().posts }; this.state = { ...this.state, posts: postsRes.posts };
} }
if (commentsRes.isSome()) { if (commentsRes) {
this.state = { ...this.state, comments: commentsRes.unwrap().comments }; this.state = { ...this.state, comments: commentsRes.comments };
} }
this.state = { this.state = {
@ -169,11 +162,10 @@ export class Community extends Component<any, State> {
} }
fetchCommunity() { fetchCommunity() {
let form = new GetCommunity({ let form: GetCommunity = {
name: Some(this.state.communityName), name: this.state.communityName,
id: None, auth: myAuth(false),
auth: auth(false).ok(), };
});
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
} }
@ -183,7 +175,7 @@ export class Community extends Component<any, State> {
componentWillUnmount() { componentWillUnmount() {
saveScrollPosition(this.context); saveScrollPosition(this.context);
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
static getDerivedStateFromProps(props: any): CommunityProps { static getDerivedStateFromProps(props: any): CommunityProps {
@ -199,58 +191,50 @@ export class Community extends Component<any, State> {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let communityName = pathSplit[2]; let communityName = pathSplit[2];
let communityForm = new GetCommunity({ let communityForm: GetCommunity = {
name: Some(communityName), name: communityName,
id: None,
auth: req.auth, auth: req.auth,
}); };
promises.push(req.client.getCommunity(communityForm)); promises.push(req.client.getCommunity(communityForm));
let dataType: DataType = pathSplit[4] let dataType: DataType = pathSplit[4]
? DataType[pathSplit[4]] ? DataType[pathSplit[4]]
: DataType.Post; : DataType.Post;
let sort: Option<SortType> = toOption( let mui = UserService.Instance.myUserInfo;
pathSplit[6]
? SortType[pathSplit[6]]
: UserService.Instance.myUserInfo.match({
some: mui =>
Object.values(SortType)[
mui.local_user_view.local_user.default_sort_type
],
none: SortType.Active,
})
);
let page = toOption(pathSplit[8] ? Number(pathSplit[8]) : 1); let sort: SortType = pathSplit[6]
? SortType[pathSplit[6]]
: mui
? Object.values(SortType)[
mui.local_user_view.local_user.default_sort_type
]
: SortType.Active;
let page = pathSplit[8] ? Number(pathSplit[8]) : 1;
if (dataType == DataType.Post) { if (dataType == DataType.Post) {
let getPostsForm = new GetPosts({ let getPostsForm: GetPosts = {
community_name: Some(communityName), community_name: communityName,
community_id: None,
page, page,
limit: Some(fetchLimit), limit: fetchLimit,
sort, sort,
type_: Some(ListingType.All), type_: ListingType.All,
saved_only: Some(false), saved_only: false,
auth: req.auth, auth: req.auth,
}); };
promises.push(req.client.getPosts(getPostsForm)); promises.push(req.client.getPosts(getPostsForm));
promises.push(Promise.resolve()); promises.push(Promise.resolve());
} else { } else {
let getCommentsForm = new GetComments({ let getCommentsForm: GetComments = {
community_name: Some(communityName), community_name: communityName,
community_id: None,
page, page,
limit: Some(fetchLimit), limit: fetchLimit,
max_depth: None, sort: postToCommentSortType(sort),
sort: sort.map(postToCommentSortType), type_: ListingType.All,
type_: Some(ListingType.All), saved_only: false,
saved_only: Some(false),
post_id: None,
parent_id: None,
auth: req.auth, auth: req.auth,
}); };
promises.push(Promise.resolve()); promises.push(Promise.resolve());
promises.push(req.client.getComments(getCommentsForm)); promises.push(req.client.getComments(getCommentsForm));
} }
@ -270,23 +254,19 @@ export class Community extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return this.state.communityRes.match({ let cRes = this.state.communityRes;
some: res => return cRes
`${res.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`, ? `${cRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`
none: "", : "";
});
} }
render() { render() {
// For some reason, this returns an empty vec if it matches the site langs // For some reason, this returns an empty vec if it matches the site langs
let communityLangs = this.state.communityRes.map(r => { let res = this.state.communityRes;
let langs = r.discussion_languages; let communityLangs =
if (langs.length == 0) { res?.discussion_languages.length == 0
return this.state.siteRes.all_languages.map(l => l.id); ? this.state.siteRes.all_languages.map(l => l.id)
} else { : res?.discussion_languages;
return langs;
}
});
return ( return (
<div className="container-lg"> <div className="container-lg">
@ -295,8 +275,7 @@ export class Community extends Component<any, State> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
this.state.communityRes.match({ res && (
some: res => (
<> <>
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
@ -338,19 +317,12 @@ export class Community extends Component<any, State> {
} }
communityLanguages={communityLangs} communityLanguages={communityLangs}
/> />
{!res.community_view.community.local && {!res.community_view.community.local && res.site && (
res.site.match({
some: site => (
<SiteSidebar <SiteSidebar
site={site} site={res.site}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
admins={None}
counts={None}
online={None}
/> />
), )}
none: <></>,
})}
</> </>
)} )}
</div> </div>
@ -373,25 +345,16 @@ export class Community extends Component<any, State> {
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
communityLanguages={communityLangs} communityLanguages={communityLangs}
/> />
{!res.community_view.community.local && {!res.community_view.community.local && res.site && (
res.site.match({
some: site => (
<SiteSidebar <SiteSidebar
site={site} site={res.site}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
admins={None}
counts={None}
online={None}
/> />
), )}
none: <></>,
})}
</div> </div>
</div> </div>
</> </>
), )
none: <></>,
})
)} )}
</div> </div>
); );
@ -424,9 +387,8 @@ export class Community extends Component<any, State> {
noIndent noIndent
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
moderators={this.state.communityRes.map(r => r.moderators)} moderators={this.state.communityRes?.moderators}
admins={Some(this.state.siteRes.admins)} admins={this.state.siteRes.admins}
maxCommentsShown={None}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
/> />
@ -434,10 +396,9 @@ export class Community extends Component<any, State> {
} }
communityInfo() { communityInfo() {
return this.state.communityRes let community = this.state.communityRes?.community_view.community;
.map(r => r.community_view.community) return (
.match({ community && (
some: community => (
<div className="mb-2"> <div className="mb-2">
<BannerIconHeader banner={community.banner} icon={community.icon} /> <BannerIconHeader banner={community.banner} icon={community.icon} />
<h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5> <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
@ -449,15 +410,19 @@ export class Community extends Component<any, State> {
hideAvatar hideAvatar
/> />
</div> </div>
), )
none: <></>, );
});
} }
selects() { selects() {
let communityRss = this.state.communityRes.map(r => // let communityRss = this.state.communityRes.map(r =>
communityRSSUrl(r.community_view.community.actor_id, this.state.sort) // communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
); // );
let res = this.state.communityRes;
let communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, this.state.sort)
: undefined;
return ( return (
<div className="mb-3"> <div className="mb-3">
<span className="mr-3"> <span className="mr-3">
@ -469,17 +434,18 @@ export class Community extends Component<any, State> {
<span className="mr-2"> <span className="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} /> <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
</span> </span>
{communityRss.match({ {communityRss && (
some: rss => (
<> <>
<a href={rss} title="RSS" rel={relTags}> <a href={communityRss} title="RSS" rel={relTags}>
<Icon icon="rss" classes="text-muted small" /> <Icon icon="rss" classes="text-muted small" />
</a> </a>
<link rel="alternate" type="application/atom+xml" href={rss} /> <link
rel="alternate"
type="application/atom+xml"
href={communityRss}
/>
</> </>
), )}
none: <></>,
})}
</div> </div>
); );
} }
@ -517,31 +483,26 @@ export class Community extends Component<any, State> {
fetchData() { fetchData() {
if (this.state.dataType == DataType.Post) { if (this.state.dataType == DataType.Post) {
let form = new GetPosts({ let form: GetPosts = {
page: Some(this.state.page), page: this.state.page,
limit: Some(fetchLimit), limit: fetchLimit,
sort: Some(this.state.sort), sort: this.state.sort,
type_: Some(ListingType.All), type_: ListingType.All,
community_name: Some(this.state.communityName), community_name: this.state.communityName,
community_id: None, saved_only: false,
saved_only: Some(false), auth: myAuth(false),
auth: auth(false).ok(), };
});
WebSocketService.Instance.send(wsClient.getPosts(form)); WebSocketService.Instance.send(wsClient.getPosts(form));
} else { } else {
let form = new GetComments({ let form: GetComments = {
page: Some(this.state.page), page: this.state.page,
limit: Some(fetchLimit), limit: fetchLimit,
max_depth: None, sort: postToCommentSortType(this.state.sort),
sort: Some(postToCommentSortType(this.state.sort)), type_: ListingType.All,
type_: Some(ListingType.All), community_name: this.state.communityName,
community_name: Some(this.state.communityName), saved_only: false,
community_id: None, auth: myAuth(false),
saved_only: Some(false), };
post_id: None,
parent_id: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(form)); WebSocketService.Instance.send(wsClient.getComments(form));
} }
} }
@ -549,25 +510,23 @@ export class Community extends Component<any, State> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op = wsUserOp(msg); let op = wsUserOp(msg);
console.log(msg); console.log(msg);
let res = this.state.communityRes;
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.context.router.history.push("/"); this.context.router.history.push("/");
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.state.communityRes.match({ if (res) {
some: res => {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ wsClient.communityJoin({
community_id: res.community_view.community.id, community_id: res.community_view.community.id,
}) })
); );
}, }
none: void 0,
});
this.fetchData(); this.fetchData();
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({ communityRes: Some(data), communityLoading: false }); this.setState({ communityRes: data, communityLoading: false });
// TODO why is there no auth in this form? // TODO why is there no auth in this form?
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ wsClient.communityJoin({
@ -579,28 +538,22 @@ export class Community extends Component<any, State> {
op == UserOperation.DeleteCommunity || op == UserOperation.DeleteCommunity ||
op == UserOperation.RemoveCommunity op == UserOperation.RemoveCommunity
) { ) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.state.communityRes.match({ if (res) {
some: res => {
res.community_view = data.community_view; res.community_view = data.community_view;
res.discussion_languages = data.discussion_languages; res.discussion_languages = data.discussion_languages;
}, }
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.state.communityRes.match({ if (res) {
some: res => {
res.community_view.subscribed = data.community_view.subscribed; res.community_view.subscribed = data.community_view.subscribed;
res.community_view.counts.subscribers = res.community_view.counts.subscribers =
data.community_view.counts.subscribers; data.community_view.counts.subscribers;
}, }
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse); let data = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts: data.posts, postsLoading: false }); this.setState({ posts: data.posts, postsLoading: false });
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
setupTippy(); setupTippy();
@ -612,15 +565,15 @@ export class Community extends Component<any, State> {
op == UserOperation.FeaturePost || op == UserOperation.FeaturePost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
editPostFindRes(data.post_view, this.state.posts); editPostFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
let showPostNotifs = UserService.Instance.myUserInfo let showPostNotifs =
.map(m => m.local_user_view.local_user.show_new_post_notifs) UserService.Instance.myUserInfo?.local_user_view.local_user
.unwrapOr(false); .show_new_post_notifs;
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
// //
@ -636,24 +589,17 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(data.post_view, this.state.posts); createPostLikeFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>( let data = wsJsonToRes<AddModToCommunityResponse>(msg);
msg, if (res) {
AddModToCommunityResponse res.moderators = data.moderators;
); }
this.state.communityRes.match({
some: res => (res.moderators = data.moderators),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let data = wsJsonToRes<BanFromCommunityResponse>( let data = wsJsonToRes<BanFromCommunityResponse>(msg);
msg,
BanFromCommunityResponse
);
// TODO this might be incorrect // TODO this might be incorrect
this.state.posts this.state.posts
@ -662,18 +608,18 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse); let data = wsJsonToRes<GetCommentsResponse>(msg);
this.setState({ comments: data.comments, commentsLoading: false }); this.setState({ comments: data.comments, commentsLoading: false });
} else if ( } else if (
op == UserOperation.EditComment || op == UserOperation.EditComment ||
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes(data.comment_view, this.state.comments); editCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
// Necessary since it might be a user reply // Necessary since it might be a user reply
if (data.form_id) { if (data.form_id) {
@ -681,41 +627,37 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(data.comment_view, this.state.comments); saveCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(data.comment_view, this.state.comments); createCommentLikeRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse); let data = wsJsonToRes<CommentReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.PurgeCommunity) { } else if (op == UserOperation.PurgeCommunity) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse); let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) { if (data.success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} else if (op == UserOperation.BlockCommunity) { } else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes<BlockCommunityResponse>( let data = wsJsonToRes<BlockCommunityResponse>(msg);
msg, if (res) {
BlockCommunityResponse res.community_view.blocked = data.blocked;
); }
this.state.communityRes.match({
some: res => (res.community_view.blocked = data.blocked),
none: void 0,
});
updateCommunityBlock(data); updateCommunityBlock(data);
this.setState(this.state); this.setState(this.state);
} }

View file

@ -1,4 +1,3 @@
import { None, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { CommunityView, GetSiteResponse } from "lemmy-js-client"; import { CommunityView, GetSiteResponse } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
@ -22,20 +21,19 @@ interface CreateCommunityState {
export class CreateCommunity extends Component<any, CreateCommunityState> { export class CreateCommunity extends Component<any, CreateCommunityState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private emptyState: CreateCommunityState = { state: CreateCommunityState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
loading: false, loading: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this); this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -43,7 +41,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -59,8 +57,6 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -71,14 +67,11 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_community")}</h5> <h5>{i18n.t("create_community")}</h5>
<CommunityForm <CommunityForm
community_view={None}
onCreate={this.handleCommunityCreate} onCreate={this.handleCommunityCreate}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
communityLanguages={Some( communityLanguages={this.state.siteRes.discussion_languages}
this.state.siteRes.discussion_languages
)}
/> />
</div> </div>
</div> </div>

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -13,7 +12,6 @@ import {
PurgeCommunity, PurgeCommunity,
RemoveCommunity, RemoveCommunity,
SubscribedType, SubscribedType,
toUndefined,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
@ -21,9 +19,9 @@ import {
amAdmin, amAdmin,
amMod, amMod,
amTopMod, amTopMod,
auth,
getUnixTime, getUnixTime,
mdToHtml, mdToHtml,
myAuth,
numToSI, numToSI,
wsClient, wsClient,
} from "../../utils"; } from "../../utils";
@ -39,7 +37,7 @@ interface SidebarProps {
admins: PersonViewSafe[]; admins: PersonViewSafe[];
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
communityLanguages: Option<number[]>; communityLanguages?: number[];
online: number; online: number;
enableNsfw?: boolean; enableNsfw?: boolean;
showIcon?: boolean; showIcon?: boolean;
@ -47,31 +45,27 @@ interface SidebarProps {
} }
interface SidebarState { interface SidebarState {
removeReason: Option<string>; removeReason?: string;
removeExpires: Option<string>; removeExpires?: string;
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean; showRemoveDialog: boolean;
showPurgeDialog: boolean; showPurgeDialog: boolean;
purgeReason: Option<string>; purgeReason?: string;
purgeLoading: boolean; purgeLoading: boolean;
showConfirmLeaveModTeam: boolean; showConfirmLeaveModTeam: boolean;
} }
export class Sidebar extends Component<SidebarProps, SidebarState> { export class Sidebar extends Component<SidebarProps, SidebarState> {
private emptyState: SidebarState = { state: SidebarState = {
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
removeReason: None,
removeExpires: None,
showPurgeDialog: false, showPurgeDialog: false,
purgeReason: None,
purgeLoading: false, purgeLoading: false,
showConfirmLeaveModTeam: false, showConfirmLeaveModTeam: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleEditCommunity = this.handleEditCommunity.bind(this); this.handleEditCommunity = this.handleEditCommunity.bind(this);
this.handleEditCancel = this.handleEditCancel.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this);
} }
@ -83,7 +77,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
this.sidebar() this.sidebar()
) : ( ) : (
<CommunityForm <CommunityForm
community_view={Some(this.props.community_view)} community_view={this.props.community_view}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
communityLanguages={this.props.communityLanguages} communityLanguages={this.props.communityLanguages}
@ -336,13 +330,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
description() { description() {
let description = this.props.community_view.community.description; let desc = this.props.community_view.community.description;
return description.match({ return (
some: desc => ( desc && (
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
), )
none: <></>, );
});
} }
adminButtons() { adminButtons() {
@ -350,7 +343,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return ( return (
<> <>
<ul className="list-inline mb-1 text-muted font-weight-bold"> <ul className="list-inline mb-1 text-muted font-weight-bold">
{amMod(Some(this.props.moderators)) && ( {amMod(this.props.moderators) && (
<> <>
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
@ -362,7 +355,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<Icon icon="edit" classes="icon-inline" /> <Icon icon="edit" classes="icon-inline" />
</button> </button>
</li> </li>
{!amTopMod(Some(this.props.moderators)) && {!amTopMod(this.props.moderators) &&
(!this.state.showConfirmLeaveModTeam ? ( (!this.state.showConfirmLeaveModTeam ? (
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
@ -401,7 +394,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</li> </li>
</> </>
))} ))}
{amTopMod(Some(this.props.moderators)) && ( {amTopMod(this.props.moderators) && (
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
className="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
@ -466,7 +459,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
id="remove-reason" id="remove-reason"
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.removeReason)} value={this.state.removeReason}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/> />
</div> </div>
@ -496,7 +489,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
id="purge-reason" id="purge-reason"
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)} value={this.state.purgeReason}
onInput={linkEvent(this, this.handlePurgeReasonChange)} onInput={linkEvent(this, this.handlePurgeReasonChange)}
/> />
</div> </div>
@ -533,32 +526,34 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleDeleteClick(i: Sidebar, event: any) { handleDeleteClick(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let deleteForm = new DeleteCommunity({ let auth = myAuth();
if (auth) {
let deleteForm: DeleteCommunity = {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
deleted: !i.props.community_view.community.deleted, deleted: !i.props.community_view.community.deleted,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm)); WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
} }
}
handleShowConfirmLeaveModTeamClick(i: Sidebar) { handleShowConfirmLeaveModTeamClick(i: Sidebar) {
i.setState({ showConfirmLeaveModTeam: true }); i.setState({ showConfirmLeaveModTeam: true });
} }
handleLeaveModTeamClick(i: Sidebar) { handleLeaveModTeamClick(i: Sidebar) {
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => { let auth = myAuth();
let form = new AddModToCommunity({ if (auth && mui) {
let form: AddModToCommunity = {
person_id: mui.local_user_view.person.id, person_id: mui.local_user_view.person.id,
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
added: false, added: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.addModToCommunity(form)); WebSocketService.Instance.send(wsClient.addModToCommunity(form));
i.setState({ showConfirmLeaveModTeam: false }); i.setState({ showConfirmLeaveModTeam: false });
}, }
none: void 0,
});
} }
handleCancelLeaveModTeamClick(i: Sidebar) { handleCancelLeaveModTeamClick(i: Sidebar) {
@ -568,46 +563,50 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleUnsubscribe(i: Sidebar, event: any) { handleUnsubscribe(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let community_id = i.props.community_view.community.id; let community_id = i.props.community_view.community.id;
let form = new FollowCommunity({ let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id, community_id,
follow: false, follow: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
}
// Update myUserInfo // Update myUserInfo
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => if (mui) {
(mui.follows = mui.follows.filter(i => i.community.id != community_id)), mui.follows = mui.follows.filter(i => i.community.id != community_id);
none: void 0, }
});
} }
handleSubscribe(i: Sidebar, event: any) { handleSubscribe(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let community_id = i.props.community_view.community.id; let community_id = i.props.community_view.community.id;
let form = new FollowCommunity({ let auth = myAuth();
if (auth) {
let form: FollowCommunity = {
community_id, community_id,
follow: true, follow: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
}
// Update myUserInfo // Update myUserInfo
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => if (mui) {
mui.follows.push({ mui.follows.push({
community: i.props.community_view.community, community: i.props.community_view.community,
follower: mui.local_user_view.person, follower: mui.local_user_view.person,
}),
none: void 0,
}); });
} }
}
get canPost(): boolean { get canPost(): boolean {
return ( return (
!this.props.community_view.community.posting_restricted_to_mods || !this.props.community_view.community.posting_restricted_to_mods ||
amMod(Some(this.props.moderators)) || amMod(this.props.moderators) ||
amAdmin() amAdmin()
); );
} }
@ -617,65 +616,80 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
handleModRemoveReasonChange(i: Sidebar, event: any) { handleModRemoveReasonChange(i: Sidebar, event: any) {
i.setState({ removeReason: Some(event.target.value) }); i.setState({ removeReason: event.target.value });
} }
handleModRemoveExpiresChange(i: Sidebar, event: any) { handleModRemoveExpiresChange(i: Sidebar, event: any) {
i.setState({ removeExpires: Some(event.target.value) }); i.setState({ removeExpires: event.target.value });
} }
handleModRemoveSubmit(i: Sidebar, event: any) { handleModRemoveSubmit(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let removeForm = new RemoveCommunity({ let auth = myAuth();
if (auth) {
let removeForm: RemoveCommunity = {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
removed: !i.props.community_view.community.removed, removed: !i.props.community_view.community.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
expires: i.state.removeExpires.map(getUnixTime), expires: getUnixTime(i.state.removeExpires),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.removeCommunity(removeForm)); WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
i.setState({ showRemoveDialog: false }); i.setState({ showRemoveDialog: false });
} }
}
handlePurgeCommunityShow(i: Sidebar) { handlePurgeCommunityShow(i: Sidebar) {
i.setState({ showPurgeDialog: true, showRemoveDialog: false }); i.setState({ showPurgeDialog: true, showRemoveDialog: false });
} }
handlePurgeReasonChange(i: Sidebar, event: any) { handlePurgeReasonChange(i: Sidebar, event: any) {
i.setState({ purgeReason: Some(event.target.value) }); i.setState({ purgeReason: event.target.value });
} }
handlePurgeSubmit(i: Sidebar, event: any) { handlePurgeSubmit(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let form = new PurgeCommunity({ let auth = myAuth();
if (auth) {
let form: PurgeCommunity = {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.purgeCommunity(form)); WebSocketService.Instance.send(wsClient.purgeCommunity(form));
i.setState({ purgeLoading: true }); i.setState({ purgeLoading: true });
} }
}
handleBlock(i: Sidebar, event: any) { handleBlock(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let blockCommunityForm = new BlockCommunity({ let auth = myAuth();
if (auth) {
let blockCommunityForm: BlockCommunity = {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
block: true, block: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm)); WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm)
);
}
} }
handleUnblock(i: Sidebar, event: any) { handleUnblock(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let blockCommunityForm = new BlockCommunity({ let auth = myAuth();
if (auth) {
let blockCommunityForm: BlockCommunity = {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
block: false, block: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm)); WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm)
);
}
} }
} }

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
@ -16,9 +15,9 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
isBrowser, isBrowser,
myAuth,
randomStr, randomStr,
setIsoData, setIsoData,
showLocal, showLocal,
@ -40,20 +39,18 @@ interface AdminSettingsState {
export class AdminSettings extends Component<any, AdminSettingsState> { export class AdminSettings extends Component<any, AdminSettingsState> {
private siteConfigTextAreaId = `site-config-${randomStr()}`; private siteConfigTextAreaId = `site-config-${randomStr()}`;
private isoData = setIsoData(this.context, BannedPersonsResponse); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private emptyState: AdminSettingsState = { state: AdminSettingsState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
banned: [], banned: [],
loading: true, loading: true,
leaveAdminTeamLoading: null, leaveAdminTeamLoading: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
@ -65,19 +62,25 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
loading: false, loading: false,
}; };
} else { } else {
let cAuth = myAuth();
if (cAuth) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getBannedPersons({ wsClient.getBannedPersons({
auth: auth().unwrap(), auth: cAuth,
}) })
); );
} }
} }
}
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let bannedPersonsForm = new GetBannedPersons({ auth: req.auth.unwrap() }); let auth = req.auth;
if (auth) {
let bannedPersonsForm: GetBannedPersons = { auth };
promises.push(req.client.getBannedPersons(bannedPersonsForm)); promises.push(req.client.getBannedPersons(bannedPersonsForm));
}
return promises; return promises;
} }
@ -91,7 +94,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -114,8 +117,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<SiteForm <SiteForm
siteRes={this.state.siteRes} siteRes={this.state.siteRes}
@ -179,10 +180,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
handleLeaveAdminTeam(i: AdminSettings) { handleLeaveAdminTeam(i: AdminSettings) {
let auth = myAuth();
if (auth) {
i.setState({ leaveAdminTeamLoading: true }); i.setState({ leaveAdminTeamLoading: true });
WebSocketService.Instance.send( WebSocketService.Instance.send(wsClient.leaveAdmin({ auth }));
wsClient.leaveAdmin({ auth: auth().unwrap() }) }
);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -194,14 +196,14 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
this.setState({ loading: false }); this.setState({ loading: false });
return; return;
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg, SiteResponse); let data = wsJsonToRes<SiteResponse>(msg);
this.setState(s => ((s.siteRes.site_view = data.site_view), s)); this.setState(s => ((s.siteRes.site_view = data.site_view), s));
toast(i18n.t("site_saved")); toast(i18n.t("site_saved"));
} else if (op == UserOperation.GetBannedPersons) { } else if (op == UserOperation.GetBannedPersons) {
let data = wsJsonToRes<BannedPersonsResponse>(msg, BannedPersonsResponse); let data = wsJsonToRes<BannedPersonsResponse>(msg);
this.setState({ banned: data.banned, loading: false }); this.setState({ banned: data.banned, loading: false });
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg);
this.setState(s => ((s.siteRes.site_view = data.site_view), s)); this.setState(s => ((s.siteRes.site_view = data.site_view), s));
this.setState({ leaveAdminTeamLoading: false }); this.setState({ leaveAdminTeamLoading: false });

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -37,7 +36,6 @@ import {
} from "../../interfaces"; } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
canCreateCommunity, canCreateCommunity,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
@ -55,6 +53,7 @@ import {
isBrowser, isBrowser,
isPostBlocked, isPostBlocked,
mdToHtml, mdToHtml,
myAuth,
notifyPost, notifyPost,
nsfwCheck, nsfwCheck,
postToCommentSortType, postToCommentSortType,
@ -96,7 +95,7 @@ interface HomeState {
showSidebarMobile: boolean; showSidebarMobile: boolean;
subscribedCollapsed: boolean; subscribedCollapsed: boolean;
loading: boolean; loading: boolean;
tagline: Option<string>; tagline?: string;
} }
interface HomeProps { interface HomeProps {
@ -114,14 +113,9 @@ interface UrlParams {
} }
export class Home extends Component<any, HomeState> { export class Home extends Component<any, HomeState> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
GetPostsResponse, state: HomeState = {
GetCommentsResponse,
ListCommunitiesResponse
);
private subscription: Subscription;
private emptyState: HomeState = {
trendingCommunities: [], trendingCommunities: [],
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showSubscribedMobile: false, showSubscribedMobile: false,
@ -140,13 +134,11 @@ export class Home extends Component<any, HomeState> {
dataType: getDataTypeFromProps(this.props), dataType: getDataTypeFromProps(this.props),
sort: getSortTypeFromProps(this.props), sort: getSortTypeFromProps(this.props),
page: getPageFromProps(this.props), page: getPageFromProps(this.props),
tagline: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
@ -157,16 +149,20 @@ export class Home extends Component<any, HomeState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
let postsRes = Some(this.isoData.routeData[0] as GetPostsResponse); let postsRes = this.isoData.routeData[0] as GetPostsResponse | undefined;
let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse); let commentsRes = this.isoData.routeData[1] as
let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse; | GetCommentsResponse
| undefined;
let trendingRes = this.isoData.routeData[2] as
| ListCommunitiesResponse
| undefined;
if (postsRes.isSome()) { if (postsRes) {
this.state = { ...this.state, posts: postsRes.unwrap().posts }; this.state = { ...this.state, posts: postsRes.posts };
} }
if (commentsRes.isSome()) { if (commentsRes) {
this.state = { ...this.state, comments: commentsRes.unwrap().comments }; this.state = { ...this.state, comments: commentsRes.comments };
} }
if (isBrowser()) { if (isBrowser()) {
@ -177,9 +173,9 @@ export class Home extends Component<any, HomeState> {
const taglines = this.state.siteRes.taglines; const taglines = this.state.siteRes.taglines;
this.state = { this.state = {
...this.state, ...this.state,
trendingCommunities: trendingRes.communities, trendingCommunities: trendingRes?.communities ?? [],
loading: false, loading: false,
tagline: taglines.map(tls => getRandomFromList(tls).content), tagline: getRandomFromList(taglines)?.content,
}; };
} else { } else {
this.fetchTrendingCommunities(); this.fetchTrendingCommunities();
@ -188,13 +184,12 @@ export class Home extends Component<any, HomeState> {
} }
fetchTrendingCommunities() { fetchTrendingCommunities() {
let listCommunitiesForm = new ListCommunities({ let listCommunitiesForm: ListCommunities = {
type_: Some(ListingType.Local), type_: ListingType.Local,
sort: Some(SortType.Hot), sort: SortType.Hot,
limit: Some(trendingFetchLimit), limit: trendingFetchLimit,
page: None, auth: myAuth(false),
auth: auth(false).ok(), };
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
); );
@ -210,7 +205,7 @@ export class Home extends Component<any, HomeState> {
componentWillUnmount() { componentWillUnmount() {
saveScrollPosition(this.context); saveScrollPosition(this.context);
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
static getDerivedStateFromProps( static getDerivedStateFromProps(
@ -230,74 +225,60 @@ export class Home extends Component<any, HomeState> {
let dataType: DataType = pathSplit[3] let dataType: DataType = pathSplit[3]
? DataType[pathSplit[3]] ? DataType[pathSplit[3]]
: DataType.Post; : DataType.Post;
let mui = UserService.Instance.myUserInfo;
let auth = req.auth;
// TODO figure out auth default_listingType, default_sort_type // TODO figure out auth default_listingType, default_sort_type
let type_: Option<ListingType> = Some( let type_: ListingType = pathSplit[5]
pathSplit[5]
? ListingType[pathSplit[5]] ? ListingType[pathSplit[5]]
: UserService.Instance.myUserInfo.match({ : mui
some: mui => ? Object.values(ListingType)[
Object.values(ListingType)[
mui.local_user_view.local_user.default_listing_type mui.local_user_view.local_user.default_listing_type
], ]
none: ListingType.Local, : ListingType.Local;
}) let sort: SortType = pathSplit[7]
);
let sort: Option<SortType> = Some(
pathSplit[7]
? SortType[pathSplit[7]] ? SortType[pathSplit[7]]
: UserService.Instance.myUserInfo.match({ : mui
some: mui => ? (Object.values(SortType)[
Object.values(SortType)[
mui.local_user_view.local_user.default_sort_type mui.local_user_view.local_user.default_sort_type
], ] as SortType)
none: SortType.Active, : SortType.Active;
})
);
let page = Some(pathSplit[9] ? Number(pathSplit[9]) : 1); let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
if (dataType == DataType.Post) { if (dataType == DataType.Post) {
let getPostsForm = new GetPosts({ let getPostsForm: GetPosts = {
community_id: None,
community_name: None,
type_, type_,
page, page,
limit: Some(fetchLimit), limit: fetchLimit,
sort, sort,
saved_only: Some(false), saved_only: false,
auth: req.auth, auth,
}); };
promises.push(req.client.getPosts(getPostsForm)); promises.push(req.client.getPosts(getPostsForm));
promises.push(Promise.resolve()); promises.push(Promise.resolve());
} else { } else {
let getCommentsForm = new GetComments({ let getCommentsForm: GetComments = {
community_id: None,
community_name: None,
page, page,
limit: Some(fetchLimit), limit: fetchLimit,
max_depth: None, sort: postToCommentSortType(sort),
sort: sort.map(postToCommentSortType),
type_, type_,
saved_only: Some(false), saved_only: false,
post_id: None, auth,
parent_id: None, };
auth: req.auth,
});
promises.push(Promise.resolve()); promises.push(Promise.resolve());
promises.push(req.client.getComments(getCommentsForm)); promises.push(req.client.getComments(getCommentsForm));
} }
let trendingCommunitiesForm = new ListCommunities({ let trendingCommunitiesForm: ListCommunities = {
type_: Some(ListingType.Local), type_: ListingType.Local,
sort: Some(SortType.Hot), sort: SortType.Hot,
limit: Some(trendingFetchLimit), limit: trendingFetchLimit,
page: None, auth,
auth: req.auth, };
});
promises.push(req.client.listCommunities(trendingCommunitiesForm)); promises.push(req.client.listCommunities(trendingCommunitiesForm));
return promises; return promises;
@ -317,33 +298,28 @@ export class Home extends Component<any, HomeState> {
get documentTitle(): string { get documentTitle(): string {
let siteView = this.state.siteRes.site_view; let siteView = this.state.siteRes.site_view;
return this.state.siteRes.site_view.site.description.match({ let desc = this.state.siteRes.site_view.site.description;
some: desc => `${siteView.site.name} - ${desc}`, return desc ? `${siteView.site.name} - ${desc}` : siteView.site.name;
none: siteView.site.name,
});
} }
render() { render() {
let tagline = this.state.tagline;
return ( return (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.siteRes.site_view.local_site.site_setup && ( {this.state.siteRes.site_view.local_site.site_setup && (
<div className="row"> <div className="row">
<main role="main" className="col-12 col-md-8"> <main role="main" className="col-12 col-md-8">
{this.state.tagline.match({ {tagline && (
some: tagline => (
<div <div
id="tagline" id="tagline"
dangerouslySetInnerHTML={mdToHtml(tagline)} dangerouslySetInnerHTML={mdToHtml(tagline)}
></div> ></div>
), )}
none: <></>,
})}
<div className="d-block d-md-none">{this.mobileView()}</div> <div className="d-block d-md-none">{this.mobileView()}</div>
{this.posts()} {this.posts()}
</main> </main>
@ -357,10 +333,8 @@ export class Home extends Component<any, HomeState> {
} }
get hasFollows(): boolean { get hasFollows(): boolean {
return UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => mui.follows.length > 0, return !!mui && mui.follows.length > 0;
none: false,
});
} }
mobileView() { mobileView() {
@ -412,9 +386,9 @@ export class Home extends Component<any, HomeState> {
{this.state.showSidebarMobile && ( {this.state.showSidebarMobile && (
<SiteSidebar <SiteSidebar
site={siteView.site} site={siteView.site}
admins={Some(siteRes.admins)} admins={siteRes.admins}
counts={Some(siteView.counts)} counts={siteView.counts}
online={Some(siteRes.online)} online={siteRes.online}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
/> />
)} )}
@ -450,9 +424,9 @@ export class Home extends Component<any, HomeState> {
</div> </div>
<SiteSidebar <SiteSidebar
site={siteView.site} site={siteView.site}
admins={Some(siteRes.admins)} admins={siteRes.admins}
counts={Some(siteView.counts)} counts={siteView.counts}
online={Some(siteRes.online)} online={siteRes.online}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
/> />
{this.hasFollows && ( {this.hasFollows && (
@ -532,10 +506,7 @@ export class Home extends Component<any, HomeState> {
</h5> </h5>
{!this.state.subscribedCollapsed && ( {!this.state.subscribedCollapsed && (
<ul className="list-inline mb-0"> <ul className="list-inline mb-0">
{UserService.Instance.myUserInfo {UserService.Instance.myUserInfo?.follows.map(cfv => (
.map(m => m.follows)
.unwrapOr([])
.map(cfv => (
<li <li
key={cfv.community.id} key={cfv.community.id}
className="list-inline-item d-inline-block" className="list-inline-item d-inline-block"
@ -595,9 +566,6 @@ export class Home extends Component<any, HomeState> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
showCommunity showCommunity
showContext showContext
@ -611,9 +579,10 @@ export class Home extends Component<any, HomeState> {
selects() { selects() {
let allRss = `/feeds/all.xml?sort=${this.state.sort}`; let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
let localRss = `/feeds/local.xml?sort=${this.state.sort}`; let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
let frontRss = auth(false) let auth = myAuth(false);
.ok() let frontRss = auth
.map(auth => `/feeds/front/${auth}.xml?sort=${this.state.sort}`); ? `/feeds/front/${auth}.xml?sort=${this.state.sort}`
: undefined;
return ( return (
<div className="mb-3"> <div className="mb-3">
@ -650,18 +619,14 @@ export class Home extends Component<any, HomeState> {
<link rel="alternate" type="application/atom+xml" href={localRss} /> <link rel="alternate" type="application/atom+xml" href={localRss} />
</> </>
)} )}
{this.state.listingType == ListingType.Subscribed && {this.state.listingType == ListingType.Subscribed && frontRss && (
frontRss.match({
some: rss => (
<> <>
<a href={rss} title="RSS" rel={relTags}> <a href={frontRss} title="RSS" rel={relTags}>
<Icon icon="rss" classes="text-muted small" /> <Icon icon="rss" classes="text-muted small" />
</a> </a>
<link rel="alternate" type="application/atom+xml" href={rss} /> <link rel="alternate" type="application/atom+xml" href={frontRss} />
</> </>
), )}
none: <></>,
})}
</div> </div>
); );
} }
@ -703,33 +668,27 @@ export class Home extends Component<any, HomeState> {
} }
fetchData() { fetchData() {
let auth = myAuth(false);
if (this.state.dataType == DataType.Post) { if (this.state.dataType == DataType.Post) {
let getPostsForm = new GetPosts({ let getPostsForm: GetPosts = {
community_id: None, page: this.state.page,
community_name: None, limit: fetchLimit,
page: Some(this.state.page), sort: this.state.sort,
limit: Some(fetchLimit), saved_only: false,
sort: Some(this.state.sort), type_: this.state.listingType,
saved_only: Some(false), auth,
auth: auth(false).ok(), };
type_: Some(this.state.listingType),
});
WebSocketService.Instance.send(wsClient.getPosts(getPostsForm)); WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
} else { } else {
let getCommentsForm = new GetComments({ let getCommentsForm: GetComments = {
community_id: None, page: this.state.page,
community_name: None, limit: fetchLimit,
page: Some(this.state.page), sort: postToCommentSortType(this.state.sort),
limit: Some(fetchLimit), saved_only: false,
max_depth: None, type_: this.state.listingType,
sort: Some(postToCommentSortType(this.state.sort)), auth,
saved_only: Some(false), };
post_id: None,
parent_id: None,
auth: auth(false).ok(),
type_: Some(this.state.listingType),
});
WebSocketService.Instance.send(wsClient.getComments(getCommentsForm)); WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
} }
} }
@ -746,17 +705,14 @@ export class Home extends Component<any, HomeState> {
); );
this.fetchData(); this.fetchData();
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>( let data = wsJsonToRes<ListCommunitiesResponse>(msg);
msg,
ListCommunitiesResponse
);
this.setState({ trendingCommunities: data.communities }); this.setState({ trendingCommunities: data.communities });
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg, SiteResponse); let data = wsJsonToRes<SiteResponse>(msg);
this.setState(s => ((s.siteRes.site_view = data.site_view), s)); this.setState(s => ((s.siteRes.site_view = data.site_view), s));
toast(i18n.t("site_saved")); toast(i18n.t("site_saved"));
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse); let data = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts: data.posts, loading: false }); this.setState({ posts: data.posts, loading: false });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 }) wsClient.communityJoin({ community_id: 0 })
@ -764,11 +720,10 @@ export class Home extends Component<any, HomeState> {
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
let mui = UserService.Instance.myUserInfo;
let showPostNotifs = UserService.Instance.myUserInfo let showPostNotifs = mui?.local_user_view.local_user.show_new_post_notifs;
.map(m => m.local_user_view.local_user.show_new_post_notifs)
.unwrapOr(false);
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
if ( if (
@ -779,9 +734,7 @@ export class Home extends Component<any, HomeState> {
// If you're on subscribed, only push it if you're subscribed. // If you're on subscribed, only push it if you're subscribed.
if (this.state.listingType == ListingType.Subscribed) { if (this.state.listingType == ListingType.Subscribed) {
if ( if (
UserService.Instance.myUserInfo mui?.follows
.map(m => m.follows)
.unwrapOr([])
.map(c => c.community.id) .map(c => c.community.id)
.includes(data.post_view.community.id) .includes(data.post_view.community.id)
) { ) {
@ -814,45 +767,43 @@ export class Home extends Component<any, HomeState> {
op == UserOperation.FeaturePost || op == UserOperation.FeaturePost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
editPostFindRes(data.post_view, this.state.posts); editPostFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(data.post_view, this.state.posts); createPostLikeFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = data.admins), s)); this.setState(s => ((s.siteRes.admins = data.admins), s));
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse); let data = wsJsonToRes<BanPersonResponse>(msg);
this.state.posts this.state.posts
.filter(p => p.creator.id == data.person_view.person.id) .filter(p => p.creator.id == data.person_view.person.id)
.forEach(p => (p.creator.banned = data.banned)); .forEach(p => (p.creator.banned = data.banned));
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse); let data = wsJsonToRes<GetCommentsResponse>(msg);
this.setState({ comments: data.comments, loading: false }); this.setState({ comments: data.comments, loading: false });
} else if ( } else if (
op == UserOperation.EditComment || op == UserOperation.EditComment ||
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes(data.comment_view, this.state.comments); editCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
// Necessary since it might be a user reply // Necessary since it might be a user reply
if (data.form_id) { if (data.form_id) {
// If you're on subscribed, only push it if you're subscribed. // If you're on subscribed, only push it if you're subscribed.
if (this.state.listingType == ListingType.Subscribed) { if (this.state.listingType == ListingType.Subscribed) {
if ( if (
UserService.Instance.myUserInfo UserService.Instance.myUserInfo?.follows
.map(m => m.follows)
.unwrapOr([])
.map(c => c.community.id) .map(c => c.community.id)
.includes(data.comment_view.community.id) .includes(data.comment_view.community.id)
) { ) {
@ -864,23 +815,23 @@ export class Home extends Component<any, HomeState> {
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(data.comment_view, this.state.comments); saveCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(data.comment_view, this.state.comments); createCommentLikeRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse); let data = wsJsonToRes<CommentReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
@ -890,7 +841,7 @@ export class Home extends Component<any, HomeState> {
op == UserOperation.PurgeComment || op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity op == UserOperation.PurgeCommunity
) { ) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse); let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) { if (data.success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -11,13 +10,12 @@ interface InstancesState {
export class Instances extends Component<any, InstancesState> { export class Instances extends Component<any, InstancesState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private emptyState: InstancesState = { state: InstancesState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
get documentTitle(): string { get documentTitle(): string {
@ -25,45 +23,37 @@ export class Instances extends Component<any, InstancesState> {
} }
render() { render() {
return this.state.siteRes.federated_instances.match({ let federated_instances = this.state.siteRes.federated_instances;
some: federated_instances => ( return federated_instances ? (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div className="row"> <div className="row">
<div className="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("linked_instances")}</h5> <h5>{i18n.t("linked_instances")}</h5>
{this.itemList(federated_instances.linked)} {this.itemList(federated_instances.linked)}
</div> </div>
{federated_instances.allowed.match({ {federated_instances.allowed &&
some: allowed => federated_instances.allowed.length > 0 && (
allowed.length > 0 && (
<div className="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("allowed_instances")}</h5> <h5>{i18n.t("allowed_instances")}</h5>
{this.itemList(allowed)} {this.itemList(federated_instances.allowed)}
</div> </div>
), )}
none: <></>, {federated_instances.blocked &&
})} federated_instances.blocked.length > 0 && (
{federated_instances.blocked.match({
some: blocked =>
blocked.length > 0 && (
<div className="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("blocked_instances")}</h5> <h5>{i18n.t("blocked_instances")}</h5>
{this.itemList(blocked)} {this.itemList(federated_instances.blocked)}
</div> </div>
), )}
none: <></>,
})}
</div> </div>
</div> </div>
), ) : (
none: <></>, <></>
}); );
} }
itemList(items: string[]) { itemList(items: string[]) {

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -11,13 +10,12 @@ interface LegalState {
export class Legal extends Component<any, LegalState> { export class Legal extends Component<any, LegalState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private emptyState: LegalState = { state: LegalState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
get documentTitle(): string { get documentTitle(): string {
@ -25,20 +23,16 @@ export class Legal extends Component<any, LegalState> {
} }
render() { render() {
let legal = this.state.siteRes.site_view.local_site.legal_information;
return ( return (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.siteRes.site_view.local_site.legal_information.match({ {legal && (
some: legal => (
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(legal)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(legal)} />
), )}
none: <></>,
})}
</div> </div>
); );
} }

View file

@ -1,8 +1,7 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse, GetSiteResponse,
Login as LoginForm, Login as LoginI,
LoginResponse, LoginResponse,
PasswordReset, PasswordReset,
UserOperation, UserOperation,
@ -24,20 +23,20 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
interface State { interface State {
loginForm: LoginForm; form: {
username_or_email?: string;
password?: string;
};
loginLoading: boolean; loginLoading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
} }
export class Login extends Component<any, State> { export class Login extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
emptyState: State = { state: State = {
loginForm: new LoginForm({ form: {},
username_or_email: undefined,
password: undefined,
}),
loginLoading: false, loginLoading: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
@ -45,8 +44,6 @@ export class Login extends Component<any, State> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
@ -57,14 +54,14 @@ export class Login extends Component<any, State> {
componentDidMount() { componentDidMount() {
// Navigate to home if already logged in // Navigate to home if already logged in
if (UserService.Instance.myUserInfo.isSome()) { if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/"); this.context.router.history.push("/");
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -82,8 +79,6 @@ export class Login extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div> <div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
@ -109,7 +104,7 @@ export class Login extends Component<any, State> {
type="text" type="text"
className="form-control" className="form-control"
id="login-email-or-username" id="login-email-or-username"
value={this.state.loginForm.username_or_email} value={this.state.form.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)} onInput={linkEvent(this, this.handleLoginUsernameChange)}
autoComplete="email" autoComplete="email"
required required
@ -125,7 +120,7 @@ export class Login extends Component<any, State> {
<input <input
type="password" type="password"
id="login-password" id="login-password"
value={this.state.loginForm.password} value={this.state.form.password}
onInput={linkEvent(this, this.handleLoginPasswordChange)} onInput={linkEvent(this, this.handleLoginPasswordChange)}
className="form-control" className="form-control"
autoComplete="current-password" autoComplete="current-password"
@ -136,7 +131,10 @@ export class Login extends Component<any, State> {
type="button" type="button"
onClick={linkEvent(this, this.handlePasswordReset)} onClick={linkEvent(this, this.handlePasswordReset)}
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed" className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
disabled={!validEmail(this.state.loginForm.username_or_email)} disabled={
!!this.state.form.username_or_email &&
!validEmail(this.state.form.username_or_email)
}
title={i18n.t("no_password_reset")} title={i18n.t("no_password_reset")}
> >
{i18n.t("forgot_password")} {i18n.t("forgot_password")}
@ -158,45 +156,54 @@ export class Login extends Component<any, State> {
handleLoginSubmit(i: Login, event: any) { handleLoginSubmit(i: Login, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ loginLoading: true }); i.setState({ loginLoading: true });
WebSocketService.Instance.send(wsClient.login(i.state.loginForm)); let lForm = i.state.form;
let username_or_email = lForm.username_or_email;
let password = lForm.password;
if (username_or_email && password) {
let form: LoginI = {
username_or_email,
password,
};
WebSocketService.Instance.send(wsClient.login(form));
}
} }
handleLoginUsernameChange(i: Login, event: any) { handleLoginUsernameChange(i: Login, event: any) {
i.state.loginForm.username_or_email = event.target.value; i.state.form.username_or_email = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleLoginPasswordChange(i: Login, event: any) { handleLoginPasswordChange(i: Login, event: any) {
i.state.loginForm.password = event.target.value; i.state.form.password = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handlePasswordReset(i: Login, event: any) { handlePasswordReset(i: Login, event: any) {
event.preventDefault(); event.preventDefault();
let resetForm = new PasswordReset({ let email = i.state.form.username_or_email;
email: i.state.loginForm.username_or_email, if (email) {
}); let resetForm: PasswordReset = { email };
WebSocketService.Instance.send(wsClient.passwordReset(resetForm)); WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
} }
}
parseMessage(msg: any) { parseMessage(msg: any) {
let op = wsUserOp(msg); let op = wsUserOp(msg);
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.setState(this.emptyState); this.setState({ form: {} });
return; return;
} else { } else {
if (op == UserOperation.Login) { if (op == UserOperation.Login) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
this.setState(this.emptyState);
UserService.Instance.login(data); UserService.Instance.login(data);
this.props.history.push("/"); this.props.history.push("/");
location.reload(); location.reload();
} else if (op == UserOperation.PasswordReset) { } else if (op == UserOperation.PasswordReset) {
toast(i18n.t("reset_password_mail_sent")); toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg);
this.setState({ siteRes: data }); this.setState({ siteRes: data });
} }
} }

View file

@ -1,11 +1,9 @@
import { None, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { import {
GetSiteResponse, GetSiteResponse,
LoginResponse, LoginResponse,
Register, Register,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -19,7 +17,17 @@ import { Spinner } from "../common/icon";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
interface State { interface State {
userForm: Register; form: {
username?: string;
email?: string;
password?: string;
password_verify?: string;
show_nsfw: boolean;
captcha_uuid?: string;
captcha_answer?: string;
honeypot?: string;
answer?: string;
};
doneRegisteringUser: boolean; doneRegisteringUser: boolean;
userLoading: boolean; userLoading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -29,20 +37,11 @@ export class Setup extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private emptyState: State = { state: State = {
userForm: new Register({ form: {
username: undefined,
password: undefined,
password_verify: undefined,
show_nsfw: true, show_nsfw: true,
// The first admin signup doesn't need a captcha },
captcha_uuid: None, doneRegisteringUser: !!UserService.Instance.myUserInfo,
captcha_answer: None,
email: None,
honeypot: None,
answer: None,
}),
doneRegisteringUser: UserService.Instance.myUserInfo.isSome(),
userLoading: false, userLoading: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
@ -50,8 +49,6 @@ export class Setup extends Component<any, State> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe( .subscribe(
@ -100,7 +97,7 @@ export class Setup extends Component<any, State> {
type="text" type="text"
className="form-control" className="form-control"
id="username" id="username"
value={this.state.userForm.username} value={this.state.form.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)} onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required required
minLength={3} minLength={3}
@ -119,7 +116,7 @@ export class Setup extends Component<any, State> {
id="email" id="email"
className="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.userForm.email)} value={this.state.form.email}
onInput={linkEvent(this, this.handleRegisterEmailChange)} onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3} minLength={3}
/> />
@ -133,7 +130,7 @@ export class Setup extends Component<any, State> {
<input <input
type="password" type="password"
id="password" id="password"
value={this.state.userForm.password} value={this.state.form.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)} onInput={linkEvent(this, this.handleRegisterPasswordChange)}
className="form-control" className="form-control"
required required
@ -151,7 +148,7 @@ export class Setup extends Component<any, State> {
<input <input
type="password" type="password"
id="verify-password" id="verify-password"
value={this.state.userForm.password_verify} value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
className="form-control" className="form-control"
required required
@ -176,26 +173,40 @@ export class Setup extends Component<any, State> {
event.preventDefault(); event.preventDefault();
i.setState({ userLoading: true }); i.setState({ userLoading: true });
event.preventDefault(); event.preventDefault();
WebSocketService.Instance.send(wsClient.register(i.state.userForm)); let cForm = i.state.form;
if (cForm.username && cForm.password && cForm.password_verify) {
let form: Register = {
username: cForm.username,
password: cForm.password,
password_verify: cForm.password_verify,
email: cForm.email,
show_nsfw: cForm.show_nsfw,
captcha_uuid: cForm.captcha_uuid,
captcha_answer: cForm.captcha_answer,
honeypot: cForm.honeypot,
answer: cForm.answer,
};
WebSocketService.Instance.send(wsClient.register(form));
}
} }
handleRegisterUsernameChange(i: Setup, event: any) { handleRegisterUsernameChange(i: Setup, event: any) {
i.state.userForm.username = event.target.value; i.state.form.username = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterEmailChange(i: Setup, event: any) { handleRegisterEmailChange(i: Setup, event: any) {
i.state.userForm.email = Some(event.target.value); i.state.form.email = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterPasswordChange(i: Setup, event: any) { handleRegisterPasswordChange(i: Setup, event: any) {
i.state.userForm.password = event.target.value; i.state.form.password = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterPasswordVerifyChange(i: Setup, event: any) { handleRegisterPasswordVerifyChange(i: Setup, event: any) {
i.state.userForm.password_verify = event.target.value; i.state.form.password_verify = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
@ -206,10 +217,10 @@ export class Setup extends Component<any, State> {
this.setState({ userLoading: false }); this.setState({ userLoading: false });
return; return;
} else if (op == UserOperation.Register) { } else if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
this.setState({ userLoading: false }); this.setState({ userLoading: false });
UserService.Instance.login(data); UserService.Instance.login(data);
if (UserService.Instance.jwtInfo.isSome()) { if (UserService.Instance.jwtInfo) {
this.setState({ doneRegisteringUser: true }); this.setState({ doneRegisteringUser: true });
} }
} else if (op == UserOperation.CreateSite) { } else if (op == UserOperation.CreateSite) {

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Options, passwordStrength } from "check-password-strength"; import { Options, passwordStrength } from "check-password-strength";
import { I18nKeys } from "i18next"; import { I18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
@ -9,8 +8,8 @@ import {
GetSiteResponse, GetSiteResponse,
LoginResponse, LoginResponse,
Register, Register,
RegistrationMode,
SiteView, SiteView,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -60,32 +59,33 @@ const passwordStrengthOptions: Options<string> = [
]; ];
interface State { interface State {
registerForm: Register; form: {
username?: string;
email?: string;
password?: string;
password_verify?: string;
show_nsfw: boolean;
captcha_uuid?: string;
captcha_answer?: string;
honeypot?: string;
answer?: string;
};
registerLoading: boolean; registerLoading: boolean;
captcha: Option<GetCaptchaResponse>; captcha?: GetCaptchaResponse;
captchaPlaying: boolean; captchaPlaying: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
} }
export class Signup extends Component<any, State> { export class Signup extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private audio: HTMLAudioElement; private audio?: HTMLAudioElement;
emptyState: State = { state: State = {
registerForm: new Register({ form: {
username: undefined,
password: undefined,
password_verify: undefined,
show_nsfw: false, show_nsfw: false,
captcha_uuid: None, },
captcha_answer: None,
honeypot: None,
answer: None,
email: None,
}),
registerLoading: false, registerLoading: false,
captcha: None,
captchaPlaying: false, captchaPlaying: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
@ -93,7 +93,6 @@ export class Signup extends Component<any, State> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleAnswerChange = this.handleAnswerChange.bind(this); this.handleAnswerChange = this.handleAnswerChange.bind(this);
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
@ -106,7 +105,7 @@ export class Signup extends Component<any, State> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -131,8 +130,6 @@ export class Signup extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3"> <div className="col-12 col-lg-6 offset-lg-3">
@ -172,7 +169,7 @@ export class Signup extends Component<any, State> {
type="text" type="text"
id="register-username" id="register-username"
className="form-control" className="form-control"
value={this.state.registerForm.username} value={this.state.form.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)} onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required required
minLength={3} minLength={3}
@ -196,14 +193,15 @@ export class Signup extends Component<any, State> {
? i18n.t("required") ? i18n.t("required")
: i18n.t("optional") : i18n.t("optional")
} }
value={toUndefined(this.state.registerForm.email)} value={this.state.form.email}
autoComplete="email" autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)} onInput={linkEvent(this, this.handleRegisterEmailChange)}
required={siteView.local_site.require_email_verification} required={siteView.local_site.require_email_verification}
minLength={3} minLength={3}
/> />
{!siteView.local_site.require_email_verification && {!siteView.local_site.require_email_verification &&
!this.state.registerForm.email.map(validEmail).unwrapOr(true) && ( this.state.form.email &&
!validEmail(this.state.form.email) && (
<div className="mt-2 mb-0 alert alert-warning" role="alert"> <div className="mt-2 mb-0 alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("no_password_reset")} {i18n.t("no_password_reset")}
@ -223,7 +221,7 @@ export class Signup extends Component<any, State> {
<input <input
type="password" type="password"
id="register-password" id="register-password"
value={this.state.registerForm.password} value={this.state.form.password}
autoComplete="new-password" autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)} onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10} minLength={10}
@ -231,7 +229,7 @@ export class Signup extends Component<any, State> {
className="form-control" className="form-control"
required required
/> />
{this.state.registerForm.password && ( {this.state.form.password && (
<div className={this.passwordColorClass}> <div className={this.passwordColorClass}>
{i18n.t(this.passwordStrength as I18nKeys)} {i18n.t(this.passwordStrength as I18nKeys)}
</div> </div>
@ -250,7 +248,7 @@ export class Signup extends Component<any, State> {
<input <input
type="password" type="password"
id="register-verify-password" id="register-verify-password"
value={this.state.registerForm.password_verify} value={this.state.form.password_verify}
autoComplete="new-password" autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
maxLength={60} maxLength={60}
@ -260,7 +258,8 @@ export class Signup extends Component<any, State> {
</div> </div>
</div> </div>
{siteView.local_site.require_application && ( {siteView.local_site.registration_mode ==
RegistrationMode.RequireApplication && (
<> <>
<div className="form-group row"> <div className="form-group row">
<div className="offset-sm-2 col-sm-10"> <div className="offset-sm-2 col-sm-10">
@ -268,15 +267,14 @@ export class Signup extends Component<any, State> {
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("fill_out_application")} {i18n.t("fill_out_application")}
</div> </div>
{siteView.local_site.application_question.match({ {siteView.local_site.application_question && (
some: question => (
<div <div
className="md-div" className="md-div"
dangerouslySetInnerHTML={mdToHtml(question)} dangerouslySetInnerHTML={mdToHtml(
siteView.local_site.application_question
)}
/> />
), )}
none: <></>,
})}
</div> </div>
</div> </div>
@ -289,11 +287,6 @@ export class Signup extends Component<any, State> {
</label> </label>
<div className="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={None}
initialLanguageId={None}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleAnswerChange} onContentChange={this.handleAnswerChange}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]} allLanguages={[]}
@ -304,7 +297,7 @@ export class Signup extends Component<any, State> {
</> </>
)} )}
{this.state.captcha.isSome() && ( {this.state.captcha && (
<div className="form-group row"> <div className="form-group row">
<label className="col-sm-2" htmlFor="register-captcha"> <label className="col-sm-2" htmlFor="register-captcha">
<span className="mr-2">{i18n.t("enter_code")}</span> <span className="mr-2">{i18n.t("enter_code")}</span>
@ -323,7 +316,7 @@ export class Signup extends Component<any, State> {
type="text" type="text"
className="form-control" className="form-control"
id="register-captcha" id="register-captcha"
value={toUndefined(this.state.registerForm.captcha_answer)} value={this.state.form.captcha_answer}
onInput={linkEvent( onInput={linkEvent(
this, this,
this.handleRegisterCaptchaAnswerChange this.handleRegisterCaptchaAnswerChange
@ -341,7 +334,7 @@ export class Signup extends Component<any, State> {
className="form-check-input" className="form-check-input"
id="register-show-nsfw" id="register-show-nsfw"
type="checkbox" type="checkbox"
checked={this.state.registerForm.show_nsfw} checked={this.state.form.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/> />
<label <label
@ -361,7 +354,7 @@ export class Signup extends Component<any, State> {
type="text" type="text"
className="form-control honeypot" className="form-control honeypot"
id="register-honey" id="register-honey"
value={toUndefined(this.state.registerForm.honeypot)} value={this.state.form.honeypot}
onInput={linkEvent(this, this.handleHoneyPotChange)} onInput={linkEvent(this, this.handleHoneyPotChange)}
/> />
<div className="form-group row"> <div className="form-group row">
@ -380,19 +373,17 @@ export class Signup extends Component<any, State> {
} }
showCaptcha() { showCaptcha() {
return this.state.captcha.match({ let captchaRes = this.state.captcha?.ok;
some: captcha => ( return captchaRes ? (
<div className="col-sm-4"> <div className="col-sm-4">
{captcha.ok.match({
some: res => (
<> <>
<img <img
className="rounded-top img-fluid" className="rounded-top img-fluid"
src={this.captchaPngSrc(res)} src={this.captchaPngSrc(captchaRes)}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;" style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
alt={i18n.t("captcha")} alt={i18n.t("captcha")}
/> />
{res.wav.isSome() && ( {captchaRes.wav && (
<button <button
className="rounded-bottom btn btn-sm btn-secondary btn-block" className="rounded-bottom btn btn-sm btn-secondary btn-block"
style="border-top-right-radius: 0; border-top-left-radius: 0;" style="border-top-right-radius: 0; border-top-left-radius: 0;"
@ -405,26 +396,23 @@ export class Signup extends Component<any, State> {
</button> </button>
)} )}
</> </>
),
none: <></>,
})}
</div> </div>
), ) : (
none: <></>, <></>
}); );
} }
get passwordStrength() { get passwordStrength(): string | undefined {
return passwordStrength( let password = this.state.form.password;
this.state.registerForm.password, return password
passwordStrengthOptions ? passwordStrength(password, passwordStrengthOptions).value
).value; : undefined;
} }
get passwordColorClass(): string { get passwordColorClass(): string {
let strength = this.passwordStrength; let strength = this.passwordStrength;
if (["weak", "medium"].includes(strength)) { if (strength && ["weak", "medium"].includes(strength)) {
return "text-warning"; return "text-warning";
} else if (strength == "strong") { } else if (strength == "strong") {
return "text-success"; return "text-success";
@ -436,53 +424,67 @@ export class Signup extends Component<any, State> {
handleRegisterSubmit(i: Signup, event: any) { handleRegisterSubmit(i: Signup, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ registerLoading: true }); i.setState({ registerLoading: true });
WebSocketService.Instance.send(wsClient.register(i.state.registerForm)); let cForm = i.state.form;
if (cForm.username && cForm.password && cForm.password_verify) {
let form: Register = {
username: cForm.username,
password: cForm.password,
password_verify: cForm.password_verify,
email: cForm.email,
show_nsfw: cForm.show_nsfw,
captcha_uuid: cForm.captcha_uuid,
captcha_answer: cForm.captcha_answer,
honeypot: cForm.honeypot,
answer: cForm.answer,
};
WebSocketService.Instance.send(wsClient.register(form));
}
} }
handleRegisterUsernameChange(i: Signup, event: any) { handleRegisterUsernameChange(i: Signup, event: any) {
i.state.registerForm.username = event.target.value; i.state.form.username = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterEmailChange(i: Signup, event: any) { handleRegisterEmailChange(i: Signup, event: any) {
i.state.registerForm.email = Some(event.target.value); i.state.form.email = event.target.value;
if (i.state.registerForm.email.unwrap() == "") { if (i.state.form.email == "") {
i.state.registerForm.email = None; i.state.form.email = undefined;
} }
i.setState(i.state); i.setState(i.state);
} }
handleRegisterPasswordChange(i: Signup, event: any) { handleRegisterPasswordChange(i: Signup, event: any) {
i.state.registerForm.password = event.target.value; i.state.form.password = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterPasswordVerifyChange(i: Signup, event: any) { handleRegisterPasswordVerifyChange(i: Signup, event: any) {
i.state.registerForm.password_verify = event.target.value; i.state.form.password_verify = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterShowNsfwChange(i: Signup, event: any) { handleRegisterShowNsfwChange(i: Signup, event: any) {
i.state.registerForm.show_nsfw = event.target.checked; i.state.form.show_nsfw = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleRegisterCaptchaAnswerChange(i: Signup, event: any) { handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
i.state.registerForm.captcha_answer = Some(event.target.value); i.state.form.captcha_answer = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleAnswerChange(val: string) { handleAnswerChange(val: string) {
this.setState(s => ((s.registerForm.answer = Some(val)), s)); this.setState(s => ((s.form.answer = val), s));
} }
handleHoneyPotChange(i: Signup, event: any) { handleHoneyPotChange(i: Signup, event: any) {
i.state.registerForm.honeypot = Some(event.target.value); i.state.form.honeypot = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleRegenCaptcha(i: Signup) { handleRegenCaptcha(i: Signup) {
i.audio = null; i.audio = undefined;
i.setState({ captchaPlaying: false }); i.setState({ captchaPlaying: false });
WebSocketService.Instance.send(wsClient.getCaptcha()); WebSocketService.Instance.send(wsClient.getCaptcha());
} }
@ -490,28 +492,23 @@ export class Signup extends Component<any, State> {
handleCaptchaPlay(i: Signup) { handleCaptchaPlay(i: Signup) {
// This was a bad bug, it should only build the new audio on a new file. // This was a bad bug, it should only build the new audio on a new file.
// Replays would stop prematurely if this was rebuilt every time. // Replays would stop prematurely if this was rebuilt every time.
i.state.captcha.match({ let captchaRes = i.state.captcha?.ok;
some: captcha => if (captchaRes) {
captcha.ok.match({ if (!i.audio) {
some: res => { let base64 = `data:audio/wav;base64,${captchaRes.wav}`;
if (i.audio == null) {
let base64 = `data:audio/wav;base64,${res.wav}`;
i.audio = new Audio(base64); i.audio = new Audio(base64);
}
i.audio.play(); i.audio.play();
i.setState({ captchaPlaying: true }); i.setState({ captchaPlaying: true });
i.audio.addEventListener("ended", () => { i.audio.addEventListener("ended", () => {
if (i.audio) {
i.audio.currentTime = 0; i.audio.currentTime = 0;
i.setState({ captchaPlaying: false }); i.setState({ captchaPlaying: false });
}
}); });
}, }
none: void 0, }
}),
none: void 0,
});
} }
captchaPngSrc(captcha: CaptchaResponse) { captchaPngSrc(captcha: CaptchaResponse) {
@ -523,17 +520,15 @@ export class Signup extends Component<any, State> {
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.setState(this.emptyState); this.setState(s => ((s.form.captcha_answer = undefined), s));
this.setState(s => ((s.registerForm.captcha_answer = undefined), s));
// Refetch another captcha // Refetch another captcha
// WebSocketService.Instance.send(wsClient.getCaptcha()); // WebSocketService.Instance.send(wsClient.getCaptcha());
return; return;
} else { } else {
if (op == UserOperation.Register) { if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
this.setState(this.emptyState);
// Only log them in if a jwt was set // Only log them in if a jwt was set
if (data.jwt.isSome()) { if (data.jwt) {
UserService.Instance.login(data); UserService.Instance.login(data);
this.props.history.push("/communities"); this.props.history.push("/communities");
location.reload(); location.reload();
@ -547,20 +542,15 @@ export class Signup extends Component<any, State> {
this.props.history.push("/"); this.props.history.push("/");
} }
} else if (op == UserOperation.GetCaptcha) { } else if (op == UserOperation.GetCaptcha) {
let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse); let data = wsJsonToRes<GetCaptchaResponse>(msg);
data.ok.match({ if (data.ok) {
some: res => { this.setState({ captcha: data });
this.setState({ captcha: Some(data) }); this.setState(s => ((s.form.captcha_uuid = data.ok?.uuid), s));
this.setState( }
s => ((s.registerForm.captcha_uuid = Some(res.uuid)), s)
);
},
none: void 0,
});
} else if (op == UserOperation.PasswordReset) { } else if (op == UserOperation.PasswordReset) {
toast(i18n.t("reset_password_mail_sent")); toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg);
this.setState({ siteRes: data }); this.setState({ siteRes: data });
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
import { None, Option } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client"; import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client";
@ -11,9 +10,9 @@ import { PersonListing } from "../person/person-listing";
interface SiteSidebarProps { interface SiteSidebarProps {
site: Site; site: Site;
showLocal: boolean; showLocal: boolean;
counts: Option<SiteAggregates>; counts?: SiteAggregates;
admins: Option<PersonViewSafe[]>; admins?: PersonViewSafe[];
online: Option<number>; online?: number;
} }
interface SiteSidebarState { interface SiteSidebarState {
@ -21,13 +20,12 @@ interface SiteSidebarState {
} }
export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> { export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
private emptyState: SiteSidebarState = { state: SiteSidebarState = {
collapsed: false, collapsed: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
render() { render() {
@ -38,7 +36,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
<div className="mb-2">{this.siteName()}</div> <div className="mb-2">{this.siteName()}</div>
{!this.state.collapsed && ( {!this.state.collapsed && (
<> <>
<BannerIconHeader banner={this.props.site.banner} icon={None} /> <BannerIconHeader banner={this.props.site.banner} />
{this.siteInfo()} {this.siteInfo()}
</> </>
)} )}
@ -72,22 +70,10 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
let site = this.props.site; let site = this.props.site;
return ( return (
<div> <div>
{site.description.match({ {site.description && <h6>{site.description}</h6>}
some: description => <h6>{description}</h6>, {site.sidebar && this.siteSidebar(site.sidebar)}
none: <></>, {this.props.counts && this.badges(this.props.counts)}
})} {this.props.admins && this.admins(this.props.admins)}
{site.sidebar.match({
some: sidebar => this.siteSidebar(sidebar),
none: <></>,
})}
{this.props.counts.match({
some: counts => this.badges(counts),
none: <></>,
})}
{this.props.admins.match({
some: admins => this.admins(admins),
none: <></>,
})}
</div> </div>
); );
} }
@ -113,7 +99,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
badges(siteAggregates: SiteAggregates) { badges(siteAggregates: SiteAggregates) {
let counts = siteAggregates; let counts = siteAggregates;
let online = this.props.online.unwrapOr(1); let online = this.props.online ?? 1;
return ( return (
<ul className="my-2 list-inline"> <ul className="my-2 list-inline">
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -24,7 +23,6 @@ import {
ModRemovePostView, ModRemovePostView,
ModTransferCommunityView, ModTransferCommunityView,
PersonSafe, PersonSafe,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -37,12 +35,12 @@ import { WebSocketService } from "../services";
import { import {
amAdmin, amAdmin,
amMod, amMod,
auth,
choicesConfig, choicesConfig,
debounce, debounce,
fetchLimit, fetchLimit,
fetchUsers, fetchUsers,
isBrowser, isBrowser,
myAuth,
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
@ -57,7 +55,7 @@ import { PersonListing } from "./person/person-listing";
type ModlogType = { type ModlogType = {
id: number; id: number;
type_: ModlogActionType; type_: ModlogActionType;
moderator: Option<PersonSafe>; moderator?: PersonSafe;
view: view:
| ModRemovePostView | ModRemovePostView
| ModLockPostView | ModLockPostView
@ -81,43 +79,32 @@ if (isBrowser()) {
} }
interface ModlogState { interface ModlogState {
res: Option<GetModlogResponse>; res?: GetModlogResponse;
communityId: Option<number>; communityId?: number;
communityMods: Option<CommunityModeratorView[]>; communityMods?: CommunityModeratorView[];
communityName: Option<string>; communityName?: string;
page: number; page: number;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
filter_action: ModlogActionType; filter_action: ModlogActionType;
filter_user: Option<number>; filter_user?: number;
filter_mod: Option<number>; filter_mod?: number;
} }
export class Modlog extends Component<any, ModlogState> { export class Modlog extends Component<any, ModlogState> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
GetModlogResponse,
GetCommunityResponse
);
private subscription: Subscription;
private userChoices: any; private userChoices: any;
private modChoices: any; private modChoices: any;
private emptyState: ModlogState = { state: ModlogState = {
res: None,
communityId: None,
communityMods: None,
communityName: None,
page: 1, page: 1,
loading: true, loading: true,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
filter_action: ModlogActionType.All, filter_action: ModlogActionType.All,
filter_user: None,
filter_mod: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
@ -126,27 +113,25 @@ export class Modlog extends Component<any, ModlogState> {
this.state = { this.state = {
...this.state, ...this.state,
communityId: this.props.match.params.community_id communityId: this.props.match.params.community_id
? Some(Number(this.props.match.params.community_id)) ? Number(this.props.match.params.community_id)
: None, : undefined,
}; };
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
res: Some(this.isoData.routeData[0] as GetModlogResponse), res: this.isoData.routeData[0] as GetModlogResponse,
}; };
if (this.isoData.routeData[1]) { let communityRes: GetCommunityResponse | undefined =
this.isoData.routeData[1];
// Getting the moderators // Getting the moderators
let communityRes = Some(
this.isoData.routeData[1] as GetCommunityResponse
);
this.state = { this.state = {
...this.state, ...this.state,
communityMods: communityRes.map(c => c.moderators), communityMods: communityRes?.moderators,
}; };
}
this.state = { ...this.state, loading: false }; this.state = { ...this.state, loading: false };
} else { } else {
@ -161,7 +146,7 @@ export class Modlog extends Component<any, ModlogState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -310,22 +295,20 @@ export class Modlog extends Component<any, ModlogState> {
switch (i.type_) { switch (i.type_) {
case ModlogActionType.ModRemovePost: { case ModlogActionType.ModRemovePost: {
let mrpv = i.view as ModRemovePostView; let mrpv = i.view as ModRemovePostView;
let reason = mrpv.mod_remove_post.reason;
return ( return (
<> <>
<span> <span>
{mrpv.mod_remove_post.removed.unwrapOr(false) {mrpv.mod_remove_post.removed ? "Removed " : "Restored "}
? "Removed "
: "Restored "}
</span> </span>
<span> <span>
Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link> Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
</span> </span>
{reason && (
<span> <span>
{mrpv.mod_remove_post.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
@ -333,11 +316,7 @@ export class Modlog extends Component<any, ModlogState> {
let mlpv = i.view as ModLockPostView; let mlpv = i.view as ModLockPostView;
return ( return (
<> <>
<span> <span>{mlpv.mod_lock_post.locked ? "Locked " : "Unlocked "}</span>
{mlpv.mod_lock_post.locked.unwrapOr(false)
? "Locked "
: "Unlocked "}
</span>
<span> <span>
Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link> Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
</span> </span>
@ -364,12 +343,11 @@ export class Modlog extends Component<any, ModlogState> {
} }
case ModlogActionType.ModRemoveComment: { case ModlogActionType.ModRemoveComment: {
let mrc = i.view as ModRemoveCommentView; let mrc = i.view as ModRemoveCommentView;
let reason = mrc.mod_remove_comment.reason;
return ( return (
<> <>
<span> <span>
{mrc.mod_remove_comment.removed.unwrapOr(false) {mrc.mod_remove_comment.removed ? "Removed " : "Restored "}
? "Removed "
: "Restored "}
</span> </span>
<span> <span>
Comment{" "} Comment{" "}
@ -381,52 +359,47 @@ export class Modlog extends Component<any, ModlogState> {
{" "} {" "}
by <PersonListing person={mrc.commenter} /> by <PersonListing person={mrc.commenter} />
</span> </span>
{reason && (
<span> <span>
{mrc.mod_remove_comment.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
case ModlogActionType.ModRemoveCommunity: { case ModlogActionType.ModRemoveCommunity: {
let mrco = i.view as ModRemoveCommunityView; let mrco = i.view as ModRemoveCommunityView;
let reason = mrco.mod_remove_community.reason;
let expires = mrco.mod_remove_community.expires;
return ( return (
<> <>
<span> <span>
{mrco.mod_remove_community.removed.unwrapOr(false) {mrco.mod_remove_community.removed ? "Removed " : "Restored "}
? "Removed "
: "Restored "}
</span> </span>
<span> <span>
Community <CommunityLink community={mrco.community} /> Community <CommunityLink community={mrco.community} />
</span> </span>
{reason && (
<span> <span>
{mrco.mod_remove_community.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
{expires && (
<span> <span>
{mrco.mod_remove_community.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div> <div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
case ModlogActionType.ModBanFromCommunity: { case ModlogActionType.ModBanFromCommunity: {
let mbfc = i.view as ModBanFromCommunityView; let mbfc = i.view as ModBanFromCommunityView;
let reason = mbfc.mod_ban_from_community.reason;
let expires = mbfc.mod_ban_from_community.expires;
return ( return (
<> <>
<span> <span>
{mbfc.mod_ban_from_community.banned.unwrapOr(false) {mbfc.mod_ban_from_community.banned ? "Banned " : "Unbanned "}{" "}
? "Banned "
: "Unbanned "}{" "}
</span> </span>
<span> <span>
<PersonListing person={mbfc.banned_person} /> <PersonListing person={mbfc.banned_person} />
@ -435,20 +408,16 @@ export class Modlog extends Component<any, ModlogState> {
<span> <span>
<CommunityLink community={mbfc.community} /> <CommunityLink community={mbfc.community} />
</span> </span>
{reason && (
<span> <span>
{mbfc.mod_ban_from_community.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
{expires && (
<span> <span>
{mbfc.mod_ban_from_community.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div> <div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
@ -457,9 +426,7 @@ export class Modlog extends Component<any, ModlogState> {
return ( return (
<> <>
<span> <span>
{mac.mod_add_community.removed.unwrapOr(false) {mac.mod_add_community.removed ? "Removed " : "Appointed "}{" "}
? "Removed "
: "Appointed "}{" "}
</span> </span>
<span> <span>
<PersonListing person={mac.modded_person} /> <PersonListing person={mac.modded_person} />
@ -476,9 +443,7 @@ export class Modlog extends Component<any, ModlogState> {
return ( return (
<> <>
<span> <span>
{mtc.mod_transfer_community.removed.unwrapOr(false) {mtc.mod_transfer_community.removed ? "Removed " : "Transferred "}{" "}
? "Removed "
: "Transferred "}{" "}
</span> </span>
<span> <span>
<CommunityLink community={mtc.community} /> <CommunityLink community={mtc.community} />
@ -492,28 +457,24 @@ export class Modlog extends Component<any, ModlogState> {
} }
case ModlogActionType.ModBan: { case ModlogActionType.ModBan: {
let mb = i.view as ModBanView; let mb = i.view as ModBanView;
let reason = mb.mod_ban.reason;
let expires = mb.mod_ban.expires;
return ( return (
<> <>
<span> <span>{mb.mod_ban.banned ? "Banned " : "Unbanned "} </span>
{mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
</span>
<span> <span>
<PersonListing person={mb.banned_person} /> <PersonListing person={mb.banned_person} />
</span> </span>
{reason && (
<span> <span>
{mb.mod_ban.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
{expires && (
<span> <span>
{mb.mod_ban.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div> <div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
@ -521,9 +482,7 @@ export class Modlog extends Component<any, ModlogState> {
let ma = i.view as ModAddView; let ma = i.view as ModAddView;
return ( return (
<> <>
<span> <span>{ma.mod_add.removed ? "Removed " : "Appointed "} </span>
{ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
</span>
<span> <span>
<PersonListing person={ma.modded_person} /> <PersonListing person={ma.modded_person} />
</span> </span>
@ -533,61 +492,61 @@ export class Modlog extends Component<any, ModlogState> {
} }
case ModlogActionType.AdminPurgePerson: { case ModlogActionType.AdminPurgePerson: {
let ap = i.view as AdminPurgePersonView; let ap = i.view as AdminPurgePersonView;
let reason = ap.admin_purge_person.reason;
return ( return (
<> <>
<span>Purged a Person</span> <span>Purged a Person</span>
{reason && (
<span> <span>
{ap.admin_purge_person.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
case ModlogActionType.AdminPurgeCommunity: { case ModlogActionType.AdminPurgeCommunity: {
let ap = i.view as AdminPurgeCommunityView; let ap = i.view as AdminPurgeCommunityView;
let reason = ap.admin_purge_community.reason;
return ( return (
<> <>
<span>Purged a Community</span> <span>Purged a Community</span>
{reason && (
<span> <span>
{ap.admin_purge_community.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
case ModlogActionType.AdminPurgePost: { case ModlogActionType.AdminPurgePost: {
let ap = i.view as AdminPurgePostView; let ap = i.view as AdminPurgePostView;
let reason = ap.admin_purge_post.reason;
return ( return (
<> <>
<span>Purged a Post from from </span> <span>Purged a Post from from </span>
<CommunityLink community={ap.community} /> <CommunityLink community={ap.community} />
{reason && (
<span> <span>
{ap.admin_purge_post.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
case ModlogActionType.AdminPurgeComment: { case ModlogActionType.AdminPurgeComment: {
let ap = i.view as AdminPurgeCommentView; let ap = i.view as AdminPurgeCommentView;
let reason = ap.admin_purge_comment.reason;
return ( return (
<> <>
<span> <span>
Purged a Comment from{" "} Purged a Comment from{" "}
<Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link> <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
</span> </span>
{reason && (
<span> <span>
{ap.admin_purge_comment.reason.match({ <div>reason: {reason}</div>
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span> </span>
)}
</> </>
); );
} }
@ -597,18 +556,19 @@ export class Modlog extends Component<any, ModlogState> {
} }
combined() { combined() {
let combined = this.state.res.map(this.buildCombined).unwrapOr([]); let res = this.state.res;
let combined = res ? this.buildCombined(res) : [];
return ( return (
<tbody> <tbody>
{combined.map(i => ( {combined.map(i => (
<tr key={i.id}> <tr key={i.id}>
<td> <td>
<MomentTime published={i.when_} updated={None} /> <MomentTime published={i.when_} />
</td> </td>
<td> <td>
{this.amAdminOrMod ? ( {this.amAdminOrMod && i.moderator ? (
<PersonListing person={i.moderator.unwrap()} /> <PersonListing person={i.moderator} />
) : ( ) : (
<div>{this.modOrAdminText(i.moderator)}</div> <div>{this.modOrAdminText(i.moderator)}</div>
)} )}
@ -624,14 +584,12 @@ export class Modlog extends Component<any, ModlogState> {
return amAdmin() || amMod(this.state.communityMods); return amAdmin() || amMod(this.state.communityMods);
} }
modOrAdminText(person: Option<PersonSafe>): string { modOrAdminText(person?: PersonSafe): string {
return person.match({ return person
some: res => ? this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
this.isoData.site_res.admins.map(a => a.person.id).includes(res.id)
? i18n.t("admin") ? i18n.t("admin")
: i18n.t("mod"), : i18n.t("mod")
none: i18n.t("mod"), : i18n.t("mod");
});
} }
get documentTitle(): string { get documentTitle(): string {
@ -639,13 +597,12 @@ export class Modlog extends Component<any, ModlogState> {
} }
render() { render() {
let communityName = this.state.communityName;
return ( return (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -654,14 +611,11 @@ export class Modlog extends Component<any, ModlogState> {
) : ( ) : (
<div> <div>
<h5> <h5>
{this.state.communityName.match({ {communityName && (
some: name => ( <Link className="text-body" to={`/c/${communityName}`}>
<Link className="text-body" to={`/c/${name}`}> /c/{communityName}{" "}
/c/{name}{" "}
</Link> </Link>
), )}
none: <></>,
})}
<span>{i18n.t("modlog")}</span> <span>{i18n.t("modlog")}</span>
</h5> </h5>
<div className="form-row"> <div className="form-row">
@ -714,7 +668,7 @@ export class Modlog extends Component<any, ModlogState> {
<select <select
id="filter-mod" id="filter-mod"
className="form-control" className="form-control"
value={toUndefined(this.state.filter_mod)} value={this.state.filter_mod}
> >
<option>{i18n.t("filter_by_mod")}</option> <option>{i18n.t("filter_by_mod")}</option>
</select> </select>
@ -724,7 +678,7 @@ export class Modlog extends Component<any, ModlogState> {
<select <select
id="filter-user" id="filter-user"
className="form-control" className="form-control"
value={toUndefined(this.state.filter_user)} value={this.state.filter_user}
> >
<option>{i18n.t("filter_by_user")}</option> <option>{i18n.t("filter_by_user")}</option>
</select> </select>
@ -763,28 +717,26 @@ export class Modlog extends Component<any, ModlogState> {
} }
refetch() { refetch() {
let modlogForm = new GetModlog({ let auth = myAuth(false);
let modlogForm: GetModlog = {
community_id: this.state.communityId, community_id: this.state.communityId,
page: Some(this.state.page), page: this.state.page,
limit: Some(fetchLimit), limit: fetchLimit,
auth: auth(false).ok(),
type_: this.state.filter_action, type_: this.state.filter_action,
other_person_id: this.state.filter_user, other_person_id: this.state.filter_user,
mod_person_id: this.state.filter_mod, mod_person_id: this.state.filter_mod,
}); auth,
};
WebSocketService.Instance.send(wsClient.getModlog(modlogForm)); WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
this.state.communityId.match({ let communityId = this.state.communityId;
some: id => { if (communityId) {
let communityForm = new GetCommunity({ let communityForm: GetCommunity = {
id: Some(id), id: communityId,
name: None, auth,
auth: auth(false).ok(), };
});
WebSocketService.Instance.send(wsClient.getCommunity(communityForm)); WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
}, }
none: void 0,
});
} }
setupUserFilter() { setupUserFilter() {
@ -795,7 +747,7 @@ export class Modlog extends Component<any, ModlogState> {
this.userChoices.passedElement.element.addEventListener( this.userChoices.passedElement.element.addEventListener(
"choice", "choice",
(e: any) => { (e: any) => {
this.setState({ filter_user: Some(Number(e.detail.choice.value)) }); this.setState({ filter_user: Number(e.detail.choice.value) });
this.refetch(); this.refetch();
}, },
false false
@ -834,7 +786,7 @@ export class Modlog extends Component<any, ModlogState> {
this.modChoices.passedElement.element.addEventListener( this.modChoices.passedElement.element.addEventListener(
"choice", "choice",
(e: any) => { (e: any) => {
this.setState({ filter_mod: Some(Number(e.detail.choice.value)) }); this.setState({ filter_mod: Number(e.detail.choice.value) });
this.refetch(); this.refetch();
}, },
false false
@ -867,27 +819,25 @@ export class Modlog extends Component<any, ModlogState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let communityId = Some(pathSplit[3]).map(Number); let communityId = pathSplit[3] ? Number(pathSplit[3]) : undefined;
let auth = req.auth;
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let modlogForm = new GetModlog({ let modlogForm: GetModlog = {
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
community_id: communityId, community_id: communityId,
mod_person_id: None,
auth: req.auth,
type_: ModlogActionType.All, type_: ModlogActionType.All,
other_person_id: None, auth,
}); };
promises.push(req.client.getModlog(modlogForm)); promises.push(req.client.getModlog(modlogForm));
if (communityId.isSome()) { if (communityId) {
let communityForm = new GetCommunity({ let communityForm: GetCommunity = {
id: communityId, id: communityId,
name: None,
auth: req.auth, auth: req.auth,
}); };
promises.push(req.client.getCommunity(communityForm)); promises.push(req.client.getCommunity(communityForm));
} else { } else {
promises.push(Promise.resolve()); promises.push(Promise.resolve());
@ -902,16 +852,16 @@ export class Modlog extends Component<any, ModlogState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.GetModlog) { } else if (op == UserOperation.GetModlog) {
let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse); let data = wsJsonToRes<GetModlogResponse>(msg);
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState({ res: Some(data), loading: false }); this.setState({ res: data, loading: false });
this.setupUserFilter(); this.setupUserFilter();
this.setupModFilter(); this.setupModFilter();
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({ this.setState({
communityMods: Some(data.moderators), communityMods: data.moderators,
communityName: Some(data.community_view.community.name), communityName: data.community_view.community.name,
}); });
} }
} }

View file

@ -1,4 +1,3 @@
import { None, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockPersonResponse, BlockPersonResponse,
@ -30,13 +29,13 @@ import { i18n } from "../../i18next";
import { CommentViewType, InitialFetchRequest } from "../../interfaces"; import { CommentViewType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
editCommentRes, editCommentRes,
enableDownvotes, enableDownvotes,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
myAuth,
relTags, relTags,
saveCommentRes, saveCommentRes,
setIsoData, setIsoData,
@ -91,14 +90,9 @@ interface InboxState {
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
GetRepliesResponse, state: InboxState = {
GetPersonMentionsResponse,
PrivateMessagesResponse
);
private subscription: Subscription;
private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
replies: [], replies: [],
@ -114,11 +108,10 @@ export class Inbox extends Component<any, InboxState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -148,24 +141,22 @@ export class Inbox extends Component<any, InboxState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
get documentTitle(): string { get documentTitle(): string {
return UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => return mui
`@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${ ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
this.state.siteRes.site_view.site.name this.state.siteRes.site_view.site.name
}`, }`
none: "", : "";
});
} }
render() { render() {
let inboxRss = auth() let auth = myAuth();
.ok() let inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
.map(a => `/feeds/inbox/${a}.xml`);
return ( return (
<div className="container-lg"> <div className="container-lg">
{this.state.loading ? ( {this.state.loading ? (
@ -178,26 +169,21 @@ export class Inbox extends Component<any, InboxState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 className="mb-2"> <h5 className="mb-2">
{i18n.t("inbox")} {i18n.t("inbox")}
{inboxRss.match({ {inboxRss && (
some: rss => (
<small> <small>
<a href={rss} title="RSS" rel={relTags}> <a href={inboxRss} title="RSS" rel={relTags}>
<Icon icon="rss" classes="ml-2 text-muted small" /> <Icon icon="rss" classes="ml-2 text-muted small" />
</a> </a>
<link <link
rel="alternate" rel="alternate"
type="application/atom+xml" type="application/atom+xml"
href={rss} href={inboxRss}
/> />
</small> </small>
), )}
none: <></>,
})}
</h5> </h5>
{this.state.replies.length + {this.state.replies.length +
this.state.mentions.length + this.state.mentions.length +
@ -387,9 +373,6 @@ export class Inbox extends Component<any, InboxState> {
{ comment_view: i.view as CommentView, children: [], depth: 0 }, { comment_view: i.view as CommentView, children: [], depth: 0 },
]} ]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
@ -411,9 +394,6 @@ export class Inbox extends Component<any, InboxState> {
}, },
]} ]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
@ -445,9 +425,6 @@ export class Inbox extends Component<any, InboxState> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.replies)} nodes={commentsToFlatNodes(this.state.replies)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
@ -468,9 +445,6 @@ export class Inbox extends Component<any, InboxState> {
key={umv.person_mention.id} key={umv.person_mention.id}
nodes={[{ comment_view: umv, children: [], depth: 0 }]} nodes={[{ comment_view: umv, children: [], depth: 0 }]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
@ -515,74 +489,80 @@ export class Inbox extends Component<any, InboxState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let sort = Some(CommentSortType.New); let sort = CommentSortType.New;
let auth = req.auth;
if (auth) {
// It can be /u/me, or /username/1 // It can be /u/me, or /username/1
let repliesForm = new GetReplies({ let repliesForm: GetReplies = {
sort, sort,
unread_only: Some(true), unread_only: true,
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
auth: req.auth.unwrap(), auth,
}); };
promises.push(req.client.getReplies(repliesForm)); promises.push(req.client.getReplies(repliesForm));
let personMentionsForm = new GetPersonMentions({ let personMentionsForm: GetPersonMentions = {
sort, sort,
unread_only: Some(true), unread_only: true,
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
auth: req.auth.unwrap(), auth,
}); };
promises.push(req.client.getPersonMentions(personMentionsForm)); promises.push(req.client.getPersonMentions(personMentionsForm));
let privateMessagesForm = new GetPrivateMessages({ let privateMessagesForm: GetPrivateMessages = {
unread_only: Some(true), unread_only: true,
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
auth: req.auth.unwrap(), auth,
}); };
promises.push(req.client.getPrivateMessages(privateMessagesForm)); promises.push(req.client.getPrivateMessages(privateMessagesForm));
}
return promises; return promises;
} }
refetch() { refetch() {
let sort = Some(this.state.sort); let sort = this.state.sort;
let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread); let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
let page = Some(this.state.page); let page = this.state.page;
let limit = Some(fetchLimit); let limit = fetchLimit;
let auth = myAuth();
let repliesForm = new GetReplies({ if (auth) {
let repliesForm: GetReplies = {
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.getReplies(repliesForm)); WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
let personMentionsForm = new GetPersonMentions({ let personMentionsForm: GetPersonMentions = {
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPersonMentions(personMentionsForm) wsClient.getPersonMentions(personMentionsForm)
); );
let privateMessagesForm = new GetPrivateMessages({ let privateMessagesForm: GetPrivateMessages = {
unread_only, unread_only,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPrivateMessages(privateMessagesForm) wsClient.getPrivateMessages(privateMessagesForm)
); );
} }
}
handleSortChange(val: CommentSortType) { handleSortChange(val: CommentSortType) {
this.setState({ sort: val, page: 1 }); this.setState({ sort: val, page: 1 });
@ -590,9 +570,11 @@ export class Inbox extends Component<any, InboxState> {
} }
markAllAsRead(i: Inbox) { markAllAsRead(i: Inbox) {
let auth = myAuth();
if (auth) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.markAllAsRead({ wsClient.markAllAsRead({
auth: auth().unwrap(), auth,
}) })
); );
i.setState({ replies: [], mentions: [], messages: [] }); i.setState({ replies: [], mentions: [], messages: [] });
@ -601,6 +583,7 @@ export class Inbox extends Component<any, InboxState> {
window.scrollTo(0, 0); window.scrollTo(0, 0);
i.setState(i.state); i.setState(i.state);
} }
}
sendUnreadCount(read: boolean) { sendUnreadCount(read: boolean) {
let urcs = UserService.Instance.unreadInboxCountSub; let urcs = UserService.Instance.unreadInboxCountSub;
@ -620,73 +603,62 @@ export class Inbox extends Component<any, InboxState> {
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.GetReplies) { } else if (op == UserOperation.GetReplies) {
let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse); let data = wsJsonToRes<GetRepliesResponse>(msg);
this.setState({ replies: data.replies }); this.setState({ replies: data.replies });
this.setState({ combined: this.buildCombined(), loading: false }); this.setState({ combined: this.buildCombined(), loading: false });
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPersonMentions) { } else if (op == UserOperation.GetPersonMentions) {
let data = wsJsonToRes<GetPersonMentionsResponse>( let data = wsJsonToRes<GetPersonMentionsResponse>(msg);
msg,
GetPersonMentionsResponse
);
this.setState({ mentions: data.mentions }); this.setState({ mentions: data.mentions });
this.setState({ combined: this.buildCombined() }); this.setState({ combined: this.buildCombined() });
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPrivateMessages) { } else if (op == UserOperation.GetPrivateMessages) {
let data = wsJsonToRes<PrivateMessagesResponse>( let data = wsJsonToRes<PrivateMessagesResponse>(msg);
msg,
PrivateMessagesResponse
);
this.setState({ messages: data.private_messages }); this.setState({ messages: data.private_messages });
this.setState({ combined: this.buildCombined() }); this.setState({ combined: this.buildCombined() });
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.EditPrivateMessage) { } else if (op == UserOperation.EditPrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg, let found = this.state.messages.find(
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
); );
if (found) { if (found) {
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.private_message_view.private_message.id i => i.id == data.private_message_view.private_message.id
).view as PrivateMessageView; )?.view as PrivateMessageView | undefined;
if (combinedView) {
found.private_message.content = combinedView.private_message.content = found.private_message.content = combinedView.private_message.content =
data.private_message_view.private_message.content; data.private_message_view.private_message.content;
found.private_message.updated = combinedView.private_message.updated = found.private_message.updated = combinedView.private_message.updated =
data.private_message_view.private_message.updated; data.private_message_view.private_message.updated;
} }
}
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.DeletePrivateMessage) { } else if (op == UserOperation.DeletePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg, let found = this.state.messages.find(
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
); );
if (found) { if (found) {
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.private_message_view.private_message.id i => i.id == data.private_message_view.private_message.id
).view as PrivateMessageView; )?.view as PrivateMessageView | undefined;
if (combinedView) {
found.private_message.deleted = combinedView.private_message.deleted = found.private_message.deleted = combinedView.private_message.deleted =
data.private_message_view.private_message.deleted; data.private_message_view.private_message.deleted;
found.private_message.updated = combinedView.private_message.updated = found.private_message.updated = combinedView.private_message.updated =
data.private_message_view.private_message.updated; data.private_message_view.private_message.updated;
} }
}
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkPrivateMessageAsRead) { } else if (op == UserOperation.MarkPrivateMessageAsRead) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg, let found = this.state.messages.find(
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
); );
@ -696,7 +668,8 @@ export class Inbox extends Component<any, InboxState> {
i => i =>
i.id == data.private_message_view.private_message.id && i.id == data.private_message_view.private_message.id &&
i.type_ == ReplyEnum.Message i.type_ == ReplyEnum.Message
).view as PrivateMessageView; )?.view as PrivateMessageView | undefined;
if (combinedView) {
found.private_message.updated = combinedView.private_message.updated = found.private_message.updated = combinedView.private_message.updated =
data.private_message_view.private_message.updated; data.private_message_view.private_message.updated;
@ -722,6 +695,7 @@ export class Inbox extends Component<any, InboxState> {
data.private_message_view.private_message.read; data.private_message_view.private_message.read;
} }
} }
}
this.sendUnreadCount(data.private_message_view.private_message.read); this.sendUnreadCount(data.private_message_view.private_message.read);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkAllAsRead) { } else if (op == UserOperation.MarkAllAsRead) {
@ -731,11 +705,11 @@ export class Inbox extends Component<any, InboxState> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes(data.comment_view, this.state.replies); editCommentRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkCommentReplyAsRead) { } else if (op == UserOperation.MarkCommentReplyAsRead) {
let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse); let data = wsJsonToRes<CommentReplyResponse>(msg);
let found = this.state.replies.find( let found = this.state.replies.find(
c => c.comment_reply.id == data.comment_reply_view.comment_reply.id c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
@ -746,7 +720,8 @@ export class Inbox extends Component<any, InboxState> {
i => i =>
i.id == data.comment_reply_view.comment_reply.id && i.id == data.comment_reply_view.comment_reply.id &&
i.type_ == ReplyEnum.Reply i.type_ == ReplyEnum.Reply
).view as CommentReplyView; )?.view as CommentReplyView | undefined;
if (combinedView) {
found.comment.content = combinedView.comment.content = found.comment.content = combinedView.comment.content =
data.comment_reply_view.comment.content; data.comment_reply_view.comment.content;
found.comment.updated = combinedView.comment.updated = found.comment.updated = combinedView.comment.updated =
@ -770,7 +745,8 @@ export class Inbox extends Component<any, InboxState> {
this.setState({ this.setState({
replies: this.state.replies.filter( replies: this.state.replies.filter(
r => r =>
r.comment_reply.id !== data.comment_reply_view.comment_reply.id r.comment_reply.id !==
data.comment_reply_view.comment_reply.id
), ),
}); });
this.setState({ this.setState({
@ -783,10 +759,11 @@ export class Inbox extends Component<any, InboxState> {
data.comment_reply_view.comment_reply.read; data.comment_reply_view.comment_reply.read;
} }
} }
}
this.sendUnreadCount(data.comment_reply_view.comment_reply.read); this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkPersonMentionAsRead) { } else if (op == UserOperation.MarkPersonMentionAsRead) {
let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse); let data = wsJsonToRes<PersonMentionResponse>(msg);
// TODO this might not be correct, it might need to use the comment id // TODO this might not be correct, it might need to use the comment id
let found = this.state.mentions.find( let found = this.state.mentions.find(
@ -798,7 +775,8 @@ export class Inbox extends Component<any, InboxState> {
i => i =>
i.id == data.person_mention_view.person_mention.id && i.id == data.person_mention_view.person_mention.id &&
i.type_ == ReplyEnum.Mention i.type_ == ReplyEnum.Mention
).view as PersonMentionView; )?.view as PersonMentionView | undefined;
if (combinedView) {
found.comment.content = combinedView.comment.content = found.comment.content = combinedView.comment.content =
data.person_mention_view.comment.content; data.person_mention_view.comment.content;
found.comment.updated = combinedView.comment.updated = found.comment.updated = combinedView.comment.updated =
@ -837,18 +815,14 @@ export class Inbox extends Component<any, InboxState> {
data.person_mention_view.person_mention.read; data.person_mention_view.person_mention.read;
} }
} }
}
this.sendUnreadCount(data.person_mention_view.person_mention.read); this.sendUnreadCount(data.person_mention_view.person_mention.read);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg, let mui = UserService.Instance.myUserInfo;
PrivateMessageResponse
);
UserService.Instance.myUserInfo.match({
some: mui => {
if ( if (
data.private_message_view.recipient.id == data.private_message_view.recipient.id == mui?.local_user_view.person.id
mui.local_user_view.person.id
) { ) {
this.state.messages.unshift(data.private_message_view); this.state.messages.unshift(data.private_message_view);
this.state.combined.unshift( this.state.combined.unshift(
@ -856,36 +830,30 @@ export class Inbox extends Component<any, InboxState> {
); );
this.setState(this.state); this.setState(this.state);
} }
},
none: void 0,
});
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(data.comment_view, this.state.replies); saveCommentRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(data.comment_view, this.state.replies); createCommentLikeRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse); let data = wsJsonToRes<CommentReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreatePrivateMessageReport) { } else if (op == UserOperation.CreatePrivateMessageReport) {
let data = wsJsonToRes<PrivateMessageReportResponse>( let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
msg,
PrivateMessageReportResponse
);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }

View file

@ -1,9 +1,8 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse, GetSiteResponse,
LoginResponse, LoginResponse,
PasswordChange as PasswordChangeForm, PasswordChange as PWordChange,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -23,21 +22,23 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
interface State { interface State {
passwordChangeForm: PasswordChangeForm; form: {
token: string;
password?: string;
password_verify?: string;
};
loading: boolean; loading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
} }
export class PasswordChange extends Component<any, State> { export class PasswordChange extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
emptyState: State = { state: State = {
passwordChangeForm: new PasswordChangeForm({ form: {
token: this.props.match.params.token, token: this.props.match.params.token,
password: undefined, },
password_verify: undefined,
}),
loading: false, loading: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
@ -45,15 +46,13 @@ export class PasswordChange extends Component<any, State> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
} }
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -69,8 +68,6 @@ export class PasswordChange extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
@ -93,7 +90,7 @@ export class PasswordChange extends Component<any, State> {
<input <input
id="new-password" id="new-password"
type="password" type="password"
value={this.state.passwordChangeForm.password} value={this.state.form.password}
onInput={linkEvent(this, this.handlePasswordChange)} onInput={linkEvent(this, this.handlePasswordChange)}
className="form-control" className="form-control"
required required
@ -109,7 +106,7 @@ export class PasswordChange extends Component<any, State> {
<input <input
id="verify-password" id="verify-password"
type="password" type="password"
value={this.state.passwordChangeForm.password_verify} value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)} onInput={linkEvent(this, this.handleVerifyPasswordChange)}
className="form-control" className="form-control"
required required
@ -133,12 +130,12 @@ export class PasswordChange extends Component<any, State> {
} }
handlePasswordChange(i: PasswordChange, event: any) { handlePasswordChange(i: PasswordChange, event: any) {
i.state.passwordChangeForm.password = event.target.value; i.state.form.password = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleVerifyPasswordChange(i: PasswordChange, event: any) { handleVerifyPasswordChange(i: PasswordChange, event: any) {
i.state.passwordChangeForm.password_verify = event.target.value; i.state.form.password_verify = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
@ -146,9 +143,18 @@ export class PasswordChange extends Component<any, State> {
event.preventDefault(); event.preventDefault();
i.setState({ loading: true }); i.setState({ loading: true });
WebSocketService.Instance.send( let password = i.state.form.password;
wsClient.passwordChange(i.state.passwordChangeForm) let password_verify = i.state.form.password_verify;
);
if (password && password_verify) {
let form: PWordChange = {
token: i.state.form.token,
password,
password_verify,
};
WebSocketService.Instance.send(wsClient.passwordChange(form));
}
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -159,8 +165,7 @@ export class PasswordChange extends Component<any, State> {
this.setState({ loading: false }); this.setState({ loading: false });
return; return;
} else if (op == UserOperation.PasswordChange) { } else if (op == UserOperation.PasswordChange) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
this.setState(this.emptyState);
UserService.Instance.login(data); UserService.Instance.login(data);
this.props.history.push("/"); this.props.history.push("/");
location.reload(); location.reload();

View file

@ -1,4 +1,3 @@
import { None, Some } from "@sniptt/monads/build";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
CommentView, CommentView,
@ -94,9 +93,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
key={i.id} key={i.id}
nodes={[{ comment_view: c, children: [], depth: 0 }]} nodes={[{ comment_view: c, children: [], depth: 0 }]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
admins={Some(this.props.admins)} admins={this.props.admins}
moderators={None}
maxCommentsShown={None}
noBorder noBorder
noIndent noIndent
showCommunity showCommunity
@ -113,9 +110,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<PostListing <PostListing
key={i.id} key={i.id}
post_view={p} post_view={p}
admins={Some(this.props.admins)} admins={this.props.admins}
duplicates={None}
moderators={None}
showCommunity showCommunity
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -171,9 +166,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.props.personRes.comments)} nodes={commentsToFlatNodes(this.props.personRes.comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
admins={Some(this.props.admins)} admins={this.props.admins}
moderators={None}
maxCommentsShown={None}
noIndent noIndent
showCommunity showCommunity
showContext showContext
@ -192,10 +185,8 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<> <>
<PostListing <PostListing
post_view={post} post_view={post}
admins={Some(this.props.admins)} admins={this.props.admins}
showCommunity showCommunity
duplicates={None}
moderators={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}

View file

@ -37,9 +37,9 @@ export class PersonListing extends Component<PersonListingProps, any> {
let displayName = this.props.useApubName let displayName = this.props.useApubName
? apubName ? apubName
: person.display_name.unwrapOr(apubName); : person.display_name ?? apubName;
if (this.props.showApubName && !local && person.display_name.isSome()) { if (this.props.showApubName && !local && person.display_name) {
displayName = `${displayName} (${apubName})`; displayName = `${displayName} (${apubName})`;
} }
@ -70,14 +70,12 @@ export class PersonListing extends Component<PersonListingProps, any> {
} }
avatarAndName(displayName: string) { avatarAndName(displayName: string) {
let avatar = this.props.person.avatar;
return ( return (
<> <>
{this.props.person.avatar.match({ {avatar && !this.props.hideAvatar && showAvatars() && (
some: avatar => <PictrsImage src={avatar} icon />
!this.props.hideAvatar && )}
showAvatars() && <PictrsImage src={avatar} icon />,
none: <></>,
})}
<span>{displayName}</span> <span>{displayName}</span>
</> </>
); );

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -14,7 +13,6 @@ import {
PostResponse, PostResponse,
PurgeItemResponse, PurgeItemResponse,
SortType, SortType,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -25,7 +23,6 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest, PersonDetailsView } from "../../interfaces"; import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
canMod, canMod,
capitalizeFirstLetter, capitalizeFirstLetter,
createCommentLikeRes, createCommentLikeRes,
@ -40,6 +37,7 @@ import {
isAdmin, isAdmin,
isBanned, isBanned,
mdToHtml, mdToHtml,
myAuth,
numToSI, numToSI,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
@ -63,15 +61,15 @@ import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing"; import { PersonListing } from "./person-listing";
interface ProfileState { interface ProfileState {
personRes: Option<GetPersonDetailsResponse>; personRes?: GetPersonDetailsResponse;
userName: string; userName: string;
view: PersonDetailsView; view: PersonDetailsView;
sort: SortType; sort: SortType;
page: number; page: number;
loading: boolean; loading: boolean;
personBlocked: boolean; personBlocked: boolean;
banReason: Option<string>; banReason?: string;
banExpireDays: Option<number>; banExpireDays?: number;
showBanDialog: boolean; showBanDialog: boolean;
removeData: boolean; removeData: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -81,7 +79,7 @@ interface ProfileProps {
view: PersonDetailsView; view: PersonDetailsView;
sort: SortType; sort: SortType;
page: number; page: number;
person_id: number | null; person_id?: number;
username: string; username: string;
} }
@ -92,10 +90,9 @@ interface UrlParams {
} }
export class Profile extends Component<any, ProfileState> { export class Profile extends Component<any, ProfileState> {
private isoData = setIsoData(this.context, GetPersonDetailsResponse); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private emptyState: ProfileState = { state: ProfileState = {
personRes: None,
userName: getUsernameFromProps(this.props), userName: getUsernameFromProps(this.props),
loading: true, loading: true,
view: Profile.getViewFromProps(this.props.match.view), view: Profile.getViewFromProps(this.props.match.view),
@ -104,15 +101,12 @@ export class Profile extends Component<any, ProfileState> {
personBlocked: false, personBlocked: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showBanDialog: false, showBanDialog: false,
banReason: null,
banExpireDays: null,
removeData: false, removeData: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
@ -123,7 +117,7 @@ export class Profile extends Component<any, ProfileState> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
personRes: Some(this.isoData.routeData[0] as GetPersonDetailsResponse), personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
loading: false, loading: false,
}; };
} else { } else {
@ -132,46 +126,35 @@ export class Profile extends Component<any, ProfileState> {
} }
fetchUserData() { fetchUserData() {
let form = new GetPersonDetails({ let form: GetPersonDetails = {
username: Some(this.state.userName), username: this.state.userName,
person_id: None, sort: this.state.sort,
community_id: None, saved_only: this.state.view === PersonDetailsView.Saved,
sort: Some(this.state.sort), page: this.state.page,
saved_only: Some(this.state.view === PersonDetailsView.Saved), limit: fetchLimit,
page: Some(this.state.page), auth: myAuth(false),
limit: Some(fetchLimit), };
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getPersonDetails(form)); WebSocketService.Instance.send(wsClient.getPersonDetails(form));
} }
get amCurrentUser() { get amCurrentUser() {
return UserService.Instance.myUserInfo.match({ return (
some: mui => UserService.Instance.myUserInfo?.local_user_view.person.id ==
this.state.personRes.match({ this.state.personRes?.person_view.person.id
some: res => );
mui.local_user_view.person.id == res.person_view.person.id,
none: false,
}),
none: false,
});
} }
setPersonBlock() { setPersonBlock() {
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => let res = this.state.personRes;
this.state.personRes.match({ if (mui && res) {
some: res =>
this.setState({ this.setState({
personBlocked: mui.person_blocks personBlocked: mui.person_blocks
.map(a => a.target.id) .map(a => a.target.id)
.includes(res.person_view.person.id), .includes(res.person_view.person.id),
}),
none: void 0,
}),
none: void 0,
}); });
} }
}
static getViewFromProps(view: string): PersonDetailsView { static getViewFromProps(view: string): PersonDetailsView {
return view ? PersonDetailsView[view] : PersonDetailsView.Overview; return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
@ -190,19 +173,17 @@ export class Profile extends Component<any, ProfileState> {
let username = pathSplit[2]; let username = pathSplit[2];
let view = this.getViewFromProps(pathSplit[4]); let view = this.getViewFromProps(pathSplit[4]);
let sort = Some(this.getSortTypeFromProps(pathSplit[6])); let sort = this.getSortTypeFromProps(pathSplit[6]);
let page = Some(this.getPageFromProps(Number(pathSplit[8]))); let page = this.getPageFromProps(Number(pathSplit[8]));
let form = new GetPersonDetails({ let form: GetPersonDetails = {
username: Some(username), username: username,
person_id: None,
community_id: None,
sort, sort,
saved_only: Some(view === PersonDetailsView.Saved), saved_only: view === PersonDetailsView.Saved,
page, page,
limit: Some(fetchLimit), limit: fetchLimit,
auth: req.auth, auth: req.auth,
}); };
return [req.client.getPersonDetails(form)]; return [req.client.getPersonDetails(form)];
} }
@ -212,7 +193,7 @@ export class Profile extends Component<any, ProfileState> {
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
saveScrollPosition(this.context); saveScrollPosition(this.context);
} }
@ -221,7 +202,7 @@ export class Profile extends Component<any, ProfileState> {
view: this.getViewFromProps(props.match.params.view), view: this.getViewFromProps(props.match.params.view),
sort: this.getSortTypeFromProps(props.match.params.sort), sort: this.getSortTypeFromProps(props.match.params.sort),
page: this.getPageFromProps(props.match.params.page), page: this.getPageFromProps(props.match.params.page),
person_id: Number(props.match.params.id) || null, person_id: Number(props.match.params.id),
username: props.match.params.username, username: props.match.params.username,
}; };
} }
@ -238,14 +219,14 @@ export class Profile extends Component<any, ProfileState> {
} }
get documentTitle(): string { get documentTitle(): string {
return this.state.personRes.match({ let res = this.state.personRes;
some: res => return res
`@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`, ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
none: "", : "";
});
} }
render() { render() {
let res = this.state.personRes;
return ( return (
<div className="container-lg"> <div className="container-lg">
{this.state.loading ? ( {this.state.loading ? (
@ -253,8 +234,7 @@ export class Profile extends Component<any, ProfileState> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
this.state.personRes.match({ res && (
some: res => (
<div className="row"> <div className="row">
<div className="col-12 col-md-8"> <div className="col-12 col-md-8">
<> <>
@ -290,9 +270,7 @@ export class Profile extends Component<any, ProfileState> {
</div> </div>
)} )}
</div> </div>
), )
none: <></>,
})
)} )}
</div> </div>
); );
@ -377,44 +355,43 @@ export class Profile extends Component<any, ProfileState> {
); );
} }
handleBlockPerson(personId: number) { handleBlockPerson(personId: number) {
let auth = myAuth();
if (auth) {
if (personId != 0) { if (personId != 0) {
let blockUserForm = new BlockPerson({ let blockUserForm: BlockPerson = {
person_id: personId, person_id: personId,
block: true, block: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
} }
}
handleUnblockPerson(recipientId: number) { handleUnblockPerson(recipientId: number) {
let blockUserForm = new BlockPerson({ let auth = myAuth();
if (auth) {
let blockUserForm: BlockPerson = {
person_id: recipientId, person_id: recipientId,
block: false, block: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
}
userInfo() { userInfo() {
return this.state.personRes let pv = this.state.personRes?.person_view;
.map(r => r.person_view) return (
.match({ pv && (
some: pv => (
<div> <div>
<BannerIconHeader <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
banner={pv.person.banner}
icon={pv.person.avatar}
/>
<div className="mb-3"> <div className="mb-3">
<div className=""> <div className="">
<div className="mb-0 d-flex flex-wrap"> <div className="mb-0 d-flex flex-wrap">
<div> <div>
{pv.person.display_name.match({ {pv.person.display_name && (
some: displayName => ( <h5 className="mb-0">{pv.person.display_name}</h5>
<h5 className="mb-0">{displayName}</h5> )}
),
none: <></>,
})}
<ul className="list-inline mb-2"> <ul className="list-inline mb-2">
<li className="list-inline-item"> <li className="list-inline-item">
<PersonListing <PersonListing
@ -449,8 +426,7 @@ export class Profile extends Component<any, ProfileState> {
</div> </div>
{this.banDialog()} {this.banDialog()}
<div className="flex-grow-1 unselectable pointer mx-2"></div> <div className="flex-grow-1 unselectable pointer mx-2"></div>
{!this.amCurrentUser && {!this.amCurrentUser && UserService.Instance.myUserInfo && (
UserService.Instance.myUserInfo.isSome() && (
<> <>
<a <a
className={`d-flex align-self-start btn btn-secondary mr-2 ${ className={`d-flex align-self-start btn btn-secondary mr-2 ${
@ -497,12 +473,8 @@ export class Profile extends Component<any, ProfileState> {
</> </>
)} )}
{canMod( {canMod(pv.person.id, undefined, this.state.siteRes.admins) &&
None, !isAdmin(pv.person.id, this.state.siteRes.admins) &&
Some(this.state.siteRes.admins),
pv.person.id
) &&
!isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
!this.state.showBanDialog && !this.state.showBanDialog &&
(!isBanned(pv.person) ? ( (!isBanned(pv.person) ? (
<button <button
@ -526,17 +498,14 @@ export class Profile extends Component<any, ProfileState> {
</button> </button>
))} ))}
</div> </div>
{pv.person.bio.match({ {pv.person.bio && (
some: bio => (
<div className="d-flex align-items-center mb-2"> <div className="d-flex align-items-center mb-2">
<div <div
className="md-div" className="md-div"
dangerouslySetInnerHTML={mdToHtml(bio)} dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
/> />
</div> </div>
), )}
none: <></>,
})}
<div> <div>
<ul className="list-inline mb-2"> <ul className="list-inline mb-2">
<li className="list-inline-item badge badge-light"> <li className="list-inline-item badge badge-light">
@ -557,7 +526,6 @@ export class Profile extends Component<any, ProfileState> {
{i18n.t("joined")}{" "} {i18n.t("joined")}{" "}
<MomentTime <MomentTime
published={pv.person.published} published={pv.person.published}
updated={None}
showAgo showAgo
ignoreUpdated ignoreUpdated
/> />
@ -575,24 +543,19 @@ export class Profile extends Component<any, ProfileState> {
</div> </div>
</div> </div>
</div> </div>
), )
none: <></>, );
});
} }
banDialog() { banDialog() {
return this.state.personRes let pv = this.state.personRes?.person_view;
.map(r => r.person_view) return (
.match({ pv && (
some: pv => (
<> <>
{this.state.showBanDialog && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<div className="form-group row col-12"> <div className="form-group row col-12">
<label <label className="col-form-label" htmlFor="profile-ban-reason">
className="col-form-label"
htmlFor="profile-ban-reason"
>
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
@ -600,7 +563,7 @@ export class Profile extends Component<any, ProfileState> {
id="profile-ban-reason" id="profile-ban-reason"
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.banReason)} value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)} onInput={linkEvent(this, this.handleModBanReasonChange)}
/> />
<label className="col-form-label" htmlFor={`mod-ban-expires`}> <label className="col-form-label" htmlFor={`mod-ban-expires`}>
@ -611,7 +574,7 @@ export class Profile extends Component<any, ProfileState> {
id={`mod-ban-expires`} id={`mod-ban-expires`}
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("number_of_days")} placeholder={i18n.t("number_of_days")}
value={toUndefined(this.state.banExpireDays)} value={this.state.banExpireDays}
onInput={linkEvent(this, this.handleModBanExpireDaysChange)} onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
/> />
<div className="form-group"> <div className="form-group">
@ -621,10 +584,7 @@ export class Profile extends Component<any, ProfileState> {
id="mod-ban-remove-data" id="mod-ban-remove-data"
type="checkbox" type="checkbox"
checked={this.state.removeData} checked={this.state.removeData}
onChange={linkEvent( onChange={linkEvent(this, this.handleModRemoveDataChange)}
this,
this.handleModRemoveDataChange
)}
/> />
<label <label
className="form-check-label" className="form-check-label"
@ -661,18 +621,15 @@ export class Profile extends Component<any, ProfileState> {
</form> </form>
)} )}
</> </>
), )
none: <></>, );
});
} }
moderates() { moderates() {
return this.state.personRes let moderates = this.state.personRes?.moderates;
.map(r => r.moderates)
.match({
some: moderates => {
if (moderates.length > 0) {
return ( return (
moderates &&
moderates.length > 0 && (
<div className="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div className="card-body"> <div className="card-body">
<h5>{i18n.t("moderates")}</h5> <h5>{i18n.t("moderates")}</h5>
@ -685,22 +642,15 @@ export class Profile extends Component<any, ProfileState> {
</ul> </ul>
</div> </div>
</div> </div>
)
); );
} else {
return <></>;
}
},
none: void 0,
});
} }
follows() { follows() {
return UserService.Instance.myUserInfo let follows = UserService.Instance.myUserInfo?.follows;
.map(m => m.follows)
.match({
some: follows => {
if (follows.length > 0) {
return ( return (
follows &&
follows.length > 0 && (
<div className="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div className="card-body"> <div className="card-body">
<h5>{i18n.t("subscribed")}</h5> <h5>{i18n.t("subscribed")}</h5>
@ -713,13 +663,8 @@ export class Profile extends Component<any, ProfileState> {
</ul> </ul>
</div> </div>
</div> </div>
)
); );
} else {
return <></>;
}
},
none: void 0,
});
} }
updateUrl(paramUpdates: UrlParams) { updateUrl(paramUpdates: UrlParams) {
@ -774,30 +719,26 @@ export class Profile extends Component<any, ProfileState> {
handleModBanSubmit(i: Profile, event?: any) { handleModBanSubmit(i: Profile, event?: any) {
if (event) event.preventDefault(); if (event) event.preventDefault();
let person = i.state.personRes?.person_view.person;
i.state.personRes let auth = myAuth();
.map(r => r.person_view.person) if (person && auth) {
.match({
some: person => {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !person.banned; let ban = !person.banned;
if (ban == false) { if (ban == false) {
i.setState({ removeData: false }); i.setState({ removeData: false });
} }
let form = new BanPerson({ let form: BanPerson = {
person_id: person.id, person_id: person.id,
ban, ban,
remove_data: Some(i.state.removeData), remove_data: i.state.removeData,
reason: i.state.banReason, reason: i.state.banReason,
expires: i.state.banExpireDays.map(futureDaysToUnixTime), expires: futureDaysToUnixTime(i.state.banExpireDays),
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.banPerson(form)); WebSocketService.Instance.send(wsClient.banPerson(form));
i.setState({ showBanDialog: false }); i.setState({ showBanDialog: false });
}, }
none: void 0,
});
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -815,50 +756,34 @@ export class Profile extends Component<any, ProfileState> {
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs // and set the parent state if it is not set or differs
// TODO this might need to get abstracted // TODO this might need to get abstracted
let data = wsJsonToRes<GetPersonDetailsResponse>( let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
msg, this.setState({ personRes: data, loading: false });
GetPersonDetailsResponse
);
this.setState({ personRes: Some(data), loading: false });
this.setPersonBlock(); this.setPersonBlock();
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = data.admins), s)); this.setState(s => ((s.siteRes.admins = data.admins), s));
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes( createCommentLikeRes(data.comment_view, this.state.personRes?.comments);
data.comment_view,
this.state.personRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditComment || op == UserOperation.EditComment ||
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes( editCommentRes(data.comment_view, this.state.personRes?.comments);
data.comment_view,
this.state.personRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => { if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
if (data.comment_view.creator.id == mui.local_user_view.person.id) {
toast(i18n.t("reply_sent")); toast(i18n.t("reply_sent"));
} }
},
none: void 0,
});
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes( saveCommentRes(data.comment_view, this.state.personRes?.comments);
data.comment_view,
this.state.personRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditPost || op == UserOperation.EditPost ||
@ -868,40 +793,30 @@ export class Profile extends Component<any, ProfileState> {
op == UserOperation.FeaturePost || op == UserOperation.FeaturePost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
editPostFindRes( editPostFindRes(data.post_view, this.state.personRes?.posts);
data.post_view,
this.state.personRes.map(r => r.posts).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes( createPostLikeFindRes(data.post_view, this.state.personRes?.posts);
data.post_view,
this.state.personRes.map(r => r.posts).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse); let data = wsJsonToRes<BanPersonResponse>(msg);
this.state.personRes.match({ let res = this.state.personRes;
some: res => { res?.comments
res.comments
.filter(c => c.creator.id == data.person_view.person.id) .filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned)); .forEach(c => (c.creator.banned = data.banned));
res.posts res?.posts
.filter(c => c.creator.id == data.person_view.person.id) .filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned)); .forEach(c => (c.creator.banned = data.banned));
let pv = res.person_view; let pv = res?.person_view;
if (pv.person.id == data.person_view.person.id) { if (pv?.person.id == data.person_view.person.id) {
pv.person.banned = data.banned; pv.person.banned = data.banned;
} }
this.setState(this.state); this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
this.setPersonBlock(); this.setPersonBlock();
this.setState(this.state); this.setState(this.state);
@ -911,7 +826,7 @@ export class Profile extends Component<any, ProfileState> {
op == UserOperation.PurgeComment || op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity op == UserOperation.PurgeCommunity
) { ) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse); let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) { if (data.success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse, GetSiteResponse,
@ -14,9 +13,9 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
myAuth,
setIsoData, setIsoData,
setupTippy, setupTippy,
toast, toast,
@ -35,7 +34,7 @@ enum UnreadOrAll {
} }
interface RegistrationApplicationsState { interface RegistrationApplicationsState {
listRegistrationApplicationsResponse: Option<ListRegistrationApplicationsResponse>; listRegistrationApplicationsResponse?: ListRegistrationApplicationsResponse;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
page: number; page: number;
@ -46,13 +45,9 @@ export class RegistrationApplications extends Component<
any, any,
RegistrationApplicationsState RegistrationApplicationsState
> { > {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
ListRegistrationApplicationsResponse state: RegistrationApplicationsState = {
);
private subscription: Subscription;
private emptyState: RegistrationApplicationsState = {
listRegistrationApplicationsResponse: None,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
page: 1, page: 1,
@ -62,10 +57,9 @@ export class RegistrationApplications extends Component<
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -77,9 +71,8 @@ export class RegistrationApplications extends Component<
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
listRegistrationApplicationsResponse: Some( listRegistrationApplicationsResponse: this.isoData
this.isoData.routeData[0] as ListRegistrationApplicationsResponse .routeData[0] as ListRegistrationApplicationsResponse,
),
loading: false, loading: false,
}; };
} else { } else {
@ -93,18 +86,17 @@ export class RegistrationApplications extends Component<
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
get documentTitle(): string { get documentTitle(): string {
return UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => return mui
`@${mui.local_user_view.person.name} ${i18n.t( ? `@${mui.local_user_view.person.name} ${i18n.t(
"registration_applications" "registration_applications"
)} - ${this.state.siteRes.site_view.site.name}`, )} - ${this.state.siteRes.site_view.site.name}`
none: "", : "";
});
} }
render() { render() {
@ -120,8 +112,6 @@ export class RegistrationApplications extends Component<
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 className="mb-2">{i18n.t("registration_applications")}</h5> <h5 className="mb-2">{i18n.t("registration_applications")}</h5>
{this.selects()} {this.selects()}
@ -179,8 +169,9 @@ export class RegistrationApplications extends Component<
} }
applicationList() { applicationList() {
return this.state.listRegistrationApplicationsResponse.match({ let res = this.state.listRegistrationApplicationsResponse;
some: res => ( return (
res && (
<div> <div>
{res.registration_applications.map(ra => ( {res.registration_applications.map(ra => (
<> <>
@ -192,9 +183,8 @@ export class RegistrationApplications extends Component<
</> </>
))} ))}
</div> </div>
), )
none: <></>, );
});
} }
handleUnreadOrAllChange(i: RegistrationApplications, event: any) { handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
@ -210,26 +200,34 @@ export class RegistrationApplications extends Component<
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let form = new ListRegistrationApplications({ let auth = req.auth;
unread_only: Some(true), if (auth) {
page: Some(1), let form: ListRegistrationApplications = {
limit: Some(fetchLimit), unread_only: true,
auth: req.auth.unwrap(), page: 1,
}); limit: fetchLimit,
auth,
};
promises.push(req.client.listRegistrationApplications(form)); promises.push(req.client.listRegistrationApplications(form));
}
return promises; return promises;
} }
refetch() { refetch() {
let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread; let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
let form = new ListRegistrationApplications({ let auth = myAuth();
unread_only: Some(unread_only), if (auth) {
page: Some(this.state.page), let form: ListRegistrationApplications = {
limit: Some(fetchLimit), unread_only: unread_only,
auth: auth().unwrap(), page: this.state.page,
}); limit: fetchLimit,
WebSocketService.Instance.send(wsClient.listRegistrationApplications(form)); auth,
};
WebSocketService.Instance.send(
wsClient.listRegistrationApplications(form)
);
}
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -241,25 +239,18 @@ export class RegistrationApplications extends Component<
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.ListRegistrationApplications) { } else if (op == UserOperation.ListRegistrationApplications) {
let data = wsJsonToRes<ListRegistrationApplicationsResponse>( let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg);
msg,
ListRegistrationApplicationsResponse
);
this.setState({ this.setState({
listRegistrationApplicationsResponse: Some(data), listRegistrationApplicationsResponse: data,
loading: false, loading: false,
}); });
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (op == UserOperation.ApproveRegistrationApplication) { } else if (op == UserOperation.ApproveRegistrationApplication) {
let data = wsJsonToRes<RegistrationApplicationResponse>( let data = wsJsonToRes<RegistrationApplicationResponse>(msg);
msg,
RegistrationApplicationResponse
);
updateRegistrationApplicationRes( updateRegistrationApplicationRes(
data.registration_application, data.registration_application,
this.state.listRegistrationApplicationsResponse this.state.listRegistrationApplicationsResponse
.map(r => r.registration_applications) ?.registration_applications
.unwrapOr([])
); );
let uacs = UserService.Instance.unreadApplicationCountSub; let uacs = UserService.Instance.unreadApplicationCountSub;
// Minor bug, where if the application switches from deny to approve, the count will still go down // Minor bug, where if the application switches from deny to approve, the count will still go down

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommentReportResponse, CommentReportResponse,
@ -24,9 +23,9 @@ import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amAdmin, amAdmin,
auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
myAuth,
setIsoData, setIsoData,
setupTippy, setupTippy,
toast, toast,
@ -69,9 +68,9 @@ type ItemType = {
}; };
interface ReportsState { interface ReportsState {
listCommentReportsResponse: Option<ListCommentReportsResponse>; listCommentReportsResponse?: ListCommentReportsResponse;
listPostReportsResponse: Option<ListPostReportsResponse>; listPostReportsResponse?: ListPostReportsResponse;
listPrivateMessageReportsResponse: Option<ListPrivateMessageReportsResponse>; listPrivateMessageReportsResponse?: ListPrivateMessageReportsResponse;
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
messageType: MessageType; messageType: MessageType;
combined: ItemType[]; combined: ItemType[];
@ -81,17 +80,9 @@ interface ReportsState {
} }
export class Reports extends Component<any, ReportsState> { export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context, private subscription?: Subscription;
ListCommentReportsResponse, state: ReportsState = {
ListPostReportsResponse,
ListPrivateMessageReportsResponse
);
private subscription: Subscription;
private emptyState: ReportsState = {
listCommentReportsResponse: None,
listPostReportsResponse: None,
listPrivateMessageReportsResponse: None,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
combined: [], combined: [],
@ -103,10 +94,9 @@ export class Reports extends Component<any, ReportsState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -118,19 +108,16 @@ export class Reports extends Component<any, ReportsState> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
listCommentReportsResponse: Some( listCommentReportsResponse: this.isoData
this.isoData.routeData[0] as ListCommentReportsResponse .routeData[0] as ListCommentReportsResponse,
), listPostReportsResponse: this.isoData
listPostReportsResponse: Some( .routeData[1] as ListPostReportsResponse,
this.isoData.routeData[1] as ListPostReportsResponse
),
}; };
if (amAdmin()) { if (amAdmin()) {
this.state = { this.state = {
...this.state, ...this.state,
listPrivateMessageReportsResponse: Some( listPrivateMessageReportsResponse: this.isoData
this.isoData.routeData[2] as ListPrivateMessageReportsResponse .routeData[2] as ListPrivateMessageReportsResponse,
),
}; };
} }
this.state = { this.state = {
@ -145,18 +132,17 @@ export class Reports extends Component<any, ReportsState> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
get documentTitle(): string { get documentTitle(): string {
return UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => return mui
`@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${ ? `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
this.state.siteRes.site_view.site.name this.state.siteRes.site_view.site.name
}`, }`
none: "", : "";
});
} }
render() { render() {
@ -172,8 +158,6 @@ export class Reports extends Component<any, ReportsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 className="mb-2">{i18n.t("reports")}</h5> <h5 className="mb-2">{i18n.t("reports")}</h5>
{this.selects()} {this.selects()}
@ -331,19 +315,22 @@ export class Reports extends Component<any, ReportsState> {
} }
buildCombined(): ItemType[] { buildCombined(): ItemType[] {
let comments: ItemType[] = this.state.listCommentReportsResponse // let comments: ItemType[] = this.state.listCommentReportsResponse
.map(r => r.comment_reports) // .map(r => r.comment_reports)
.unwrapOr([]) // .unwrapOr([])
.map(r => this.commentReportToItemType(r)); // .map(r => this.commentReportToItemType(r));
let posts: ItemType[] = this.state.listPostReportsResponse let comments =
.map(r => r.post_reports) this.state.listCommentReportsResponse?.comment_reports.map(
.unwrapOr([]) this.commentReportToItemType
.map(r => this.postReportToItemType(r)); ) ?? [];
let privateMessages: ItemType[] = let posts =
this.state.listPrivateMessageReportsResponse this.state.listPostReportsResponse?.post_reports.map(
.map(r => r.private_message_reports) this.postReportToItemType
.unwrapOr([]) ) ?? [];
.map(r => this.privateMessageReportToItemType(r)); let privateMessages =
this.state.listPrivateMessageReportsResponse?.private_message_reports.map(
this.privateMessageReportToItemType
) ?? [];
return [...comments, ...posts, ...privateMessages].sort((a, b) => return [...comments, ...posts, ...privateMessages].sort((a, b) =>
b.published.localeCompare(a.published) b.published.localeCompare(a.published)
@ -384,42 +371,44 @@ export class Reports extends Component<any, ReportsState> {
} }
commentReports() { commentReports() {
return this.state.listCommentReportsResponse.match({ let reports = this.state.listCommentReportsResponse?.comment_reports;
some: res => ( return (
reports && (
<div> <div>
{res.comment_reports.map(cr => ( {reports.map(cr => (
<> <>
<hr /> <hr />
<CommentReport key={cr.comment_report.id} report={cr} /> <CommentReport key={cr.comment_report.id} report={cr} />
</> </>
))} ))}
</div> </div>
), )
none: <></>, );
});
} }
postReports() { postReports() {
return this.state.listPostReportsResponse.match({ let reports = this.state.listPostReportsResponse?.post_reports;
some: res => ( return (
reports && (
<div> <div>
{res.post_reports.map(pr => ( {reports.map(pr => (
<> <>
<hr /> <hr />
<PostReport key={pr.post_report.id} report={pr} /> <PostReport key={pr.post_report.id} report={pr} />
</> </>
))} ))}
</div> </div>
), )
none: <></>, );
});
} }
privateMessageReports() { privateMessageReports() {
return this.state.listPrivateMessageReportsResponse.match({ let reports =
some: res => ( this.state.listPrivateMessageReportsResponse?.private_message_reports;
return (
reports && (
<div> <div>
{res.private_message_reports.map(pmr => ( {reports.map(pmr => (
<> <>
<hr /> <hr />
<PrivateMessageReport <PrivateMessageReport
@ -429,9 +418,8 @@ export class Reports extends Component<any, ReportsState> {
</> </>
))} ))}
</div> </div>
), )
none: <></>, );
});
} }
handlePageChange(page: number) { handlePageChange(page: number) {
@ -452,87 +440,81 @@ export class Reports extends Component<any, ReportsState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let unresolved_only = Some(true); let unresolved_only = true;
let page = Some(1); let page = 1;
let limit = Some(fetchLimit); let limit = fetchLimit;
let community_id = None; let auth = req.auth;
let auth = req.auth.unwrap();
let commentReportsForm = new ListCommentReports({ if (auth) {
// TODO community_id let commentReportsForm: ListCommentReports = {
unresolved_only, unresolved_only,
community_id,
page, page,
limit, limit,
auth, auth,
}); };
promises.push(req.client.listCommentReports(commentReportsForm)); promises.push(req.client.listCommentReports(commentReportsForm));
let postReportsForm = new ListPostReports({ let postReportsForm: ListPostReports = {
// TODO community_id
unresolved_only, unresolved_only,
community_id,
page, page,
limit, limit,
auth, auth,
}); };
promises.push(req.client.listPostReports(postReportsForm)); promises.push(req.client.listPostReports(postReportsForm));
if (amAdmin()) { if (amAdmin()) {
let privateMessageReportsForm = new ListPrivateMessageReports({ let privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth, auth,
}); };
promises.push( promises.push(
req.client.listPrivateMessageReports(privateMessageReportsForm) req.client.listPrivateMessageReports(privateMessageReportsForm)
); );
} }
}
return promises; return promises;
} }
refetch() { refetch() {
let unresolved_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread); let unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
let community_id = None; let page = this.state.page;
let page = Some(this.state.page); let limit = fetchLimit;
let limit = Some(fetchLimit); let auth = myAuth();
if (auth) {
let commentReportsForm = new ListCommentReports({ let commentReportsForm: ListCommentReports = {
unresolved_only, unresolved_only,
// TODO community_id
community_id,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommentReports(commentReportsForm) wsClient.listCommentReports(commentReportsForm)
); );
let postReportsForm = new ListPostReports({ let postReportsForm: ListPostReports = {
unresolved_only, unresolved_only,
// TODO community_id
community_id,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm)); WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
if (amAdmin()) { if (amAdmin()) {
let privateMessageReportsForm = new ListPrivateMessageReports({ let privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listPrivateMessageReports(privateMessageReportsForm) wsClient.listPrivateMessageReports(privateMessageReportsForm)
); );
} }
} }
}
parseMessage(msg: any) { parseMessage(msg: any) {
let op = wsUserOp(msg); let op = wsUserOp(msg);
@ -543,40 +525,31 @@ export class Reports extends Component<any, ReportsState> {
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.ListCommentReports) { } else if (op == UserOperation.ListCommentReports) {
let data = wsJsonToRes<ListCommentReportsResponse>( let data = wsJsonToRes<ListCommentReportsResponse>(msg);
msg, this.setState({ listCommentReportsResponse: data });
ListCommentReportsResponse
);
this.setState({ listCommentReportsResponse: Some(data) });
this.setState({ combined: this.buildCombined(), loading: false }); this.setState({ combined: this.buildCombined(), loading: false });
// this.sendUnreadCount(); // this.sendUnreadCount();
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.ListPostReports) { } else if (op == UserOperation.ListPostReports) {
let data = wsJsonToRes<ListPostReportsResponse>( let data = wsJsonToRes<ListPostReportsResponse>(msg);
msg, this.setState({ listPostReportsResponse: data });
ListPostReportsResponse
);
this.setState({ listPostReportsResponse: Some(data) });
this.setState({ combined: this.buildCombined(), loading: false }); this.setState({ combined: this.buildCombined(), loading: false });
// this.sendUnreadCount(); // this.sendUnreadCount();
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.ListPrivateMessageReports) { } else if (op == UserOperation.ListPrivateMessageReports) {
let data = wsJsonToRes<ListPrivateMessageReportsResponse>( let data = wsJsonToRes<ListPrivateMessageReportsResponse>(msg);
msg, this.setState({ listPrivateMessageReportsResponse: data });
ListPrivateMessageReportsResponse
);
this.setState({ listPrivateMessageReportsResponse: Some(data) });
this.setState({ combined: this.buildCombined(), loading: false }); this.setState({ combined: this.buildCombined(), loading: false });
// this.sendUnreadCount(); // this.sendUnreadCount();
window.scrollTo(0, 0); window.scrollTo(0, 0);
setupTippy(); setupTippy();
} else if (op == UserOperation.ResolvePostReport) { } else if (op == UserOperation.ResolvePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg);
updatePostReportRes( updatePostReportRes(
data.post_report_view, data.post_report_view,
this.state.listPostReportsResponse.map(r => r.post_reports).unwrapOr([]) this.state.listPostReportsResponse?.post_reports
); );
let urcs = UserService.Instance.unreadReportCountSub; let urcs = UserService.Instance.unreadReportCountSub;
if (data.post_report_view.post_report.resolved) { if (data.post_report_view.post_report.resolved) {
@ -586,12 +559,10 @@ export class Reports extends Component<any, ReportsState> {
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ResolveCommentReport) { } else if (op == UserOperation.ResolveCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse); let data = wsJsonToRes<CommentReportResponse>(msg);
updateCommentReportRes( updateCommentReportRes(
data.comment_report_view, data.comment_report_view,
this.state.listCommentReportsResponse this.state.listCommentReportsResponse?.comment_reports
.map(r => r.comment_reports)
.unwrapOr([])
); );
let urcs = UserService.Instance.unreadReportCountSub; let urcs = UserService.Instance.unreadReportCountSub;
if (data.comment_report_view.comment_report.resolved) { if (data.comment_report_view.comment_report.resolved) {
@ -601,15 +572,10 @@ export class Reports extends Component<any, ReportsState> {
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ResolvePrivateMessageReport) { } else if (op == UserOperation.ResolvePrivateMessageReport) {
let data = wsJsonToRes<PrivateMessageReportResponse>( let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
msg,
PrivateMessageReportResponse
);
updatePrivateMessageReportRes( updatePrivateMessageReportRes(
data.private_message_report_view, data.private_message_report_view,
this.state.listPrivateMessageReportsResponse this.state.listPrivateMessageReportsResponse?.private_message_reports
.map(r => r.private_message_reports)
.unwrapOr([])
); );
let urcs = UserService.Instance.unreadReportCountSub; let urcs = UserService.Instance.unreadReportCountSub;
if (data.private_message_report_view.private_message_report.resolved) { if (data.private_message_report_view.private_message_report.resolved) {

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockCommunity, BlockCommunity,
@ -16,7 +15,6 @@ import {
PersonViewSafe, PersonViewSafe,
SaveUserSettings, SaveUserSettings,
SortType, SortType,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -25,7 +23,6 @@ import { Subscription } from "rxjs";
import { i18n, languages } from "../../i18next"; import { i18n, languages } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
communitySelectName, communitySelectName,
@ -38,6 +35,7 @@ import {
fetchUsers, fetchUsers,
getLanguages, getLanguages,
isBrowser, isBrowser,
myAuth,
personSelectName, personSelectName,
personToChoice, personToChoice,
relTags, relTags,
@ -67,11 +65,38 @@ if (isBrowser()) {
} }
interface SettingsState { interface SettingsState {
saveUserSettingsForm: SaveUserSettings; // TODO redo these forms
changePasswordForm: ChangePassword; saveUserSettingsForm: {
deleteAccountForm: DeleteAccount; show_nsfw?: boolean;
theme?: string;
default_sort_type?: number;
default_listing_type?: number;
interface_language?: string;
avatar?: string;
banner?: string;
display_name?: string;
email?: string;
bio?: string;
matrix_user_id?: string;
show_avatars?: boolean;
show_scores?: boolean;
send_notifications_to_email?: boolean;
bot_account?: boolean;
show_bot_accounts?: boolean;
show_read_posts?: boolean;
show_new_post_notifs?: boolean;
discussion_languages?: number[];
};
changePasswordForm: {
new_password?: string;
new_password_verify?: string;
old_password?: string;
};
deleteAccountForm: {
password?: string;
};
personBlocks: PersonBlockView[]; personBlocks: PersonBlockView[];
blockPerson: Option<PersonViewSafe>; blockPerson?: PersonViewSafe;
communityBlocks: CommunityBlockView[]; communityBlocks: CommunityBlockView[];
blockCommunityId: number; blockCommunityId: number;
blockCommunity?: CommunityView; blockCommunity?: CommunityView;
@ -88,46 +113,16 @@ export class Settings extends Component<any, SettingsState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private blockPersonChoices: any; private blockPersonChoices: any;
private blockCommunityChoices: any; private blockCommunityChoices: any;
private subscription: Subscription; private subscription?: Subscription;
private emptyState: SettingsState = { state: SettingsState = {
saveUserSettingsForm: new SaveUserSettings({ saveUserSettingsForm: {},
show_nsfw: None, changePasswordForm: {},
show_scores: None,
show_avatars: None,
show_read_posts: None,
show_bot_accounts: None,
show_new_post_notifs: None,
default_sort_type: None,
default_listing_type: None,
theme: None,
interface_language: None,
discussion_languages: None,
avatar: None,
banner: None,
display_name: None,
email: None,
bio: None,
matrix_user_id: None,
send_notifications_to_email: None,
bot_account: None,
auth: undefined,
}),
changePasswordForm: new ChangePassword({
new_password: undefined,
new_password_verify: undefined,
old_password: undefined,
auth: undefined,
}),
saveUserSettingsLoading: false, saveUserSettingsLoading: false,
changePasswordLoading: false, changePasswordLoading: false,
deleteAccountLoading: false, deleteAccountLoading: false,
deleteAccountShowConfirm: false, deleteAccountShowConfirm: false,
deleteAccountForm: new DeleteAccount({ deleteAccountForm: {},
password: undefined,
auth: undefined,
}),
personBlocks: [], personBlocks: [],
blockPerson: None,
communityBlocks: [], communityBlocks: [],
blockCommunityId: 0, blockCommunityId: 0,
currentTab: "settings", currentTab: "settings",
@ -138,7 +133,6 @@ export class Settings extends Component<any, SettingsState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortTypeChange = this.handleSortTypeChange.bind(this); this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleBioChange = this.handleBioChange.bind(this); this.handleBioChange = this.handleBioChange.bind(this);
@ -154,8 +148,8 @@ export class Settings extends Component<any, SettingsState> {
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (UserService.Instance.myUserInfo.isSome()) { let mui = UserService.Instance.myUserInfo;
let mui = UserService.Instance.myUserInfo.unwrap(); if (mui) {
let luv = mui.local_user_view; let luv = mui.local_user_view;
this.state = { this.state = {
...this.state, ...this.state,
@ -163,26 +157,25 @@ export class Settings extends Component<any, SettingsState> {
communityBlocks: mui.community_blocks, communityBlocks: mui.community_blocks,
saveUserSettingsForm: { saveUserSettingsForm: {
...this.state.saveUserSettingsForm, ...this.state.saveUserSettingsForm,
show_nsfw: Some(luv.local_user.show_nsfw), show_nsfw: luv.local_user.show_nsfw,
theme: Some(luv.local_user.theme ? luv.local_user.theme : "browser"), theme: luv.local_user.theme ? luv.local_user.theme : "browser",
default_sort_type: Some(luv.local_user.default_sort_type), default_sort_type: luv.local_user.default_sort_type,
default_listing_type: Some(luv.local_user.default_listing_type), default_listing_type: luv.local_user.default_listing_type,
interface_language: Some(luv.local_user.interface_language), interface_language: luv.local_user.interface_language,
discussion_languages: Some(mui.discussion_languages), discussion_languages: mui.discussion_languages,
avatar: luv.person.avatar, avatar: luv.person.avatar,
banner: luv.person.banner, banner: luv.person.banner,
display_name: luv.person.display_name, display_name: luv.person.display_name,
show_avatars: Some(luv.local_user.show_avatars), show_avatars: luv.local_user.show_avatars,
bot_account: Some(luv.person.bot_account), bot_account: luv.person.bot_account,
show_bot_accounts: Some(luv.local_user.show_bot_accounts), show_bot_accounts: luv.local_user.show_bot_accounts,
show_scores: Some(luv.local_user.show_scores), show_scores: luv.local_user.show_scores,
show_read_posts: Some(luv.local_user.show_read_posts), show_read_posts: luv.local_user.show_read_posts,
show_new_post_notifs: Some(luv.local_user.show_new_post_notifs), show_new_post_notifs: luv.local_user.show_new_post_notifs,
email: luv.local_user.email, email: luv.local_user.email,
bio: luv.person.bio, bio: luv.person.bio,
send_notifications_to_email: Some( send_notifications_to_email:
luv.local_user.send_notifications_to_email luv.local_user.send_notifications_to_email,
),
matrix_user_id: luv.person.matrix_user_id, matrix_user_id: luv.person.matrix_user_id,
}, },
}; };
@ -195,7 +188,7 @@ export class Settings extends Component<any, SettingsState> {
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
get documentTitle(): string { get documentTitle(): string {
@ -209,7 +202,7 @@ export class Settings extends Component<any, SettingsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={Some(this.documentTitle)} description={this.documentTitle}
image={this.state.saveUserSettingsForm.avatar} image={this.state.saveUserSettingsForm.avatar}
/> />
<ul className="nav nav-tabs mb-2"> <ul className="nav nav-tabs mb-2">
@ -391,6 +384,7 @@ export class Settings extends Component<any, SettingsState> {
} }
blockUserForm() { blockUserForm() {
let blockPerson = this.state.blockPerson;
return ( return (
<div className="form-group row"> <div className="form-group row">
<label <label
@ -403,17 +397,14 @@ export class Settings extends Component<any, SettingsState> {
<select <select
className="form-control" className="form-control"
id="block-person-filter" id="block-person-filter"
value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)} value={blockPerson?.person.id ?? 0}
> >
<option value="0"></option> <option value="0"></option>
{this.state.blockPerson.match({ {blockPerson && (
some: personView => ( <option value={blockPerson.person.id}>
<option value={personView.person.id}> {personSelectName(blockPerson)}
{personSelectName(personView)}
</option> </option>
), )}
none: <></>,
})}
</select> </select>
</div> </div>
</div> </div>
@ -500,9 +491,7 @@ export class Settings extends Component<any, SettingsState> {
type="text" type="text"
className="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined( value={this.state.saveUserSettingsForm.display_name}
this.state.saveUserSettingsForm.display_name
)}
onInput={linkEvent(this, this.handleDisplayNameChange)} onInput={linkEvent(this, this.handleDisplayNameChange)}
pattern="^(?!@)(.+)$" pattern="^(?!@)(.+)$"
minLength={3} minLength={3}
@ -516,11 +505,8 @@ export class Settings extends Component<any, SettingsState> {
<div className="col-sm-9"> <div className="col-sm-9">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.saveUserSettingsForm.bio} initialContent={this.state.saveUserSettingsForm.bio}
initialLanguageId={None}
onContentChange={this.handleBioChange} onContentChange={this.handleBioChange}
maxLength={Some(300)} maxLength={300}
placeholder={None}
buttonTitle={None}
hideNavigationWarnings hideNavigationWarnings
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
@ -537,7 +523,7 @@ export class Settings extends Component<any, SettingsState> {
id="user-email" id="user-email"
className="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.saveUserSettingsForm.email)} value={this.state.saveUserSettingsForm.email}
onInput={linkEvent(this, this.handleEmailChange)} onInput={linkEvent(this, this.handleEmailChange)}
minLength={3} minLength={3}
/> />
@ -555,9 +541,7 @@ export class Settings extends Component<any, SettingsState> {
type="text" type="text"
className="form-control" className="form-control"
placeholder="@user:example.com" placeholder="@user:example.com"
value={toUndefined( value={this.state.saveUserSettingsForm.matrix_user_id}
this.state.saveUserSettingsForm.matrix_user_id
)}
onInput={linkEvent(this, this.handleMatrixUserIdChange)} onInput={linkEvent(this, this.handleMatrixUserIdChange)}
pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
/> />
@ -593,9 +577,7 @@ export class Settings extends Component<any, SettingsState> {
<div className="col-sm-9"> <div className="col-sm-9">
<select <select
id="user-language" id="user-language"
value={toUndefined( value={this.state.saveUserSettingsForm.interface_language}
this.state.saveUserSettingsForm.interface_language
)}
onChange={linkEvent(this, this.handleInterfaceLangChange)} onChange={linkEvent(this, this.handleInterfaceLangChange)}
className="custom-select w-auto" className="custom-select w-auto"
> >
@ -631,7 +613,7 @@ export class Settings extends Component<any, SettingsState> {
<div className="col-sm-9"> <div className="col-sm-9">
<select <select
id="user-theme" id="user-theme"
value={toUndefined(this.state.saveUserSettingsForm.theme)} value={this.state.saveUserSettingsForm.theme}
onChange={linkEvent(this, this.handleThemeChange)} onChange={linkEvent(this, this.handleThemeChange)}
className="custom-select w-auto" className="custom-select w-auto"
> >
@ -653,9 +635,7 @@ export class Settings extends Component<any, SettingsState> {
<ListingTypeSelect <ListingTypeSelect
type_={ type_={
Object.values(ListingType)[ Object.values(ListingType)[
this.state.saveUserSettingsForm.default_listing_type.unwrapOr( this.state.saveUserSettingsForm.default_listing_type ?? 1
1
)
] ]
} }
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
@ -670,9 +650,7 @@ export class Settings extends Component<any, SettingsState> {
<SortSelect <SortSelect
sort={ sort={
Object.values(SortType)[ Object.values(SortType)[
this.state.saveUserSettingsForm.default_sort_type.unwrapOr( this.state.saveUserSettingsForm.default_sort_type ?? 0
0
)
] ]
} }
onChange={this.handleSortTypeChange} onChange={this.handleSortTypeChange}
@ -686,9 +664,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-nsfw" id="user-show-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_nsfw}
this.state.saveUserSettingsForm.show_nsfw
)}
onChange={linkEvent(this, this.handleShowNsfwChange)} onChange={linkEvent(this, this.handleShowNsfwChange)}
/> />
<label className="form-check-label" htmlFor="user-show-nsfw"> <label className="form-check-label" htmlFor="user-show-nsfw">
@ -703,9 +679,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-scores" id="user-show-scores"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_scores}
this.state.saveUserSettingsForm.show_scores
)}
onChange={linkEvent(this, this.handleShowScoresChange)} onChange={linkEvent(this, this.handleShowScoresChange)}
/> />
<label className="form-check-label" htmlFor="user-show-scores"> <label className="form-check-label" htmlFor="user-show-scores">
@ -719,9 +693,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-avatars" id="user-show-avatars"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_avatars}
this.state.saveUserSettingsForm.show_avatars
)}
onChange={linkEvent(this, this.handleShowAvatarsChange)} onChange={linkEvent(this, this.handleShowAvatarsChange)}
/> />
<label className="form-check-label" htmlFor="user-show-avatars"> <label className="form-check-label" htmlFor="user-show-avatars">
@ -735,9 +707,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-bot-account" id="user-bot-account"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.bot_account}
this.state.saveUserSettingsForm.bot_account
)}
onChange={linkEvent(this, this.handleBotAccount)} onChange={linkEvent(this, this.handleBotAccount)}
/> />
<label className="form-check-label" htmlFor="user-bot-account"> <label className="form-check-label" htmlFor="user-bot-account">
@ -751,9 +721,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-bot-accounts" id="user-show-bot-accounts"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_bot_accounts}
this.state.saveUserSettingsForm.show_bot_accounts
)}
onChange={linkEvent(this, this.handleShowBotAccounts)} onChange={linkEvent(this, this.handleShowBotAccounts)}
/> />
<label <label
@ -770,9 +738,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-read-posts" id="user-show-read-posts"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_read_posts}
this.state.saveUserSettingsForm.show_read_posts
)}
onChange={linkEvent(this, this.handleReadPosts)} onChange={linkEvent(this, this.handleReadPosts)}
/> />
<label <label
@ -789,9 +755,7 @@ export class Settings extends Component<any, SettingsState> {
className="form-check-input" className="form-check-input"
id="user-show-new-post-notifs" id="user-show-new-post-notifs"
type="checkbox" type="checkbox"
checked={toUndefined( checked={this.state.saveUserSettingsForm.show_new_post_notifs}
this.state.saveUserSettingsForm.show_new_post_notifs
)}
onChange={linkEvent(this, this.handleShowNewPostNotifs)} onChange={linkEvent(this, this.handleShowNewPostNotifs)}
/> />
<label <label
@ -809,9 +773,9 @@ export class Settings extends Component<any, SettingsState> {
id="user-send-notifications-to-email" id="user-send-notifications-to-email"
type="checkbox" type="checkbox"
disabled={!this.state.saveUserSettingsForm.email} disabled={!this.state.saveUserSettingsForm.email}
checked={toUndefined( checked={
this.state.saveUserSettingsForm.send_notifications_to_email this.state.saveUserSettingsForm.send_notifications_to_email
)} }
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSendNotificationsToEmailChange this.handleSendNotificationsToEmailChange
@ -959,32 +923,37 @@ export class Settings extends Component<any, SettingsState> {
} }
handleBlockPerson(personId: number) { handleBlockPerson(personId: number) {
if (personId != 0) { let auth = myAuth();
let blockUserForm = new BlockPerson({ if (auth && personId != 0) {
let blockUserForm: BlockPerson = {
person_id: personId, person_id: personId,
block: true, block: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
} }
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) { handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
let blockUserForm = new BlockPerson({ let auth = myAuth();
if (auth) {
let blockUserForm: BlockPerson = {
person_id: i.recipientId, person_id: i.recipientId,
block: false, block: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
}
handleBlockCommunity(community_id: number) { handleBlockCommunity(community_id: number) {
if (community_id != 0) { let auth = myAuth();
let blockCommunityForm = new BlockCommunity({ if (auth && community_id != 0) {
let blockCommunityForm: BlockCommunity = {
community_id, community_id,
block: true, block: true,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm) wsClient.blockCommunity(blockCommunityForm)
); );
@ -992,94 +961,93 @@ export class Settings extends Component<any, SettingsState> {
} }
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) { handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
let blockCommunityForm = new BlockCommunity({ let auth = myAuth();
if (auth) {
let blockCommunityForm: BlockCommunity = {
community_id: i.communityId, community_id: i.communityId,
block: false, block: false,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm)); WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm)
);
}
} }
handleShowNsfwChange(i: Settings, event: any) { handleShowNsfwChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_nsfw = Some(event.target.checked); i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleShowAvatarsChange(i: Settings, event: any) { handleShowAvatarsChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_avatars = Some(event.target.checked); i.state.saveUserSettingsForm.show_avatars = event.target.checked;
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => if (mui) {
(mui.local_user_view.local_user.show_avatars = event.target.checked), mui.local_user_view.local_user.show_avatars = event.target.checked;
none: void 0, }
});
i.setState(i.state); i.setState(i.state);
} }
handleBotAccount(i: Settings, event: any) { handleBotAccount(i: Settings, event: any) {
i.state.saveUserSettingsForm.bot_account = Some(event.target.checked); i.state.saveUserSettingsForm.bot_account = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleShowBotAccounts(i: Settings, event: any) { handleShowBotAccounts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_bot_accounts = Some(event.target.checked); i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleReadPosts(i: Settings, event: any) { handleReadPosts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_read_posts = Some(event.target.checked); i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
i.setState(i.state); i.setState(i.state);
} }
handleShowNewPostNotifs(i: Settings, event: any) { handleShowNewPostNotifs(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_new_post_notifs = Some( i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
event.target.checked
);
i.setState(i.state); i.setState(i.state);
} }
handleShowScoresChange(i: Settings, event: any) { handleShowScoresChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_scores = Some(event.target.checked); i.state.saveUserSettingsForm.show_scores = event.target.checked;
UserService.Instance.myUserInfo.match({ let mui = UserService.Instance.myUserInfo;
some: mui => if (mui) {
(mui.local_user_view.local_user.show_scores = event.target.checked), mui.local_user_view.local_user.show_scores = event.target.checked;
none: void 0, }
});
i.setState(i.state); i.setState(i.state);
} }
handleSendNotificationsToEmailChange(i: Settings, event: any) { handleSendNotificationsToEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.send_notifications_to_email = Some( i.state.saveUserSettingsForm.send_notifications_to_email =
event.target.checked event.target.checked;
);
i.setState(i.state); i.setState(i.state);
} }
handleThemeChange(i: Settings, event: any) { handleThemeChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.theme = Some(event.target.value); i.state.saveUserSettingsForm.theme = event.target.value;
setTheme(event.target.value, true); setTheme(event.target.value, true);
i.setState(i.state); i.setState(i.state);
} }
handleInterfaceLangChange(i: Settings, event: any) { handleInterfaceLangChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.interface_language = Some(event.target.value); i.state.saveUserSettingsForm.interface_language = event.target.value;
i18n.changeLanguage( i18n.changeLanguage(
getLanguages(i.state.saveUserSettingsForm.interface_language.unwrap())[0] getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
); );
i.setState(i.state); i.setState(i.state);
} }
handleDiscussionLanguageChange(val: number[]) { handleDiscussionLanguageChange(val: number[]) {
this.setState( this.setState(
s => ((s.saveUserSettingsForm.discussion_languages = Some(val)), s) s => ((s.saveUserSettingsForm.discussion_languages = val), s)
); );
} }
handleSortTypeChange(val: SortType) { handleSortTypeChange(val: SortType) {
this.setState( this.setState(
s => ( s => (
(s.saveUserSettingsForm.default_sort_type = Some( (s.saveUserSettingsForm.default_sort_type =
Object.keys(SortType).indexOf(val) Object.keys(SortType).indexOf(val)),
)),
s s
) )
); );
@ -1088,46 +1056,45 @@ export class Settings extends Component<any, SettingsState> {
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.setState( this.setState(
s => ( s => (
(s.saveUserSettingsForm.default_listing_type = Some( (s.saveUserSettingsForm.default_listing_type =
Object.keys(ListingType).indexOf(val) Object.keys(ListingType).indexOf(val)),
)),
s s
) )
); );
} }
handleEmailChange(i: Settings, event: any) { handleEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.email = Some(event.target.value); i.state.saveUserSettingsForm.email = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleBioChange(val: string) { handleBioChange(val: string) {
this.setState(s => ((s.saveUserSettingsForm.bio = Some(val)), s)); this.setState(s => ((s.saveUserSettingsForm.bio = val), s));
} }
handleAvatarUpload(url: string) { handleAvatarUpload(url: string) {
this.setState(s => ((s.saveUserSettingsForm.avatar = Some(url)), s)); this.setState(s => ((s.saveUserSettingsForm.avatar = url), s));
} }
handleAvatarRemove() { handleAvatarRemove() {
this.setState(s => ((s.saveUserSettingsForm.avatar = Some("")), s)); this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s));
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.setState(s => ((s.saveUserSettingsForm.banner = Some(url)), s)); this.setState(s => ((s.saveUserSettingsForm.banner = url), s));
} }
handleBannerRemove() { handleBannerRemove() {
this.setState(s => ((s.saveUserSettingsForm.banner = Some("")), s)); this.setState(s => ((s.saveUserSettingsForm.banner = ""), s));
} }
handleDisplayNameChange(i: Settings, event: any) { handleDisplayNameChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.display_name = Some(event.target.value); i.state.saveUserSettingsForm.display_name = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
handleMatrixUserIdChange(i: Settings, event: any) { handleMatrixUserIdChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.matrix_user_id = Some(event.target.value); i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
@ -1158,21 +1125,32 @@ export class Settings extends Component<any, SettingsState> {
handleSaveSettingsSubmit(i: Settings, event: any) { handleSaveSettingsSubmit(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ saveUserSettingsLoading: true }); i.setState({ saveUserSettingsLoading: true });
i.setState(s => ((s.saveUserSettingsForm.auth = auth().unwrap()), s)); let auth = myAuth();
if (auth) {
let form = new SaveUserSettings({ ...i.state.saveUserSettingsForm }); let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
WebSocketService.Instance.send(wsClient.saveUserSettings(form)); WebSocketService.Instance.send(wsClient.saveUserSettings(form));
} }
}
handleChangePasswordSubmit(i: Settings, event: any) { handleChangePasswordSubmit(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ changePasswordLoading: true }); i.setState({ changePasswordLoading: true });
i.setState(s => ((s.changePasswordForm.auth = auth().unwrap()), s)); let auth = myAuth();
let pForm = i.state.changePasswordForm;
let form = new ChangePassword({ ...i.state.changePasswordForm }); let new_password = pForm.new_password;
let new_password_verify = pForm.new_password_verify;
let old_password = pForm.old_password;
if (auth && new_password && old_password && new_password_verify) {
let form: ChangePassword = {
new_password,
new_password_verify,
old_password,
auth,
};
WebSocketService.Instance.send(wsClient.changePassword(form)); WebSocketService.Instance.send(wsClient.changePassword(form));
} }
}
handleDeleteAccountShowConfirmToggle(i: Settings, event: any) { handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
@ -1187,12 +1165,16 @@ export class Settings extends Component<any, SettingsState> {
handleDeleteAccount(i: Settings, event: any) { handleDeleteAccount(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ deleteAccountLoading: true }); i.setState({ deleteAccountLoading: true });
i.setState(s => ((s.deleteAccountForm.auth = auth().unwrap()), s)); let auth = myAuth();
let password = i.state.deleteAccountForm.password;
let form = new DeleteAccount({ ...i.state.deleteAccountForm }); if (auth && password) {
let form: DeleteAccount = {
password,
auth,
};
WebSocketService.Instance.send(wsClient.deleteAccount(form)); WebSocketService.Instance.send(wsClient.deleteAccount(form));
} }
}
handleSwitchTab(i: { ctx: Settings; tab: string }) { handleSwitchTab(i: { ctx: Settings; tab: string }) {
i.ctx.setState({ currentTab: i.tab }); i.ctx.setState({ currentTab: i.tab });
@ -1215,14 +1197,14 @@ export class Settings extends Component<any, SettingsState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.SaveUserSettings) { } else if (op == UserOperation.SaveUserSettings) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
UserService.Instance.login(data); UserService.Instance.login(data);
location.reload(); location.reload();
this.setState({ saveUserSettingsLoading: false }); this.setState({ saveUserSettingsLoading: false });
toast(i18n.t("saved")); toast(i18n.t("saved"));
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (op == UserOperation.ChangePassword) { } else if (op == UserOperation.ChangePassword) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg);
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState({ changePasswordLoading: false }); this.setState({ changePasswordLoading: false });
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -1235,20 +1217,19 @@ export class Settings extends Component<any, SettingsState> {
UserService.Instance.logout(); UserService.Instance.logout();
window.location.href = "/"; window.location.href = "/";
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data).match({ updatePersonBlock(data);
some: blocks => this.setState({ personBlocks: blocks }), let mui = UserService.Instance.myUserInfo;
none: void 0, if (mui) {
}); this.setState({ personBlocks: mui.person_blocks });
}
} else if (op == UserOperation.BlockCommunity) { } else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes<BlockCommunityResponse>( let data = wsJsonToRes<BlockCommunityResponse>(msg);
msg, updateCommunityBlock(data);
BlockCommunityResponse let mui = UserService.Instance.myUserInfo;
); if (mui) {
updateCommunityBlock(data).match({ this.setState({ communityBlocks: mui.community_blocks });
some: blocks => this.setState({ communityBlocks: blocks }), }
none: void 0,
});
} }
} }
} }

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads/build";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
GetSiteResponse, GetSiteResponse,
@ -27,20 +26,18 @@ interface State {
export class VerifyEmail extends Component<any, State> { export class VerifyEmail extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
emptyState: State = { state: State = {
verifyEmailForm: new VerifyEmailForm({ verifyEmailForm: {
token: this.props.match.params.token, token: this.props.match.params.token,
}), },
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
} }
@ -53,7 +50,7 @@ export class VerifyEmail extends Component<any, State> {
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -69,8 +66,6 @@ export class VerifyEmail extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
@ -90,10 +85,9 @@ export class VerifyEmail extends Component<any, State> {
this.props.history.push("/"); this.props.history.push("/");
return; return;
} else if (op == UserOperation.VerifyEmail) { } else if (op == UserOperation.VerifyEmail) {
let data = wsJsonToRes<VerifyEmailResponse>(msg, VerifyEmailResponse); let data = wsJsonToRes<VerifyEmailResponse>(msg);
if (data) { if (data) {
toast(i18n.t("email_verified")); toast(i18n.t("email_verified"));
this.setState(this.emptyState);
this.props.history.push("/login"); this.props.history.push("/login");
} }
} }

View file

@ -1,4 +1,3 @@
import { Either, Left, None, Option, Right, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
GetCommunity, GetCommunity,
@ -9,7 +8,6 @@ import {
ListingType, ListingType,
PostView, PostView,
SortType, SortType,
toOption,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -19,11 +17,11 @@ import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
myAuth,
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
@ -34,30 +32,28 @@ import { Spinner } from "../common/icon";
import { PostForm } from "./post-form"; import { PostForm } from "./post-form";
interface CreatePostState { interface CreatePostState {
listCommunitiesResponse: Option<ListCommunitiesResponse>; listCommunitiesResponse?: ListCommunitiesResponse;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
} }
export class CreatePost extends Component<any, CreatePostState> { export class CreatePost extends Component<any, CreatePostState> {
private isoData = setIsoData(this.context, ListCommunitiesResponse); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private emptyState: CreatePostState = { state: CreatePostState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
listCommunitiesResponse: None,
loading: true, loading: true,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -66,9 +62,8 @@ export class CreatePost extends Component<any, CreatePostState> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
listCommunitiesResponse: Some( listCommunitiesResponse: this.isoData
this.isoData.routeData[0] as ListCommunitiesResponse .routeData[0] as ListCommunitiesResponse,
),
loading: false, loading: false,
}; };
} else { } else {
@ -77,44 +72,38 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
refetch() { refetch() {
this.params.nameOrId.match({ let nameOrId = this.params.nameOrId;
some: opt => let auth = myAuth(false);
opt.match({ if (nameOrId) {
left: name => { if (typeof nameOrId === "string") {
let form = new GetCommunity({ let form: GetCommunity = {
name: Some(name), name: nameOrId,
id: None, auth,
auth: auth(false).ok(), };
});
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
}, } else {
right: id => { let form: GetCommunity = {
let form = new GetCommunity({ id: nameOrId,
id: Some(id), auth,
name: None, };
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
}, }
}), } else {
none: () => { let listCommunitiesForm: ListCommunities = {
let listCommunitiesForm = new ListCommunities({ type_: ListingType.All,
type_: Some(ListingType.All), sort: SortType.TopAll,
sort: Some(SortType.TopAll), limit: fetchLimit,
limit: Some(fetchLimit), auth,
page: None, };
auth: auth(false).ok(),
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
); );
}, }
});
} }
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
@ -125,29 +114,26 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
render() { render() {
let res = this.state.listCommunitiesResponse;
return ( return (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
this.state.listCommunitiesResponse.match({ res && (
some: res => (
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_post")}</h5> <h5>{i18n.t("create_post")}</h5>
<PostForm <PostForm
post_view={None} communities={res.communities}
communities={Some(res.communities)}
onCreate={this.handlePostCreate} onCreate={this.handlePostCreate}
params={Some(this.params)} params={this.params}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
@ -155,9 +141,7 @@ export class CreatePost extends Component<any, CreatePostState> {
/> />
</div> </div>
</div> </div>
), )
none: <></>,
})
)} )}
</div> </div>
); );
@ -165,48 +149,42 @@ export class CreatePost extends Component<any, CreatePostState> {
get params(): PostFormParams { get params(): PostFormParams {
let urlParams = new URLSearchParams(this.props.location.search); let urlParams = new URLSearchParams(this.props.location.search);
let name = toOption(urlParams.get("community_name")).or( let name = urlParams.get("community_name") ?? this.prevCommunityName;
this.prevCommunityName let communityIdParam = urlParams.get("community_id");
); let id = communityIdParam ? Number(communityIdParam) : this.prevCommunityId;
let id = toOption(urlParams.get("community_id")) let nameOrId: string | number | undefined;
.map(Number) if (name) {
.or(this.prevCommunityId); nameOrId = name;
let nameOrId: Option<Either<string, number>>; } else if (id) {
if (name.isSome()) { nameOrId = id;
nameOrId = Some(Left(name.unwrap()));
} else if (id.isSome()) {
nameOrId = Some(Right(id.unwrap()));
} else {
nameOrId = None;
} }
let params: PostFormParams = { let params: PostFormParams = {
name: toOption(urlParams.get("title")), name: urlParams.get("title") ?? undefined,
nameOrId, nameOrId,
body: toOption(urlParams.get("body")), body: urlParams.get("body") ?? undefined,
url: toOption(urlParams.get("url")), url: urlParams.get("url") ?? undefined,
}; };
return params; return params;
} }
get prevCommunityName(): Option<string> { get prevCommunityName(): string | undefined {
if (this.props.match.params.name) { if (this.props.match.params.name) {
return toOption(this.props.match.params.name); return this.props.match.params.name;
} else if (this.props.location.state) { } else if (this.props.location.state) {
let lastLocation = this.props.location.state.prevPath; let lastLocation = this.props.location.state.prevPath;
if (lastLocation.includes("/c/")) { if (lastLocation.includes("/c/")) {
return toOption(lastLocation.split("/c/")[1]); return lastLocation.split("/c/").at(1);
} }
} }
return None; return undefined;
} }
get prevCommunityId(): Option<number> { get prevCommunityId(): number | undefined {
if (this.props.match.params.id) { // TODO is this actually a number? Whats the real return type
return toOption(this.props.match.params.id); let id = this.props.match.params.id;
} return id ?? undefined;
return None;
} }
handlePostCreate(post_view: PostView) { handlePostCreate(post_view: PostView) {
@ -214,13 +192,12 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let listCommunitiesForm = new ListCommunities({ let listCommunitiesForm: ListCommunities = {
type_: Some(ListingType.All), type_: ListingType.All,
sort: Some(SortType.TopAll), sort: SortType.TopAll,
limit: Some(fetchLimit), limit: fetchLimit,
page: None,
auth: req.auth, auth: req.auth,
}); };
return [req.client.listCommunities(listCommunitiesForm)]; return [req.client.listCommunities(listCommunitiesForm)];
} }
@ -231,17 +208,14 @@ export class CreatePost extends Component<any, CreatePostState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>( let data = wsJsonToRes<ListCommunitiesResponse>(msg);
msg, this.setState({ listCommunitiesResponse: data, loading: false });
ListCommunitiesResponse
);
this.setState({ listCommunitiesResponse: Some(data), loading: false });
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({ this.setState({
listCommunitiesResponse: Some({ listCommunitiesResponse: {
communities: [data.community_view], communities: [data.community_view],
}), },
loading: false, loading: false,
}); });
} }

View file

@ -17,63 +17,51 @@ export class MetadataCard extends Component<
MetadataCardProps, MetadataCardProps,
MetadataCardState MetadataCardState
> { > {
private emptyState: MetadataCardState = { state: MetadataCardState = {
expanded: false, expanded: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
} }
render() { render() {
let post = this.props.post; let post = this.props.post;
return ( return (
<> <>
{!this.state.expanded && {!this.state.expanded && post.embed_title && post.url && (
post.embed_title.match({
some: embedTitle =>
post.url.match({
some: url => (
<div className="card border-secondary mt-3 mb-2"> <div className="card border-secondary mt-3 mb-2">
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<div className="card-body"> <div className="card-body">
{post.name !== embedTitle && ( {post.name !== post.embed_title && (
<> <>
<h5 className="card-title d-inline"> <h5 className="card-title d-inline">
<a <a className="text-body" href={post.url} rel={relTags}>
className="text-body" {post.embed_title}
href={url}
rel={relTags}
>
{embedTitle}
</a> </a>
</h5> </h5>
<span className="d-inline-block ml-2 mb-2 small text-muted"> <span className="d-inline-block ml-2 mb-2 small text-muted">
<a <a
className="text-muted font-italic" className="text-muted font-italic"
href={url} href={post.url}
rel={relTags} rel={relTags}
> >
{new URL(url).hostname} {new URL(post.url).hostname}
<Icon icon="external-link" classes="ml-1" /> <Icon icon="external-link" classes="ml-1" />
</a> </a>
</span> </span>
</> </>
)} )}
{post.embed_description.match({ {post.embed_description && (
some: desc => (
<div <div
className="card-text small text-muted md-div" className="card-text small text-muted md-div"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: sanitizeHtml(desc), __html: sanitizeHtml(post.embed_description),
}} }}
/> />
), )}
none: <></>, {post.embed_video_url && (
})}
{post.embed_video_url.isSome() && (
<button <button
className="mt-2 btn btn-secondary text-monospace" className="mt-2 btn btn-secondary text-monospace"
onClick={linkEvent(this, this.handleIframeExpand)} onClick={linkEvent(this, this.handleIframeExpand)}
@ -85,16 +73,10 @@ export class MetadataCard extends Component<
</div> </div>
</div> </div>
</div> </div>
), )}
none: <></>, {this.state.expanded && post.embed_video_url && (
}), <iframe src={post.embed_video_url}></iframe>
none: <></>, )}
})}
{this.state.expanded &&
post.embed_video_url.match({
some: video_url => <iframe src={video_url}></iframe>,
none: <></>,
})}
</> </>
); );
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
@ -14,7 +13,6 @@ import {
SearchResponse, SearchResponse,
SearchType, SearchType,
SortType, SortType,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -26,7 +24,6 @@ import { PostFormParams } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
archiveTodayUrl, archiveTodayUrl,
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
communitySelectName, communitySelectName,
@ -37,6 +34,7 @@ import {
ghostArchiveUrl, ghostArchiveUrl,
isBrowser, isBrowser,
isImage, isImage,
myAuth,
myFirstDiscussionLanguageId, myFirstDiscussionLanguageId,
pictrsDeleteToast, pictrsDeleteToast,
relTags, relTags,
@ -62,11 +60,11 @@ if (isBrowser()) {
const MAX_POST_TITLE_LENGTH = 200; const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps { interface PostFormProps {
post_view: Option<PostView>; // If a post is given, that means this is an edit post_view?: PostView; // If a post is given, that means this is an edit
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
communities: Option<CommunityView[]>; communities?: CommunityView[];
params: Option<PostFormParams>; params?: PostFormParams;
onCancel?(): any; onCancel?(): any;
onCreate?(post: PostView): any; onCreate?(post: PostView): any;
onEdit?(post: PostView): any; onEdit?(post: PostView): any;
@ -75,10 +73,18 @@ interface PostFormProps {
} }
interface PostFormState { interface PostFormState {
postForm: CreatePost; form: {
suggestedTitle: Option<string>; name?: string;
suggestedPosts: Option<PostView[]>; url?: string;
crossPosts: Option<PostView[]>; body?: string;
nsfw?: boolean;
language_id?: number;
community_id?: number;
honeypot?: string;
};
suggestedTitle?: string;
suggestedPosts?: PostView[];
crossPosts?: PostView[];
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
communitySearchLoading: boolean; communitySearchLoading: boolean;
@ -86,26 +92,14 @@ interface PostFormState {
} }
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
private subscription: Subscription; private subscription?: Subscription;
private choices: any; private choices: any;
private emptyState: PostFormState = { state: PostFormState = {
postForm: new CreatePost({ form: {},
community_id: undefined,
name: undefined,
nsfw: Some(false),
url: None,
body: None,
honeypot: None,
language_id: None,
auth: undefined,
}),
loading: false, loading: false,
imageLoading: false, imageLoading: false,
communitySearchLoading: false, communitySearchLoading: false,
previewMode: false, previewMode: false,
suggestedTitle: None,
suggestedPosts: None,
crossPosts: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -115,37 +109,32 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.handlePostBodyChange = this.handlePostBodyChange.bind(this); this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
this.handleLanguageChange = this.handleLanguageChange.bind(this); this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
// Means its an edit // Means its an edit
if (this.props.post_view.isSome()) { let pv = this.props.post_view;
let pv = this.props.post_view.unwrap(); if (pv) {
this.state = { this.state = {
...this.state, ...this.state,
postForm: new CreatePost({ form: {
body: pv.post.body, body: pv.post.body,
name: pv.post.name, name: pv.post.name,
community_id: pv.community.id, community_id: pv.community.id,
url: pv.post.url, url: pv.post.url,
nsfw: Some(pv.post.nsfw), nsfw: pv.post.nsfw,
honeypot: None, language_id: pv.post.language_id,
language_id: Some(pv.post.language_id), },
auth: auth().unwrap(),
}),
}; };
} }
if (this.props.params.isSome()) { let params = this.props.params;
let params = this.props.params.unwrap(); if (params) {
this.state = { this.state = {
...this.state, ...this.state,
postForm: { form: {
...this.state.postForm, ...this.state.form,
name: toUndefined(params.name), name: params.name,
url: params.url, url: params.url,
body: params.body, body: params.body,
}, },
@ -165,41 +154,39 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
componentDidUpdate() { componentDidUpdate() {
if ( if (
!this.state.loading && !this.state.loading &&
(this.state.postForm.name || (this.state.form.name || this.state.form.url || this.state.form.body)
this.state.postForm.url.isSome() ||
this.state.postForm.body.isSome())
) { ) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
window.onbeforeunload = undefined; window.onbeforeunload = null;
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
/* this.choices && this.choices.destroy(); */ /* this.choices && this.choices.destroy(); */
window.onbeforeunload = null; window.onbeforeunload = null;
} }
render() { render() {
let selectedLangs = this.state.postForm.language_id let firstLang =
.or( this.state.form.language_id ??
myFirstDiscussionLanguageId( myFirstDiscussionLanguageId(
this.props.allLanguages, this.props.allLanguages,
this.props.siteLanguages, this.props.siteLanguages,
UserService.Instance.myUserInfo UserService.Instance.myUserInfo
) );
) let selectedLangs = firstLang ? Array.of(firstLang) : undefined;
.map(Array.of);
let url = this.state.form.url;
return ( return (
<div> <div>
<Prompt <Prompt
when={ when={
!this.state.loading && !this.state.loading &&
(this.state.postForm.name || (this.state.form.name ||
this.state.postForm.url.isSome() || this.state.form.url ||
this.state.postForm.body.isSome()) this.state.form.body)
} }
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
@ -213,27 +200,25 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
type="url" type="url"
id="post-url" id="post-url"
className="form-control" className="form-control"
value={toUndefined(this.state.postForm.url)} value={this.state.form.url}
onInput={linkEvent(this, this.handlePostUrlChange)} onInput={linkEvent(this, this.handlePostUrlChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} onPaste={linkEvent(this, this.handleImageUploadPaste)}
/> />
{this.state.suggestedTitle.match({ {this.state.suggestedTitle && (
some: title => (
<div <div
className="mt-1 text-muted small font-weight-bold pointer" className="mt-1 text-muted small font-weight-bold pointer"
role="button" role="button"
onClick={linkEvent(this, this.copySuggestedTitle)} onClick={linkEvent(this, this.copySuggestedTitle)}
> >
{i18n.t("copy_suggested_title", { title: "" })} {title} {i18n.t("copy_suggested_title", { title: "" })}{" "}
{this.state.suggestedTitle}
</div> </div>
), )}
none: <></>,
})}
<form> <form>
<label <label
htmlFor="file-upload" htmlFor="file-upload"
className={`${ className={`${
UserService.Instance.myUserInfo.isSome() && "pointer" UserService.Instance.myUserInfo && "pointer"
} d-inline-block float-right text-muted font-weight-bold`} } d-inline-block float-right text-muted font-weight-bold`}
data-tippy-content={i18n.t("upload_image")} data-tippy-content={i18n.t("upload_image")}
> >
@ -245,18 +230,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
className="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>
{this.state.postForm.url.match({ {url && validURL(url) && (
some: url =>
validURL(url) && (
<div> <div>
<a <a
href={`${webArchiveUrl}/save/${encodeURIComponent( href={`${webArchiveUrl}/save/${encodeURIComponent(url)}`}
url
)}`}
className="mr-2 d-inline-block float-right text-muted small font-weight-bold" className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
rel={relTags} rel={relTags}
> >
@ -281,36 +262,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
archive.today {i18n.t("archive_link")} archive.today {i18n.t("archive_link")}
</a> </a>
</div> </div>
), )}
none: <></>,
})}
{this.state.imageLoading && <Spinner />} {this.state.imageLoading && <Spinner />}
{this.state.postForm.url.match({ {url && isImage(url) && (
some: url =>
isImage(url) && (
<img src={url} className="img-fluid" alt="" /> <img src={url} className="img-fluid" alt="" />
), )}
none: <></>, {this.state.crossPosts && this.state.crossPosts.length > 0 && (
})}
{this.state.crossPosts.match({
some: xPosts =>
xPosts.length > 0 && (
<> <>
<div className="my-1 text-muted small font-weight-bold"> <div className="my-1 text-muted small font-weight-bold">
{i18n.t("cross_posts")} {i18n.t("cross_posts")}
</div> </div>
<PostListings <PostListings
showCommunity showCommunity
posts={xPosts} posts={this.state.crossPosts}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
/> />
</> </>
), )}
none: <></>,
})}
</div> </div>
</div> </div>
<div className="form-group row"> <div className="form-group row">
@ -319,41 +290,38 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</label> </label>
<div className="col-sm-10"> <div className="col-sm-10">
<textarea <textarea
value={this.state.postForm.name} value={this.state.form.name}
id="post-title" id="post-title"
onInput={linkEvent(this, this.handlePostNameChange)} onInput={linkEvent(this, this.handlePostNameChange)}
className={`form-control ${ className={`form-control ${
!validTitle(this.state.postForm.name) && "is-invalid" !validTitle(this.state.form.name) && "is-invalid"
}`} }`}
required required
rows={1} rows={1}
minLength={3} minLength={3}
maxLength={MAX_POST_TITLE_LENGTH} maxLength={MAX_POST_TITLE_LENGTH}
/> />
{!validTitle(this.state.postForm.name) && ( {!validTitle(this.state.form.name) && (
<div className="invalid-feedback"> <div className="invalid-feedback">
{i18n.t("invalid_post_title")} {i18n.t("invalid_post_title")}
</div> </div>
)} )}
{this.state.suggestedPosts.match({ {this.state.suggestedPosts &&
some: sPosts => this.state.suggestedPosts.length > 0 && (
sPosts.length > 0 && (
<> <>
<div className="my-1 text-muted small font-weight-bold"> <div className="my-1 text-muted small font-weight-bold">
{i18n.t("related_posts")} {i18n.t("related_posts")}
</div> </div>
<PostListings <PostListings
showCommunity showCommunity
posts={sPosts} posts={this.state.suggestedPosts}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
/> />
</> </>
), )}
none: <></>,
})}
</div> </div>
</div> </div>
@ -361,18 +329,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<label className="col-sm-2 col-form-label">{i18n.t("body")}</label> <label className="col-sm-2 col-form-label">{i18n.t("body")}</label>
<div className="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.postForm.body} initialContent={this.state.form.body}
initialLanguageId={None}
onContentChange={this.handlePostBodyChange} onContentChange={this.handlePostBodyChange}
placeholder={None}
buttonTitle={None}
maxLength={None}
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
/> />
</div> </div>
</div> </div>
{this.props.post_view.isNone() && ( {!this.props.post_view && (
<div className="form-group row"> <div className="form-group row">
<label <label
className="col-sm-2 col-form-label" className="col-sm-2 col-form-label"
@ -388,11 +352,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<select <select
className="form-control" className="form-control"
id="post-community" id="post-community"
value={this.state.postForm.community_id} value={this.state.form.community_id}
onInput={linkEvent(this, this.handlePostCommunityChange)} onInput={linkEvent(this, this.handlePostCommunityChange)}
> >
<option>{i18n.t("select_a_community")}</option> <option>{i18n.t("select_a_community")}</option>
{this.props.communities.unwrapOr([]).map(cv => ( {this.props.communities?.map(cv => (
<option key={cv.community.id} value={cv.community.id}> <option key={cv.community.id} value={cv.community.id}>
{communitySelectName(cv)} {communitySelectName(cv)}
</option> </option>
@ -412,7 +376,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
className="form-check-input position-static" className="form-check-input position-static"
id="post-nsfw" id="post-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.postForm.nsfw)} checked={this.state.form.nsfw}
onChange={linkEvent(this, this.handlePostNsfwChange)} onChange={linkEvent(this, this.handlePostNsfwChange)}
/> />
</div> </div>
@ -433,27 +397,25 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
type="text" type="text"
className="form-control honeypot" className="form-control honeypot"
id="register-honey" id="register-honey"
value={toUndefined(this.state.postForm.honeypot)} value={this.state.form.honeypot}
onInput={linkEvent(this, this.handleHoneyPotChange)} onInput={linkEvent(this, this.handleHoneyPotChange)}
/> />
<div className="form-group row"> <div className="form-group row">
<div className="col-sm-10"> <div className="col-sm-10">
<button <button
disabled={ disabled={!this.state.form.community_id || this.state.loading}
!this.state.postForm.community_id || this.state.loading
}
type="submit" type="submit"
className="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.post_view.isSome() ? ( ) : this.props.post_view ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("create")) capitalizeFirstLetter(i18n.t("create"))
)} )}
</button> </button>
{this.props.post_view.isSome() && ( {this.props.post_view && (
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
@ -475,74 +437,74 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
i.setState({ loading: true }); i.setState({ loading: true });
// Coerce empty url string to undefined // Coerce empty url string to undefined
if ( if ((i.state.form.url ?? "blank") === "") {
i.state.postForm.url.isSome() && i.setState(s => ((s.form.url = undefined), s));
i.state.postForm.url.unwrapOr("blank") === ""
) {
i.setState(s => ((s.postForm.url = None), s));
} }
let pForm = i.state.postForm; let pForm = i.state.form;
i.props.post_view.match({ let pv = i.props.post_view;
some: pv => { let auth = myAuth();
let form = new EditPost({ if (auth) {
name: Some(pForm.name), if (pv) {
let form: EditPost = {
name: pForm.name,
url: pForm.url, url: pForm.url,
body: pForm.body, body: pForm.body,
nsfw: pForm.nsfw, nsfw: pForm.nsfw,
post_id: pv.post.id, post_id: pv.post.id,
language_id: Some(pv.post.language_id), language_id: pv.post.language_id,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.editPost(form)); WebSocketService.Instance.send(wsClient.editPost(form));
}, } else {
none: () => { if (pForm.name && pForm.community_id) {
i.setState(s => ((s.postForm.auth = auth().unwrap()), s)); let form: CreatePost = {
let form = new CreatePost({ ...i.state.postForm }); name: pForm.name,
community_id: pForm.community_id,
url: pForm.url,
body: pForm.body,
nsfw: pForm.nsfw,
language_id: pForm.language_id,
honeypot: pForm.honeypot,
auth,
};
WebSocketService.Instance.send(wsClient.createPost(form)); WebSocketService.Instance.send(wsClient.createPost(form));
}, }
}); }
}
} }
copySuggestedTitle(i: PostForm) { copySuggestedTitle(i: PostForm) {
i.state.suggestedTitle.match({ let sTitle = i.state.suggestedTitle;
some: sTitle => { if (sTitle) {
i.setState( i.setState(
s => ( s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s)
(s.postForm.name = sTitle.substring(0, MAX_POST_TITLE_LENGTH)), s
)
); );
i.setState({ suggestedTitle: None }); i.setState({ suggestedTitle: undefined });
setTimeout(() => { setTimeout(() => {
let textarea: any = document.getElementById("post-title"); let textarea: any = document.getElementById("post-title");
autosize.update(textarea); autosize.update(textarea);
}, 10); }, 10);
}, }
none: void 0,
});
} }
handlePostUrlChange(i: PostForm, event: any) { handlePostUrlChange(i: PostForm, event: any) {
i.setState(s => ((s.postForm.url = Some(event.target.value)), s)); i.setState(s => ((s.form.url = event.target.value), s));
i.fetchPageTitle(); i.fetchPageTitle();
} }
fetchPageTitle() { fetchPageTitle() {
this.state.postForm.url.match({ let url = this.state.form.url;
some: url => { if (url && validURL(url)) {
if (validURL(url)) { let form: Search = {
let form = new Search({
q: url, q: url,
community_id: None, type_: SearchType.Url,
community_name: None, sort: SortType.TopAll,
creator_id: None, listing_type: ListingType.All,
type_: Some(SearchType.Url), page: 1,
sort: Some(SortType.TopAll), limit: trendingFetchLimit,
listing_type: Some(ListingType.All), auth: myAuth(false),
page: Some(1), };
limit: Some(trendingFetchLimit),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
@ -551,63 +513,57 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState({ suggestedTitle: d.metadata.title }); this.setState({ suggestedTitle: d.metadata.title });
}); });
} else { } else {
this.setState({ suggestedTitle: None, crossPosts: None }); this.setState({ suggestedTitle: undefined, crossPosts: undefined });
} }
},
none: void 0,
});
} }
handlePostNameChange(i: PostForm, event: any) { handlePostNameChange(i: PostForm, event: any) {
i.setState(s => ((s.postForm.name = event.target.value), s)); i.setState(s => ((s.form.name = event.target.value), s));
i.fetchSimilarPosts(); i.fetchSimilarPosts();
} }
fetchSimilarPosts() { fetchSimilarPosts() {
let form = new Search({ let q = this.state.form.name;
q: this.state.postForm.name, if (q && q !== "") {
type_: Some(SearchType.Posts), let form: Search = {
sort: Some(SortType.TopAll), q,
listing_type: Some(ListingType.All), type_: SearchType.Posts,
community_id: Some(this.state.postForm.community_id), sort: SortType.TopAll,
community_name: None, listing_type: ListingType.All,
creator_id: None, community_id: this.state.form.community_id,
page: Some(1), page: 1,
limit: Some(trendingFetchLimit), limit: trendingFetchLimit,
auth: auth(false).ok(), auth: myAuth(false),
}); };
if (this.state.postForm.name !== "") {
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
} else { } else {
this.setState({ suggestedPosts: None }); this.setState({ suggestedPosts: undefined });
} }
} }
handlePostBodyChange(val: string) { handlePostBodyChange(val: string) {
this.setState(s => ((s.postForm.body = Some(val)), s)); this.setState(s => ((s.form.body = val), s));
} }
handlePostCommunityChange(i: PostForm, event: any) { handlePostCommunityChange(i: PostForm, event: any) {
i.setState( i.setState(s => ((s.form.community_id = Number(event.target.value)), s));
s => ((s.postForm.community_id = Number(event.target.value)), s)
);
} }
handlePostNsfwChange(i: PostForm, event: any) { handlePostNsfwChange(i: PostForm, event: any) {
i.setState(s => ((s.postForm.nsfw = Some(event.target.checked)), s)); i.setState(s => ((s.form.nsfw = event.target.checked), s));
} }
handleLanguageChange(val: number[]) { handleLanguageChange(val: number[]) {
this.setState(s => ((s.postForm.language_id = Some(val[0])), s)); this.setState(s => ((s.form.language_id = val.at(0)), s));
} }
handleHoneyPotChange(i: PostForm, event: any) { handleHoneyPotChange(i: PostForm, event: any) {
i.setState(s => ((s.postForm.honeypot = Some(event.target.value)), s)); i.setState(s => ((s.form.honeypot = event.target.value), s));
} }
handleCancel(i: PostForm) { handleCancel(i: PostForm) {
i.props.onCancel(); i.props.onCancel?.();
} }
handlePreviewToggle(i: PostForm, event: any) { handlePreviewToggle(i: PostForm, event: any) {
@ -649,7 +605,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
let url = `${pictrsUri}/${hash}`; let url = `${pictrsUri}/${hash}`;
let deleteToken = res.files[0].delete_token; let deleteToken = res.files[0].delete_token;
let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`; let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
i.state.postForm.url = Some(url); i.state.form.url = url;
i.setState({ imageLoading: false }); i.setState({ imageLoading: false });
pictrsDeleteToast( pictrsDeleteToast(
`${i18n.t("click_to_delete_picture")}: ${file.name}`, `${i18n.t("click_to_delete_picture")}: ${file.name}`,
@ -679,9 +635,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
"choice", "choice",
(e: any) => { (e: any) => {
this.setState( this.setState(
s => ( s => ((s.form.community_id = Number(e.detail.choice.value)), s)
(s.postForm.community_id = Number(e.detail.choice.value)), s
)
); );
}, },
false false
@ -711,41 +665,31 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
} }
this.props.post_view.match({ let pv = this.props.post_view;
some: pv => this.setState(s => ((s.form.community_id = pv?.community.id), s));
this.setState(s => ((s.postForm.community_id = pv.community.id), s)),
none: void 0,
});
this.props.params.match({
some: params =>
params.nameOrId.match({
some: nameOrId =>
nameOrId.match({
left: name => {
let foundCommunityId = this.props.communities
.unwrapOr([])
.find(r => r.community.name == name).community.id;
this.setState(
s => ((s.postForm.community_id = foundCommunityId), s)
);
},
right: id =>
this.setState(s => ((s.postForm.community_id = id), s)),
}),
none: void 0,
}),
none: void 0,
});
if (isBrowser() && this.state.postForm.community_id) { let nameOrId = this.props.params?.nameOrId;
this.choices.setChoiceByValue( if (nameOrId) {
this.state.postForm.community_id.toString() if (typeof nameOrId === "string") {
); let name_ = nameOrId;
let foundCommunityId = this.props.communities?.find(
r => r.community.name == name_
)?.community.id;
this.setState(s => ((s.form.community_id = foundCommunityId), s));
} else {
let id = nameOrId;
this.setState(s => ((s.form.community_id = id), s));
}
}
if (isBrowser() && this.state.form.community_id) {
this.choices.setChoiceByValue(this.state.form.community_id.toString());
} }
this.setState(this.state); this.setState(this.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
let mui = UserService.Instance.myUserInfo;
let op = wsUserOp(msg); let op = wsUserOp(msg);
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
@ -754,33 +698,23 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState({ loading: false }); this.setState({ loading: false });
return; return;
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
UserService.Instance.myUserInfo.match({ if (data.post_view.creator.id == mui?.local_user_view.person.id) {
some: mui => { this.props.onCreate?.(data.post_view);
if (data.post_view.creator.id == mui.local_user_view.person.id) {
this.props.onCreate(data.post_view);
} }
},
none: void 0,
});
} else if (op == UserOperation.EditPost) { } else if (op == UserOperation.EditPost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
UserService.Instance.myUserInfo.match({ if (data.post_view.creator.id == mui?.local_user_view.person.id) {
some: mui => {
if (data.post_view.creator.id == mui.local_user_view.person.id) {
this.setState({ loading: false }); this.setState({ loading: false });
this.props.onEdit(data.post_view); this.props.onEdit?.(data.post_view);
} }
},
none: void 0,
});
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg);
if (data.type_ == SearchType[SearchType.Posts]) { if (data.type_ == SearchType[SearchType.Posts]) {
this.setState({ suggestedPosts: Some(data.posts) }); this.setState({ suggestedPosts: data.posts });
} else if (data.type_ == SearchType[SearchType.Url]) { } else if (data.type_ == SearchType[SearchType.Url]) {
this.setState({ crossPosts: Some(data.posts) }); this.setState({ crossPosts: data.posts });
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
import { None, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -12,8 +11,8 @@ interface PostListingsProps {
siteLanguages: number[]; siteLanguages: number[];
showCommunity?: boolean; showCommunity?: boolean;
removeDuplicates?: boolean; removeDuplicates?: boolean;
enableDownvotes: boolean; enableDownvotes?: boolean;
enableNsfw: boolean; enableNsfw?: boolean;
} }
export class PostListings extends Component<PostListingsProps, any> { export class PostListings extends Component<PostListingsProps, any> {
@ -37,9 +36,7 @@ export class PostListings extends Component<PostListingsProps, any> {
<> <>
<PostListing <PostListing
post_view={post_view} post_view={post_view}
duplicates={Some(this.duplicatesMap.get(post_view.post.id))} duplicates={this.duplicatesMap.get(post_view.post.id)}
moderators={None}
admins={None}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -72,20 +69,20 @@ export class PostListings extends Component<PostListingsProps, any> {
// Loop over the posts, find ones with same urls // Loop over the posts, find ones with same urls
for (let pv of posts) { for (let pv of posts) {
let url = pv.post.url;
if (
!pv.post.deleted && !pv.post.deleted &&
!pv.post.removed && !pv.post.removed &&
!pv.community.deleted && !pv.community.deleted &&
!pv.community.removed && !pv.community.removed &&
pv.post.url.match({ url
some: url => { ) {
if (!urlMap.get(url)) { if (!urlMap.get(url)) {
urlMap.set(url, [pv]); urlMap.set(url, [pv]);
} else { } else {
urlMap.get(url).push(pv); urlMap.get(url)?.push(pv);
}
} }
},
none: void 0,
});
} }
// Sort by oldest // Sort by oldest
@ -100,8 +97,8 @@ export class PostListings extends Component<PostListingsProps, any> {
for (let i = 0; i < posts.length; i++) { for (let i = 0; i < posts.length; i++) {
let pv = posts[i]; let pv = posts[i];
pv.post.url.match({ let url = pv.post.url;
some: url => { if (url) {
let found = urlMap.get(url); let found = urlMap.get(url);
if (found) { if (found) {
// If its the oldest, add // If its the oldest, add
@ -113,9 +110,7 @@ export class PostListings extends Component<PostListingsProps, any> {
posts.splice(i--, 1); posts.splice(i--, 1);
} }
} }
}, }
none: void 0,
});
} }
return posts; return posts;

View file

@ -1,4 +1,3 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -9,7 +8,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { auth, wsClient } from "../../utils"; import { myAuth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { PostListing } from "./post-listing"; import { PostListing } from "./post-listing";
@ -25,6 +24,7 @@ export class PostReport extends Component<PostReportProps, any> {
render() { render() {
let r = this.props.report; let r = this.props.report;
let resolver = r.resolver;
let post = r.post; let post = r.post;
let tippyContent = i18n.t( let tippyContent = i18n.t(
r.post_report.resolved ? "unresolve_report" : "resolve_report" r.post_report.resolved ? "unresolve_report" : "resolve_report"
@ -52,9 +52,6 @@ export class PostReport extends Component<PostReportProps, any> {
<div> <div>
<PostListing <PostListing
post_view={pv} post_view={pv}
duplicates={None}
moderators={None}
admins={None}
showCommunity={true} showCommunity={true}
enableDownvotes={true} enableDownvotes={true}
enableNsfw={true} enableNsfw={true}
@ -69,8 +66,7 @@ export class PostReport extends Component<PostReportProps, any> {
<div> <div>
{i18n.t("reason")}: {r.post_report.reason} {i18n.t("reason")}: {r.post_report.reason}
</div> </div>
{r.resolver.match({ {resolver && (
some: resolver => (
<div> <div>
{r.post_report.resolved ? ( {r.post_report.resolved ? (
<T i18nKey="resolved_by"> <T i18nKey="resolved_by">
@ -84,9 +80,7 @@ export class PostReport extends Component<PostReportProps, any> {
</T> </T>
)} )}
</div> </div>
), )}
none: <></>,
})}
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)} onClick={linkEvent(this, this.handleResolveReport)}
@ -105,11 +99,14 @@ export class PostReport extends Component<PostReportProps, any> {
} }
handleResolveReport(i: PostReport) { handleResolveReport(i: PostReport) {
let form = new ResolvePostReport({ let auth = myAuth();
if (auth) {
let form: ResolvePostReport = {
report_id: i.props.report.post_report.id, report_id: i.props.report.post_report.id,
resolved: !i.props.report.post_report.resolved, resolved: !i.props.report.post_report.resolved,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.resolvePostReport(form)); WebSocketService.Instance.send(wsClient.resolvePostReport(form));
} }
}
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Right, Some } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Component, createRef, linkEvent, RefObject } from "inferno";
import { import {
@ -27,7 +26,6 @@ import {
SearchResponse, SearchResponse,
SearchType, SearchType,
SortType, SortType,
toOption,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -37,7 +35,6 @@ import { i18n } from "../../i18next";
import { CommentViewType, InitialFetchRequest } from "../../interfaces"; import { CommentViewType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
buildCommentsTree, buildCommentsTree,
commentsToFlatNodes, commentsToFlatNodes,
commentTreeMaxDepth, commentTreeMaxDepth,
@ -54,6 +51,7 @@ import {
insertCommentIntoTree, insertCommentIntoTree,
isBrowser, isBrowser,
isImage, isImage,
myAuth,
restoreScrollPosition, restoreScrollPosition,
saveCommentRes, saveCommentRes,
saveScrollPosition, saveScrollPosition,
@ -75,16 +73,16 @@ import { PostListing } from "./post-listing";
const commentsShownInterval = 15; const commentsShownInterval = 15;
interface PostState { interface PostState {
postId: Option<number>; postId?: number;
commentId: Option<number>; commentId?: number;
postRes: Option<GetPostResponse>; postRes?: GetPostResponse;
commentsRes: Option<GetCommentsResponse>; commentsRes?: GetCommentsResponse;
commentTree: CommentNodeI[]; commentTree: CommentNodeI[];
commentSort: CommentSortType; commentSort: CommentSortType;
commentViewType: CommentViewType; commentViewType: CommentViewType;
scrolled?: boolean; scrolled?: boolean;
loading: boolean; loading: boolean;
crossPosts: Option<PostView[]>; crossPosts?: PostView[];
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
commentSectionRef?: RefObject<HTMLDivElement>; commentSectionRef?: RefObject<HTMLDivElement>;
showSidebarMobile: boolean; showSidebarMobile: boolean;
@ -92,16 +90,10 @@ interface PostState {
} }
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
private subscription: Subscription; private subscription?: Subscription;
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context,
GetPostResponse,
GetCommentsResponse
);
private commentScrollDebounced: () => void; private commentScrollDebounced: () => void;
private emptyState: PostState = { state: PostState = {
postRes: None,
commentsRes: None,
postId: getIdFromProps(this.props), postId: getIdFromProps(this.props),
commentId: getCommentIdFromProps(this.props), commentId: getCommentIdFromProps(this.props),
commentTree: [], commentTree: [],
@ -109,9 +101,7 @@ export class Post extends Component<any, PostState> {
commentViewType: CommentViewType.Tree, commentViewType: CommentViewType.Tree,
scrolled: false, scrolled: false,
loading: true, loading: true,
crossPosts: None,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
commentSectionRef: null,
showSidebarMobile: false, showSidebarMobile: false,
maxCommentsShown: commentsShownInterval, maxCommentsShown: commentsShownInterval,
}; };
@ -119,8 +109,6 @@ export class Post extends Component<any, PostState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
@ -130,16 +118,16 @@ export class Post extends Component<any, PostState> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
postRes: Some(this.isoData.routeData[0] as GetPostResponse), postRes: this.isoData.routeData[0] as GetPostResponse,
commentsRes: Some(this.isoData.routeData[1] as GetCommentsResponse), commentsRes: this.isoData.routeData[1] as GetCommentsResponse,
}; };
if (this.state.commentsRes.isSome()) { if (this.state.commentsRes) {
this.state = { this.state = {
...this.state, ...this.state,
commentTree: buildCommentsTree( commentTree: buildCommentsTree(
this.state.commentsRes.unwrap().comments, this.state.commentsRes.comments,
this.state.commentId.isSome() !!this.state.commentId
), ),
}; };
} }
@ -147,18 +135,19 @@ export class Post extends Component<any, PostState> {
this.state = { ...this.state, loading: false }; this.state = { ...this.state, loading: false };
if (isBrowser()) { if (isBrowser()) {
if (this.state.postRes) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ wsClient.communityJoin({
community_id: community_id: this.state.postRes.community_view.community.id,
this.state.postRes.unwrap().community_view.community.id,
}) })
); );
}
this.state.postId.match({ if (this.state.postId) {
some: post_id => WebSocketService.Instance.send(
WebSocketService.Instance.send(wsClient.postJoin({ post_id })), wsClient.postJoin({ post_id: this.state.postId })
none: void 0, );
}); }
this.fetchCrossPosts(); this.fetchCrossPosts();
@ -172,87 +161,70 @@ export class Post extends Component<any, PostState> {
} }
fetchPost() { fetchPost() {
this.setState({ commentsRes: None }); this.setState({ commentsRes: undefined, postRes: undefined });
let postForm = new GetPost({ let auth = myAuth(false);
let postForm: GetPost = {
id: this.state.postId, id: this.state.postId,
comment_id: this.state.commentId, comment_id: this.state.commentId,
auth: auth(false).ok(), auth,
}); };
WebSocketService.Instance.send(wsClient.getPost(postForm)); WebSocketService.Instance.send(wsClient.getPost(postForm));
let commentsForm = new GetComments({ let commentsForm: GetComments = {
post_id: this.state.postId, post_id: this.state.postId,
parent_id: this.state.commentId, parent_id: this.state.commentId,
max_depth: Some(commentTreeMaxDepth), max_depth: commentTreeMaxDepth,
page: None, sort: this.state.commentSort,
limit: None, type_: ListingType.All,
sort: Some(this.state.commentSort), saved_only: false,
type_: Some(ListingType.All), auth,
community_name: None, };
community_id: None,
saved_only: Some(false),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(commentsForm)); WebSocketService.Instance.send(wsClient.getComments(commentsForm));
} }
fetchCrossPosts() { fetchCrossPosts() {
this.state.postRes let q = this.state.postRes?.post_view.post.url;
.andThen(r => r.post_view.post.url) if (q) {
.match({ let form: Search = {
some: url => { q,
let form = new Search({ type_: SearchType.Url,
q: url, sort: SortType.TopAll,
type_: Some(SearchType.Url), listing_type: ListingType.All,
sort: Some(SortType.TopAll), page: 1,
listing_type: Some(ListingType.All), limit: trendingFetchLimit,
page: Some(1), auth: myAuth(false),
limit: Some(trendingFetchLimit), };
community_id: None,
community_name: None,
creator_id: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
}, }
none: void 0,
});
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let pathType = pathSplit[1]; let pathType = pathSplit.at(1);
let id = Number(pathSplit[2]); let id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
let auth = req.auth;
let postForm = new GetPost({ let postForm: GetPost = {
id: None, auth,
comment_id: None, };
auth: req.auth,
});
let commentsForm = new GetComments({ let commentsForm: GetComments = {
post_id: None, max_depth: commentTreeMaxDepth,
parent_id: None, sort: CommentSortType.Hot,
max_depth: Some(commentTreeMaxDepth), type_: ListingType.All,
page: None, saved_only: false,
limit: None, auth,
sort: Some(CommentSortType.Hot), };
type_: Some(ListingType.All),
community_name: None,
community_id: None,
saved_only: Some(false),
auth: req.auth,
});
// Set the correct id based on the path type // Set the correct id based on the path type
if (pathType == "post") { if (pathType == "post") {
postForm.id = Some(id); postForm.id = id;
commentsForm.post_id = Some(id); commentsForm.post_id = id;
} else { } else {
postForm.comment_id = Some(id); postForm.comment_id = id;
commentsForm.parent_id = Some(id); commentsForm.parent_id = id;
} }
promises.push(req.client.getPost(postForm)); promises.push(req.client.getPost(postForm));
@ -262,7 +234,7 @@ export class Post extends Component<any, PostState> {
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
document.removeEventListener("scroll", this.commentScrollDebounced); document.removeEventListener("scroll", this.commentScrollDebounced);
saveScrollPosition(this.context); saveScrollPosition(this.context);
@ -295,7 +267,7 @@ export class Post extends Component<any, PostState> {
} }
scrollIntoCommentSection() { scrollIntoCommentSection() {
this.state.commentSectionRef.current?.scrollIntoView(); this.state.commentSectionRef?.current?.scrollIntoView();
} }
isBottom(el: Element): boolean { isBottom(el: Element): boolean {
@ -315,31 +287,21 @@ export class Post extends Component<any, PostState> {
}; };
get documentTitle(): string { get documentTitle(): string {
return this.state.postRes.match({ let name_ = this.state.postRes?.post_view.post.name;
some: res => let siteName = this.state.siteRes.site_view.site.name;
`${res.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`, return name_ ? `${name_} - ${siteName}` : "";
none: "",
});
} }
get imageTag(): Option<string> { get imageTag(): string | undefined {
return this.state.postRes.match({ let post = this.state.postRes?.post_view.post;
some: res => let thumbnail = post?.thumbnail_url;
res.post_view.post.thumbnail_url.or( let url = post?.url;
res.post_view.post.url.match({ return thumbnail || (url && isImage(url) ? url : undefined);
some: url => (isImage(url) ? Some(url) : None),
none: None,
})
),
none: None,
});
}
get descriptionTag(): Option<string> {
return this.state.postRes.andThen(r => r.post_view.post.body);
} }
render() { render() {
let res = this.state.postRes;
let description = res?.post_view.post.body;
return ( return (
<div className="container-lg"> <div className="container-lg">
{this.state.loading ? ( {this.state.loading ? (
@ -347,23 +309,22 @@ export class Post extends Component<any, PostState> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
this.state.postRes.match({ res && (
some: res => (
<div className="row"> <div className="row">
<div className="col-12 col-md-8 mb-3"> <div className="col-12 col-md-8 mb-3">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
image={this.imageTag} image={this.imageTag}
description={this.descriptionTag} description={description}
/> />
<PostListing <PostListing
post_view={res.post_view} post_view={res.post_view}
duplicates={this.state.crossPosts} duplicates={this.state.crossPosts}
showBody showBody
showCommunity showCommunity
moderators={Some(res.moderators)} moderators={res.moderators}
admins={Some(this.state.siteRes.admins)} admins={this.state.siteRes.admins}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
@ -371,7 +332,7 @@ export class Post extends Component<any, PostState> {
/> />
<div ref={this.state.commentSectionRef} className="mb-2" /> <div ref={this.state.commentSectionRef} className="mb-2" />
<CommentForm <CommentForm
node={Right(res.post_view.post.id)} node={res.post_view.post.id}
disabled={res.post_view.post.locked} disabled={res.post_view.post.locked}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
@ -399,13 +360,9 @@ export class Post extends Component<any, PostState> {
{this.state.commentViewType == CommentViewType.Flat && {this.state.commentViewType == CommentViewType.Flat &&
this.commentsFlat()} this.commentsFlat()}
</div> </div>
<div className="d-none d-md-block col-md-4"> <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
{this.sidebar()}
</div> </div>
</div> )
),
none: <></>,
})
)} )}
</div> </div>
); );
@ -493,35 +450,34 @@ export class Post extends Component<any, PostState> {
commentsFlat() { commentsFlat() {
// These are already sorted by new // These are already sorted by new
return this.state.commentsRes.match({ let commentsRes = this.state.commentsRes;
some: commentsRes => let postRes = this.state.postRes;
this.state.postRes.match({ return (
some: postRes => ( commentsRes &&
postRes && (
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(commentsRes.comments)} nodes={commentsToFlatNodes(commentsRes.comments)}
viewType={this.state.commentViewType} viewType={this.state.commentViewType}
maxCommentsShown={Some(this.state.maxCommentsShown)} maxCommentsShown={this.state.maxCommentsShown}
noIndent noIndent
locked={postRes.post_view.post.locked} locked={postRes.post_view.post.locked}
moderators={Some(postRes.moderators)} moderators={postRes.moderators}
admins={Some(this.state.siteRes.admins)} admins={this.state.siteRes.admins}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
showContext showContext
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
/> />
</div> </div>
), )
none: <></>, );
}),
none: <></>,
});
} }
sidebar() { sidebar() {
return this.state.postRes.match({ let res = this.state.postRes;
some: res => ( return (
res && (
<div className="mb-3"> <div className="mb-3">
<Sidebar <Sidebar
community_view={res.community_view} community_view={res.community_view}
@ -532,12 +488,10 @@ export class Post extends Component<any, PostState> {
showIcon showIcon
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
communityLanguages={None}
/> />
</div> </div>
), )
none: <></>, );
});
} }
handleCommentSortChange(i: Post, event: any) { handleCommentSortChange(i: Post, event: any) {
@ -549,48 +503,46 @@ export class Post extends Component<any, PostState> {
} }
handleCommentViewTypeChange(i: Post, event: any) { handleCommentViewTypeChange(i: Post, event: any) {
let comments = i.state.commentsRes?.comments;
if (comments) {
i.setState({ i.setState({
commentViewType: Number(event.target.value), commentViewType: Number(event.target.value),
commentSort: CommentSortType.New, commentSort: CommentSortType.New,
commentTree: buildCommentsTree( commentTree: buildCommentsTree(comments, !!i.state.commentId),
i.state.commentsRes.map(r => r.comments).unwrapOr([]),
i.state.commentId.isSome()
),
}); });
} }
}
handleShowSidebarMobile(i: Post) { handleShowSidebarMobile(i: Post) {
i.setState({ showSidebarMobile: !i.state.showSidebarMobile }); i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
} }
handleViewPost(i: Post) { handleViewPost(i: Post) {
i.state.postRes.match({ let id = i.state.postRes?.post_view.post.id;
some: res => if (id) {
i.context.router.history.push(`/post/${res.post_view.post.id}`), i.context.router.history.push(`/post/${id}`);
none: void 0, }
});
} }
handleViewContext(i: Post) { handleViewContext(i: Post) {
i.state.commentsRes.match({ let parentId = getCommentParentId(
some: res => i.state.commentsRes?.comments?.at(0)?.comment
i.context.router.history.push( );
`/comment/${getCommentParentId(res.comments[0].comment).unwrap()}` if (parentId) {
), i.context.router.history.push(`/comment/${parentId}`);
none: void 0, }
});
} }
commentsTree() { commentsTree() {
let showContextButton = toOption(this.state.commentTree[0]).match({ let res = this.state.postRes;
some: comment => getDepthFromComment(comment.comment_view.comment) > 0, let firstComment = this.state.commentTree.at(0)?.comment_view.comment;
none: false, let depth = getDepthFromComment(firstComment);
}); let showContextButton = depth ? depth > 0 : false;
return this.state.postRes.match({ return (
some: res => ( res && (
<div> <div>
{this.state.commentId.isSome() && ( {!!this.state.commentId && (
<> <>
<button <button
className="pl-0 d-block btn btn-link text-muted" className="pl-0 d-block btn btn-link text-muted"
@ -611,18 +563,17 @@ export class Post extends Component<any, PostState> {
<CommentNodes <CommentNodes
nodes={this.state.commentTree} nodes={this.state.commentTree}
viewType={this.state.commentViewType} viewType={this.state.commentViewType}
maxCommentsShown={Some(this.state.maxCommentsShown)} maxCommentsShown={this.state.maxCommentsShown}
locked={res.post_view.post.locked} locked={res.post_view.post.locked}
moderators={Some(res.moderators)} moderators={res.moderators}
admins={Some(this.state.siteRes.admins)} admins={this.state.siteRes.admins}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
/> />
</div> </div>
), )
none: <></>, );
});
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -632,25 +583,19 @@ export class Post extends Component<any, PostState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.state.postRes.match({ let post_id = this.state.postRes?.post_view.post.id;
some: res => { if (post_id) {
let postId = res.post_view.post.id; WebSocketService.Instance.send(wsClient.postJoin({ post_id }));
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: postId })
);
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPost({ wsClient.getPost({
id: Some(postId), id: post_id,
comment_id: None, auth: myAuth(false),
auth: auth(false).ok(),
}) })
); );
}, }
none: void 0,
});
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse); let data = wsJsonToRes<GetPostResponse>(msg);
this.setState({ postRes: Some(data) }); this.setState({ postRes: data });
// join the rooms // join the rooms
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -666,60 +611,55 @@ export class Post extends Component<any, PostState> {
// TODO move this into initial fetch and refetch // TODO move this into initial fetch and refetch
this.fetchCrossPosts(); this.fetchCrossPosts();
setupTippy(); setupTippy();
if (this.state.commentId.isNone()) restoreScrollPosition(this.context); if (!this.state.commentId) restoreScrollPosition(this.context);
if (this.checkScrollIntoCommentsParam) { if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection(); this.scrollIntoCommentSection();
} }
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse); let data = wsJsonToRes<GetCommentsResponse>(msg);
// This section sets the comments res
let comments = this.state.commentsRes?.comments;
if (comments) {
// You might need to append here, since this could be building more comments from a tree fetch // You might need to append here, since this could be building more comments from a tree fetch
this.state.commentsRes.match({
some: res => {
// Remove the first comment, since it is the parent // Remove the first comment, since it is the parent
let newComments = data.comments; let newComments = data.comments;
newComments.shift(); newComments.shift();
res.comments.push(...newComments); comments.push(...newComments);
}, } else {
none: () => this.setState({ commentsRes: Some(data) }), this.setState({ commentsRes: data });
}); }
// this.state.commentsRes = Some(data);
let cComments = this.state.commentsRes?.comments ?? [];
this.setState({ this.setState({
commentTree: buildCommentsTree( commentTree: buildCommentsTree(cComments, !!this.state.commentId),
this.state.commentsRes.map(r => r.comments).unwrapOr([]),
this.state.commentId.isSome()
),
loading: false, loading: false,
}); });
this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
// Don't get comments from the post room, if the creator is blocked // Don't get comments from the post room, if the creator is blocked
let creatorBlocked = UserService.Instance.myUserInfo let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
.map(m => m.person_blocks)
.unwrapOr([])
.map(pb => pb.target.id) .map(pb => pb.target.id)
.includes(data.comment_view.creator.id); .includes(data.comment_view.creator.id);
// Necessary since it might be a user reply, which has the recipients, to avoid double // Necessary since it might be a user reply, which has the recipients, to avoid double
if (data.recipient_ids.length == 0 && !creatorBlocked) { let postRes = this.state.postRes;
this.state.postRes.match({ let commentsRes = this.state.commentsRes;
some: postRes => if (
this.state.commentsRes.match({ data.recipient_ids.length == 0 &&
some: commentsRes => { !creatorBlocked &&
postRes &&
commentsRes
) {
commentsRes.comments.unshift(data.comment_view); commentsRes.comments.unshift(data.comment_view);
insertCommentIntoTree( insertCommentIntoTree(
this.state.commentTree, this.state.commentTree,
data.comment_view, data.comment_view,
this.state.commentId.isSome() !!this.state.commentId
); );
postRes.post_view.counts.comments++; postRes.post_view.counts.comments++;
},
none: void 0,
}),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} }
@ -728,34 +668,22 @@ export class Post extends Component<any, PostState> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
editCommentRes( editCommentRes(data.comment_view, this.state.commentsRes?.comments);
data.comment_view,
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
saveCommentRes( saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
data.comment_view,
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes( createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
data.comment_view,
this.state.commentsRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
this.state.postRes.match({ createPostLikeRes(data.post_view, this.state.postRes?.post_view);
some: res => createPostLikeRes(data.post_view, res.post_view),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditPost || op == UserOperation.EditPost ||
@ -765,112 +693,91 @@ export class Post extends Component<any, PostState> {
op == UserOperation.FeaturePost || op == UserOperation.FeaturePost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
this.state.postRes.match({ let pv = this.state.postRes?.post_view;
some: res => (res.post_view = data.post_view), if (pv) {
none: void 0, pv = data.post_view;
});
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
}
} else if ( } else if (
op == UserOperation.EditCommunity || op == UserOperation.EditCommunity ||
op == UserOperation.DeleteCommunity || op == UserOperation.DeleteCommunity ||
op == UserOperation.RemoveCommunity || op == UserOperation.RemoveCommunity ||
op == UserOperation.FollowCommunity op == UserOperation.FollowCommunity
) { ) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg);
this.state.postRes.match({ let res = this.state.postRes;
some: res => { if (res) {
res.community_view = data.community_view; res.community_view = data.community_view;
res.post_view.community = data.community_view.community; res.post_view.community = data.community_view.community;
this.setState(this.state); this.setState(this.state);
}, }
none: void 0,
});
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let data = wsJsonToRes<BanFromCommunityResponse>( let data = wsJsonToRes<BanFromCommunityResponse>(msg);
msg,
BanFromCommunityResponse let postRes = this.state.postRes;
); if (postRes) {
this.state.postRes.match({
some: postRes =>
this.state.commentsRes.match({
some: commentsRes => {
commentsRes.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator_banned_from_community = data.banned));
if (postRes.post_view.creator.id == data.person_view.person.id) { if (postRes.post_view.creator.id == data.person_view.person.id) {
postRes.post_view.creator_banned_from_community = data.banned; postRes.post_view.creator_banned_from_community = data.banned;
} }
}
this.state.commentsRes?.comments
.filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator_banned_from_community = data.banned));
this.setState(this.state); this.setState(this.state);
},
none: void 0,
}),
none: void 0,
});
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>( let data = wsJsonToRes<AddModToCommunityResponse>(msg);
msg, let postRes = this.state.postRes;
AddModToCommunityResponse if (postRes) {
); postRes.moderators = data.moderators;
this.state.postRes.match({
some: res => {
res.moderators = data.moderators;
this.setState(this.state); this.setState(this.state);
}, }
none: void 0,
});
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse); let data = wsJsonToRes<BanPersonResponse>(msg);
this.state.postRes.match({ this.state.commentsRes?.comments
some: postRes =>
this.state.commentsRes.match({
some: commentsRes => {
commentsRes.comments
.filter(c => c.creator.id == data.person_view.person.id) .filter(c => c.creator.id == data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned)); .forEach(c => (c.creator.banned = data.banned));
let postRes = this.state.postRes;
if (postRes) {
if (postRes.post_view.creator.id == data.person_view.person.id) { if (postRes.post_view.creator.id == data.person_view.person.id) {
postRes.post_view.creator.banned = data.banned; postRes.post_view.creator.banned = data.banned;
} }
}
this.setState(this.state); this.setState(this.state);
},
none: void 0,
}),
none: void 0,
});
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = data.admins), s)); this.setState(s => ((s.siteRes.admins = data.admins), s));
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg);
let xPosts = data.posts.filter( let xPosts = data.posts.filter(
p => p.post.id != Number(this.props.match.params.id) p => p.post.id != Number(this.props.match.params.id)
); );
this.setState({ crossPosts: xPosts.length > 0 ? Some(xPosts) : None }); this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg);
this.setState({ siteRes: data }); this.setState({ siteRes: data });
} else if (op == UserOperation.TransferCommunity) { } else if (op == UserOperation.TransferCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg);
this.state.postRes.match({ let postRes = this.state.postRes;
some: res => { if (postRes) {
res.community_view = data.community_view; postRes.community_view = data.community_view;
res.post_view.community = data.community_view.community; postRes.post_view.community = data.community_view.community;
res.moderators = data.moderators; postRes.moderators = data.moderators;
this.setState(this.state); this.setState(this.state);
}, }
none: void 0,
});
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse); let data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse); let data = wsJsonToRes<CommentReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
@ -880,7 +787,7 @@ export class Post extends Component<any, PostState> {
op == UserOperation.PurgeComment || op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity op == UserOperation.PurgeCommunity
) { ) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse); let data = wsJsonToRes<PurgeItemResponse>(msg);
if (data.success) { if (data.success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
GetPersonDetails, GetPersonDetails,
@ -14,9 +13,9 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth,
getRecipientIdFromProps, getRecipientIdFromProps,
isBrowser, isBrowser,
myAuth,
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
@ -28,7 +27,7 @@ import { PrivateMessageForm } from "./private-message-form";
interface CreatePrivateMessageState { interface CreatePrivateMessageState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
recipientDetailsRes: Option<GetPersonDetailsResponse>; recipientDetailsRes?: GetPersonDetailsResponse;
recipient_id: number; recipient_id: number;
loading: boolean; loading: boolean;
} }
@ -37,25 +36,23 @@ export class CreatePrivateMessage extends Component<
any, any,
CreatePrivateMessageState CreatePrivateMessageState
> { > {
private isoData = setIsoData(this.context, GetPersonDetailsResponse); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription?: Subscription;
private emptyState: CreatePrivateMessageState = { state: CreatePrivateMessageState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
recipientDetailsRes: None,
recipient_id: getRecipientIdFromProps(this.props), recipient_id: getRecipientIdFromProps(this.props),
loading: true, loading: true,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate =
this.handlePrivateMessageCreate.bind(this); this.handlePrivateMessageCreate.bind(this);
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (!UserService.Instance.myUserInfo && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
@ -64,9 +61,8 @@ export class CreatePrivateMessage extends Component<
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state = { this.state = {
...this.state, ...this.state,
recipientDetailsRes: Some( recipientDetailsRes: this.isoData
this.isoData.routeData[0] as GetPersonDetailsResponse .routeData[0] as GetPersonDetailsResponse,
),
loading: false, loading: false,
}; };
} else { } else {
@ -75,77 +71,61 @@ export class CreatePrivateMessage extends Component<
} }
fetchPersonDetails() { fetchPersonDetails() {
let form = new GetPersonDetails({ let form: GetPersonDetails = {
person_id: Some(this.state.recipient_id), person_id: this.state.recipient_id,
sort: Some(SortType.New), sort: SortType.New,
saved_only: Some(false), saved_only: false,
username: None, auth: myAuth(false),
page: None, };
limit: None,
community_id: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getPersonDetails(form)); WebSocketService.Instance.send(wsClient.getPersonDetails(form));
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let person_id = Some(Number(req.path.split("/").pop())); let person_id = Number(req.path.split("/").pop());
let form = new GetPersonDetails({ let form: GetPersonDetails = {
person_id, person_id,
sort: Some(SortType.New), sort: SortType.New,
saved_only: Some(false), saved_only: false,
username: None,
page: None,
limit: None,
community_id: None,
auth: req.auth, auth: req.auth,
}); };
return [req.client.getPersonDetails(form)]; return [req.client.getPersonDetails(form)];
} }
get documentTitle(): string { get documentTitle(): string {
return this.state.recipientDetailsRes.match({ let name_ = this.state.recipientDetailsRes?.person_view.person.name;
some: res => return name_ ? `${i18n.t("create_private_message")} - ${name_}` : "";
`${i18n.t("create_private_message")} - ${res.person_view.person.name}`,
none: "",
});
} }
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
} }
} }
render() { render() {
let res = this.state.recipientDetailsRes;
return ( return (
<div className="container-lg"> <div className="container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
this.state.recipientDetailsRes.match({ res && (
some: res => (
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_private_message")}</h5> <h5>{i18n.t("create_private_message")}</h5>
<PrivateMessageForm <PrivateMessageForm
privateMessageView={None}
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
recipient={res.person_view.person} recipient={res.person_view.person}
/> />
</div> </div>
</div> </div>
), )
none: <></>,
})
)} )}
</div> </div>
); );
@ -166,11 +146,8 @@ export class CreatePrivateMessage extends Component<
this.setState({ loading: false }); this.setState({ loading: false });
return; return;
} else if (op == UserOperation.GetPersonDetails) { } else if (op == UserOperation.GetPersonDetails) {
let data = wsJsonToRes<GetPersonDetailsResponse>( let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
msg, this.setState({ recipientDetailsRes: data, loading: false });
GetPersonDetailsResponse
);
this.setState({ recipientDetailsRes: Some(data), loading: false });
} }
} }
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
@ -16,9 +15,9 @@ import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
isBrowser, isBrowser,
myAuth,
relTags, relTags,
setupTippy, setupTippy,
toast, toast,
@ -31,14 +30,14 @@ import { PersonListing } from "../person/person-listing";
interface PrivateMessageFormProps { interface PrivateMessageFormProps {
recipient: PersonSafe; recipient: PersonSafe;
privateMessageView: Option<PrivateMessageView>; // If a pm is given, that means this is an edit privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit
onCancel?(): any; onCancel?(): any;
onCreate?(message: PrivateMessageView): any; onCreate?(message: PrivateMessageView): any;
onEdit?(message: PrivateMessageView): any; onEdit?(message: PrivateMessageView): any;
} }
interface PrivateMessageFormState { interface PrivateMessageFormState {
privateMessageForm: CreatePrivateMessage; content?: string;
loading: boolean; loading: boolean;
previewMode: boolean; previewMode: boolean;
showDisclaimer: boolean; showDisclaimer: boolean;
@ -48,13 +47,8 @@ export class PrivateMessageForm extends Component<
PrivateMessageFormProps, PrivateMessageFormProps,
PrivateMessageFormState PrivateMessageFormState
> { > {
private subscription: Subscription; private subscription?: Subscription;
private emptyState: PrivateMessageFormState = { state: PrivateMessageFormState = {
privateMessageForm: new CreatePrivateMessage({
content: null,
recipient_id: this.props.recipient.id,
auth: auth().unwrap(),
}),
loading: false, loading: false,
previewMode: false, previewMode: false,
showDisclaimer: false, showDisclaimer: false,
@ -63,17 +57,15 @@ export class PrivateMessageForm extends Component<
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleContentChange = this.handleContentChange.bind(this); this.handleContentChange = this.handleContentChange.bind(this);
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
// Its an edit // Its an edit
if (this.props.privateMessageView.isSome()) { if (this.props.privateMessageView) {
this.state.privateMessageForm.content = this.state.content =
this.props.privateMessageView.unwrap().private_message.content; this.props.privateMessageView.private_message.content;
} }
} }
@ -82,16 +74,16 @@ export class PrivateMessageForm extends Component<
} }
componentDidUpdate() { componentDidUpdate() {
if (!this.state.loading && this.state.privateMessageForm.content) { if (!this.state.loading && this.state.content) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
window.onbeforeunload = undefined; window.onbeforeunload = null;
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (isBrowser()) { if (isBrowser()) {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
window.onbeforeunload = null; window.onbeforeunload = null;
} }
} }
@ -100,11 +92,11 @@ export class PrivateMessageForm extends Component<
return ( return (
<div> <div>
<Prompt <Prompt
when={!this.state.loading && this.state.privateMessageForm.content} when={!this.state.loading && this.state.content}
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}> <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
{this.props.privateMessageView.isNone() && ( {!this.props.privateMessageView && (
<div className="form-group row"> <div className="form-group row">
<label className="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{capitalizeFirstLetter(i18n.t("to"))} {capitalizeFirstLetter(i18n.t("to"))}
@ -129,11 +121,7 @@ export class PrivateMessageForm extends Component<
</label> </label>
<div className="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={Some(this.state.privateMessageForm.content)} initialContent={this.state.content}
initialLanguageId={None}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleContentChange} onContentChange={this.handleContentChange}
allLanguages={[]} allLanguages={[]}
siteLanguages={[]} siteLanguages={[]}
@ -168,13 +156,13 @@ export class PrivateMessageForm extends Component<
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.privateMessageView.isSome() ? ( ) : this.props.privateMessageView ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("send_message")) capitalizeFirstLetter(i18n.t("send_message"))
)} )}
</button> </button>
{this.props.privateMessageView.isSome() && ( {this.props.privateMessageView && (
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
@ -195,28 +183,35 @@ export class PrivateMessageForm extends Component<
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) { handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
event.preventDefault(); event.preventDefault();
i.props.privateMessageView.match({ let pm = i.props.privateMessageView;
some: pm => { let auth = myAuth();
let form = new EditPrivateMessage({ let content = i.state.content;
if (auth && content) {
if (pm) {
let form: EditPrivateMessage = {
private_message_id: pm.private_message.id, private_message_id: pm.private_message.id,
content: i.state.privateMessageForm.content, content,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.editPrivateMessage(form)); WebSocketService.Instance.send(wsClient.editPrivateMessage(form));
}, } else {
none: WebSocketService.Instance.send( let form: CreatePrivateMessage = {
wsClient.createPrivateMessage(i.state.privateMessageForm) content,
), recipient_id: i.props.recipient.id,
}); auth,
};
WebSocketService.Instance.send(wsClient.createPrivateMessage(form));
}
i.setState({ loading: true }); i.setState({ loading: true });
} }
}
handleContentChange(val: string) { handleContentChange(val: string) {
this.setState(s => ((s.privateMessageForm.content = val), s)); this.setState({ content: val });
} }
handleCancel(i: PrivateMessageForm) { handleCancel(i: PrivateMessageForm) {
i.props.onCancel(); i.props.onCancel?.();
} }
handlePreviewToggle(i: PrivateMessageForm, event: any) { handlePreviewToggle(i: PrivateMessageForm, event: any) {
@ -240,18 +235,12 @@ export class PrivateMessageForm extends Component<
op == UserOperation.DeletePrivateMessage || op == UserOperation.DeletePrivateMessage ||
op == UserOperation.MarkPrivateMessageAsRead op == UserOperation.MarkPrivateMessageAsRead
) { ) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg,
PrivateMessageResponse
);
this.setState({ loading: false }); this.setState({ loading: false });
this.props.onEdit(data.private_message_view); this.props.onEdit?.(data.private_message_view);
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(msg);
msg, this.props.onCreate?.(data.private_message_view);
PrivateMessageResponse
);
this.props.onCreate(data.private_message_view);
} }
} }
} }

View file

@ -6,7 +6,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { auth, mdToHtml, wsClient } from "../../utils"; import { mdToHtml, myAuth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -45,24 +45,21 @@ export class PrivateMessageReport extends Component<Props, any> {
<div> <div>
{i18n.t("reason")}: {pmr.reason} {i18n.t("reason")}: {pmr.reason}
</div> </div>
{r.resolver.match({ {r.resolver && (
some: resolver => (
<div> <div>
{pmr.resolved ? ( {pmr.resolved ? (
<T i18nKey="resolved_by"> <T i18nKey="resolved_by">
# #
<PersonListing person={resolver} /> <PersonListing person={r.resolver} />
</T> </T>
) : ( ) : (
<T i18nKey="unresolved_by"> <T i18nKey="unresolved_by">
# #
<PersonListing person={resolver} /> <PersonListing person={r.resolver} />
</T> </T>
)} )}
</div> </div>
), )}
none: <></>,
})}
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)} onClick={linkEvent(this, this.handleResolveReport)}
@ -82,11 +79,16 @@ export class PrivateMessageReport extends Component<Props, any> {
handleResolveReport(i: PrivateMessageReport) { handleResolveReport(i: PrivateMessageReport) {
let pmr = i.props.report.private_message_report; let pmr = i.props.report.private_message_report;
let form = new ResolvePrivateMessageReport({ let auth = myAuth();
if (auth) {
let form: ResolvePrivateMessageReport = {
report_id: pmr.id, report_id: pmr.id,
resolved: !pmr.resolved, resolved: !pmr.resolved,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.resolvePrivateMessageReport(form)); WebSocketService.Instance.send(
wsClient.resolvePrivateMessageReport(form)
);
}
} }
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads/build";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CreatePrivateMessageReport, CreatePrivateMessageReport,
@ -6,11 +5,10 @@ import {
MarkPrivateMessageAsRead, MarkPrivateMessageAsRead,
PersonSafe, PersonSafe,
PrivateMessageView, PrivateMessageView,
toUndefined,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { auth, mdToHtml, toast, wsClient } from "../../utils"; import { mdToHtml, myAuth, toast, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { MomentTime } from "../common/moment-time"; import { MomentTime } from "../common/moment-time";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -22,7 +20,7 @@ interface PrivateMessageState {
collapsed: boolean; collapsed: boolean;
viewSource: boolean; viewSource: boolean;
showReportDialog: boolean; showReportDialog: boolean;
reportReason: Option<string>; reportReason?: string;
} }
interface PrivateMessageProps { interface PrivateMessageProps {
@ -33,19 +31,17 @@ export class PrivateMessage extends Component<
PrivateMessageProps, PrivateMessageProps,
PrivateMessageState PrivateMessageState
> { > {
private emptyState: PrivateMessageState = { state: PrivateMessageState = {
showReply: false, showReply: false,
showEdit: false, showEdit: false,
collapsed: false, collapsed: false,
viewSource: false, viewSource: false,
showReportDialog: false, showReportDialog: false,
reportReason: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate =
this.handlePrivateMessageCreate.bind(this); this.handlePrivateMessageCreate.bind(this);
@ -53,13 +49,10 @@ export class PrivateMessage extends Component<
} }
get mine(): boolean { get mine(): boolean {
return UserService.Instance.myUserInfo return (
.map( UserService.Instance.myUserInfo?.local_user_view.person.id ==
m =>
m.local_user_view.person.id ==
this.props.private_message_view.creator.id this.props.private_message_view.creator.id
) );
.unwrapOr(false);
} }
render() { render() {
@ -104,7 +97,7 @@ export class PrivateMessage extends Component<
{this.state.showEdit && ( {this.state.showEdit && (
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
privateMessageView={Some(message_view)} privateMessageView={message_view}
onEdit={this.handlePrivateMessageEdit} onEdit={this.handlePrivateMessageEdit}
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
onCancel={this.handleReplyCancel} onCancel={this.handleReplyCancel}
@ -230,7 +223,7 @@ export class PrivateMessage extends Component<
className="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
required required
value={toUndefined(this.state.reportReason)} value={this.state.reportReason}
onInput={linkEvent(this, this.handleReportReasonChange)} onInput={linkEvent(this, this.handleReportReasonChange)}
/> />
<button <button
@ -245,7 +238,6 @@ export class PrivateMessage extends Component<
{this.state.showReply && ( {this.state.showReply && (
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
privateMessageView={None}
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
/> />
)} )}
@ -283,26 +275,32 @@ export class PrivateMessage extends Component<
} }
handleDeleteClick(i: PrivateMessage) { handleDeleteClick(i: PrivateMessage) {
let form = new DeletePrivateMessage({ let auth = myAuth();
if (auth) {
let form: DeletePrivateMessage = {
private_message_id: i.props.private_message_view.private_message.id, private_message_id: i.props.private_message_view.private_message.id,
deleted: !i.props.private_message_view.private_message.deleted, deleted: !i.props.private_message_view.private_message.deleted,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.deletePrivateMessage(form)); WebSocketService.Instance.send(wsClient.deletePrivateMessage(form));
} }
}
handleReplyCancel() { handleReplyCancel() {
this.setState({ showReply: false, showEdit: false }); this.setState({ showReply: false, showEdit: false });
} }
handleMarkRead(i: PrivateMessage) { handleMarkRead(i: PrivateMessage) {
let form = new MarkPrivateMessageAsRead({ let auth = myAuth();
if (auth) {
let form: MarkPrivateMessageAsRead = {
private_message_id: i.props.private_message_view.private_message.id, private_message_id: i.props.private_message_view.private_message.id,
read: !i.props.private_message_view.private_message.read, read: !i.props.private_message_view.private_message.read,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form)); WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form));
} }
}
handleMessageCollapse(i: PrivateMessage) { handleMessageCollapse(i: PrivateMessage) {
i.setState({ collapsed: !i.state.collapsed }); i.setState({ collapsed: !i.state.collapsed });
@ -317,34 +315,36 @@ export class PrivateMessage extends Component<
} }
handleReportReasonChange(i: PrivateMessage, event: any) { handleReportReasonChange(i: PrivateMessage, event: any) {
i.setState({ reportReason: Some(event.target.value) }); i.setState({ reportReason: event.target.value });
} }
handleReportSubmit(i: PrivateMessage, event: any) { handleReportSubmit(i: PrivateMessage, event: any) {
event.preventDefault(); event.preventDefault();
let form = new CreatePrivateMessageReport({ let auth = myAuth();
let reason = i.state.reportReason;
if (auth && reason) {
let form: CreatePrivateMessageReport = {
private_message_id: i.props.private_message_view.private_message.id, private_message_id: i.props.private_message_view.private_message.id,
reason: toUndefined(i.state.reportReason), reason,
auth: auth().unwrap(), auth,
}); };
WebSocketService.Instance.send(wsClient.createPrivateMessageReport(form)); WebSocketService.Instance.send(wsClient.createPrivateMessageReport(form));
i.setState({ showReportDialog: false }); i.setState({ showReportDialog: false });
} }
}
handlePrivateMessageEdit() { handlePrivateMessageEdit() {
this.setState({ showEdit: false }); this.setState({ showEdit: false });
} }
handlePrivateMessageCreate(message: PrivateMessageView) { handlePrivateMessageCreate(message: PrivateMessageView) {
UserService.Instance.myUserInfo.match({ if (
some: mui => { message.creator.id ==
if (message.creator.id == mui.local_user_view.person.id) { UserService.Instance.myUserInfo?.local_user_view.person.id
) {
this.setState({ showReply: false }); this.setState({ showReply: false });
toast(i18n.t("message_sent")); toast(i18n.t("message_sent"));
} }
},
none: void 0,
});
} }
} }

View file

@ -1,4 +1,3 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommentResponse, CommentResponse,
@ -30,7 +29,6 @@ import { i18n } from "../i18next";
import { CommentViewType, InitialFetchRequest } from "../interfaces"; import { CommentViewType, InitialFetchRequest } from "../interfaces";
import { WebSocketService } from "../services"; import { WebSocketService } from "../services";
import { import {
auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
commentsToFlatNodes, commentsToFlatNodes,
@ -45,6 +43,7 @@ import {
fetchLimit, fetchLimit,
fetchUsers, fetchUsers,
isBrowser, isBrowser,
myAuth,
numToSI, numToSI,
personSelectName, personSelectName,
personToChoice, personToChoice,
@ -93,13 +92,13 @@ interface SearchState {
communityId: number; communityId: number;
creatorId: number; creatorId: number;
page: number; page: number;
searchResponse: Option<SearchResponse>; searchResponse?: SearchResponse;
communities: CommunityView[]; communities: CommunityView[];
creatorDetails: Option<GetPersonDetailsResponse>; creatorDetails?: GetPersonDetailsResponse;
loading: boolean; loading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
searchText: string; searchText: string;
resolveObjectResponse: Option<ResolveObjectResponse>; resolveObjectResponse?: ResolveObjectResponse;
} }
interface UrlParams { interface UrlParams {
@ -119,18 +118,11 @@ interface Combined {
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private isoData = setIsoData( private isoData = setIsoData(this.context);
this.context,
GetCommunityResponse,
ListCommunitiesResponse,
GetPersonDetailsResponse,
SearchResponse,
ResolveObjectResponse
);
private communityChoices: any; private communityChoices: any;
private creatorChoices: any; private creatorChoices: any;
private subscription: Subscription; private subscription?: Subscription;
private emptyState: SearchState = { state: SearchState = {
q: Search.getSearchQueryFromProps(this.props.match.params.q), q: Search.getSearchQueryFromProps(this.props.match.params.q),
type_: Search.getSearchTypeFromProps(this.props.match.params.type), type_: Search.getSearchTypeFromProps(this.props.match.params.type),
sort: Search.getSortTypeFromProps(this.props.match.params.sort), sort: Search.getSortTypeFromProps(this.props.match.params.sort),
@ -143,9 +135,6 @@ export class Search extends Component<any, SearchState> {
this.props.match.params.community_id this.props.match.params.community_id
), ),
creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id), creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
searchResponse: None,
resolveObjectResponse: None,
creatorDetails: None,
loading: true, loading: true,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
communities: [], communities: [],
@ -182,7 +171,6 @@ export class Search extends Component<any, SearchState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
@ -192,42 +180,38 @@ export class Search extends Component<any, SearchState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
let communityRes = Some( let communityRes = this.isoData.routeData[0] as
this.isoData.routeData[0] as GetCommunityResponse | GetCommunityResponse
); | undefined;
let communitiesRes = Some( let communitiesRes = this.isoData.routeData[1] as
this.isoData.routeData[1] as ListCommunitiesResponse | ListCommunitiesResponse
); | undefined;
// This can be single or multiple communities given // This can be single or multiple communities given
if (communitiesRes.isSome()) { if (communitiesRes) {
this.state = { this.state = {
...this.state, ...this.state,
communities: communitiesRes.unwrap().communities, communities: communitiesRes.communities,
}; };
} }
if (communityRes.isSome()) { if (communityRes) {
this.state = { this.state = {
...this.state, ...this.state,
communities: [communityRes.unwrap().community_view], communities: [communityRes.community_view],
}; };
} }
this.state = { this.state = {
...this.state, ...this.state,
creatorDetails: Some( creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse,
this.isoData.routeData[2] as GetPersonDetailsResponse
),
}; };
if (this.state.q != "") { if (this.state.q != "") {
this.state = { this.state = {
...this.state, ...this.state,
searchResponse: Some(this.isoData.routeData[3] as SearchResponse), searchResponse: this.isoData.routeData[3] as SearchResponse,
resolveObjectResponse: Some( resolveObjectResponse: this.isoData
this.isoData.routeData[4] as ResolveObjectResponse .routeData[4] as ResolveObjectResponse,
),
loading: false, loading: false,
}; };
} else { } else {
@ -240,7 +224,7 @@ export class Search extends Component<any, SearchState> {
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription?.unsubscribe();
saveScrollPosition(this.context); saveScrollPosition(this.context);
} }
@ -266,13 +250,12 @@ export class Search extends Component<any, SearchState> {
} }
fetchCommunities() { fetchCommunities() {
let listCommunitiesForm = new ListCommunities({ let listCommunitiesForm: ListCommunities = {
type_: Some(ListingType.All), type_: ListingType.All,
sort: Some(SortType.TopAll), sort: SortType.TopAll,
limit: Some(fetchLimit), limit: fetchLimit,
page: None, auth: myAuth(false),
auth: auth(false).ok(), };
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
); );
@ -281,71 +264,56 @@ export class Search extends Component<any, SearchState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let auth = req.auth;
let communityId = this.getCommunityIdFromProps(pathSplit[11]); let communityId = this.getCommunityIdFromProps(pathSplit[11]);
let community_id: Option<number> = let community_id = communityId == 0 ? undefined : communityId;
communityId == 0 ? None : Some(communityId); if (community_id) {
community_id.match({ let getCommunityForm: GetCommunity = {
some: id => { id: community_id,
let getCommunityForm = new GetCommunity({ auth,
id: Some(id), };
name: None,
auth: req.auth,
});
promises.push(req.client.getCommunity(getCommunityForm)); promises.push(req.client.getCommunity(getCommunityForm));
promises.push(Promise.resolve()); promises.push(Promise.resolve());
}, } else {
none: () => { let listCommunitiesForm: ListCommunities = {
let listCommunitiesForm = new ListCommunities({ type_: ListingType.All,
type_: Some(ListingType.All), sort: SortType.TopAll,
sort: Some(SortType.TopAll), limit: fetchLimit,
limit: Some(fetchLimit),
page: None,
auth: req.auth, auth: req.auth,
}); };
promises.push(Promise.resolve()); promises.push(Promise.resolve());
promises.push(req.client.listCommunities(listCommunitiesForm)); promises.push(req.client.listCommunities(listCommunitiesForm));
}, }
});
let creatorId = this.getCreatorIdFromProps(pathSplit[13]); let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
let creator_id: Option<number> = creatorId == 0 ? None : Some(creatorId); let creator_id = creatorId == 0 ? undefined : creatorId;
creator_id.match({ if (creator_id) {
some: id => { let getCreatorForm: GetPersonDetails = {
let getCreatorForm = new GetPersonDetails({ person_id: creator_id,
person_id: Some(id),
username: None,
sort: None,
page: None,
limit: None,
community_id: None,
saved_only: None,
auth: req.auth, auth: req.auth,
}); };
promises.push(req.client.getPersonDetails(getCreatorForm)); promises.push(req.client.getPersonDetails(getCreatorForm));
}, } else {
none: () => {
promises.push(Promise.resolve()); promises.push(Promise.resolve());
}, }
});
let form = new SearchForm({ let form: SearchForm = {
q: this.getSearchQueryFromProps(pathSplit[3]), q: this.getSearchQueryFromProps(pathSplit[3]),
community_id, community_id,
community_name: None,
creator_id, creator_id,
type_: Some(this.getSearchTypeFromProps(pathSplit[5])), type_: this.getSearchTypeFromProps(pathSplit[5]),
sort: Some(this.getSortTypeFromProps(pathSplit[7])), sort: this.getSortTypeFromProps(pathSplit[7]),
listing_type: Some(this.getListingTypeFromProps(pathSplit[9])), listing_type: this.getListingTypeFromProps(pathSplit[9]),
page: Some(this.getPageFromProps(pathSplit[15])), page: this.getPageFromProps(pathSplit[15]),
limit: Some(fetchLimit), limit: fetchLimit,
auth: req.auth, auth: req.auth,
}); };
let resolveObjectForm = new ResolveObject({ let resolveObjectForm: ResolveObject = {
q: this.getSearchQueryFromProps(pathSplit[3]), q: this.getSearchQueryFromProps(pathSplit[3]),
auth: req.auth, auth: req.auth,
}); };
if (form.q != "") { if (form.q != "") {
promises.push(req.client.search(form)); promises.push(req.client.search(form));
@ -371,8 +339,6 @@ export class Search extends Component<any, SearchState> {
this.setState({ this.setState({
loading: true, loading: true,
searchText: this.state.q, searchText: this.state.q,
searchResponse: None,
resolveObjectResponse: None,
}); });
this.search(); this.search();
} }
@ -391,8 +357,6 @@ export class Search extends Component<any, SearchState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5>{i18n.t("search")}</h5> <h5>{i18n.t("search")}</h5>
{this.selects()} {this.selects()}
@ -512,53 +476,47 @@ export class Search extends Component<any, SearchState> {
buildCombined(): Combined[] { buildCombined(): Combined[] {
let combined: Combined[] = []; let combined: Combined[] = [];
let resolveRes = this.state.resolveObjectResponse;
// Push the possible resolve / federated objects first // Push the possible resolve / federated objects first
this.state.resolveObjectResponse.match({ if (resolveRes) {
some: res => { let resolveComment = resolveRes.comment;
let resolveComment = res.comment; if (resolveComment) {
if (resolveComment.isSome()) { combined.push(this.commentViewToCombined(resolveComment));
combined.push(this.commentViewToCombined(resolveComment.unwrap()));
} }
let resolvePost = res.post; let resolvePost = resolveRes.post;
if (resolvePost.isSome()) { if (resolvePost) {
combined.push(this.postViewToCombined(resolvePost.unwrap())); combined.push(this.postViewToCombined(resolvePost));
} }
let resolveCommunity = res.community; let resolveCommunity = resolveRes.community;
if (resolveCommunity.isSome()) { if (resolveCommunity) {
combined.push( combined.push(this.communityViewToCombined(resolveCommunity));
this.communityViewToCombined(resolveCommunity.unwrap()) }
); let resolveUser = resolveRes.person;
if (resolveUser) {
combined.push(this.personViewSafeToCombined(resolveUser));
} }
let resolveUser = res.person;
if (resolveUser.isSome()) {
combined.push(this.personViewSafeToCombined(resolveUser.unwrap()));
} }
},
none: void 0,
});
// Push the search results // Push the search results
this.state.searchResponse.match({ let searchRes = this.state.searchResponse;
some: res => { if (searchRes) {
pushNotNull( pushNotNull(
combined, combined,
res.comments?.map(e => this.commentViewToCombined(e)) searchRes.comments?.map(e => this.commentViewToCombined(e))
); );
pushNotNull( pushNotNull(
combined, combined,
res.posts?.map(e => this.postViewToCombined(e)) searchRes.posts?.map(e => this.postViewToCombined(e))
); );
pushNotNull( pushNotNull(
combined, combined,
res.communities?.map(e => this.communityViewToCombined(e)) searchRes.communities?.map(e => this.communityViewToCombined(e))
); );
pushNotNull( pushNotNull(
combined, combined,
res.users?.map(e => this.personViewSafeToCombined(e)) searchRes.users?.map(e => this.personViewSafeToCombined(e))
); );
}, }
none: void 0,
});
// Sort it // Sort it
if (this.state.sort == SortType.New) { if (this.state.sort == SortType.New) {
@ -588,9 +546,6 @@ export class Search extends Component<any, SearchState> {
<PostListing <PostListing
key={(i.data as PostView).post.id} key={(i.data as PostView).post.id}
post_view={i.data as PostView} post_view={i.data as PostView}
duplicates={None}
moderators={None}
admins={None}
showCommunity showCommunity
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
@ -611,9 +566,6 @@ export class Search extends Component<any, SearchState> {
]} ]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
viewOnly viewOnly
moderators={None}
admins={None}
maxCommentsShown={None}
locked locked
noIndent noIndent
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
@ -636,15 +588,8 @@ export class Search extends Component<any, SearchState> {
comments() { comments() {
let comments: CommentView[] = []; let comments: CommentView[] = [];
pushNotNull(comments, this.state.resolveObjectResponse?.comment);
this.state.resolveObjectResponse.match({ pushNotNull(comments, this.state.searchResponse?.comments);
some: res => pushNotNull(comments, res.comment),
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(comments, res.comments),
none: void 0,
});
return ( return (
<CommentNodes <CommentNodes
@ -653,9 +598,6 @@ export class Search extends Component<any, SearchState> {
viewOnly viewOnly
locked locked
noIndent noIndent
moderators={None}
admins={None}
maxCommentsShown={None}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
@ -666,14 +608,8 @@ export class Search extends Component<any, SearchState> {
posts() { posts() {
let posts: PostView[] = []; let posts: PostView[] = [];
this.state.resolveObjectResponse.match({ pushNotNull(posts, this.state.resolveObjectResponse?.post);
some: res => pushNotNull(posts, res.post), pushNotNull(posts, this.state.searchResponse?.posts);
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(posts, res.posts),
none: void 0,
});
return ( return (
<> <>
@ -683,9 +619,6 @@ export class Search extends Component<any, SearchState> {
<PostListing <PostListing
post_view={pv} post_view={pv}
showCommunity showCommunity
duplicates={None}
moderators={None}
admins={None}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
@ -702,14 +635,8 @@ export class Search extends Component<any, SearchState> {
communities() { communities() {
let communities: CommunityView[] = []; let communities: CommunityView[] = [];
this.state.resolveObjectResponse.match({ pushNotNull(communities, this.state.resolveObjectResponse?.community);
some: res => pushNotNull(communities, res.community), pushNotNull(communities, this.state.searchResponse?.communities);
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(communities, res.communities),
none: void 0,
});
return ( return (
<> <>
@ -725,14 +652,8 @@ export class Search extends Component<any, SearchState> {
users() { users() {
let users: PersonViewSafe[] = []; let users: PersonViewSafe[] = [];
this.state.resolveObjectResponse.match({ pushNotNull(users, this.state.resolveObjectResponse?.person);
some: res => pushNotNull(users, res.person), pushNotNull(users, this.state.searchResponse?.users);
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(users, res.users),
none: void 0,
});
return ( return (
<> <>
@ -800,6 +721,7 @@ export class Search extends Component<any, SearchState> {
} }
creatorFilter() { creatorFilter() {
let creatorPv = this.state.creatorDetails?.person_view;
return ( return (
<div className="form-group col-sm-6"> <div className="form-group col-sm-6">
<label className="col-form-label" htmlFor="creator-filter"> <label className="col-form-label" htmlFor="creator-filter">
@ -812,14 +734,11 @@ export class Search extends Component<any, SearchState> {
value={this.state.creatorId} value={this.state.creatorId}
> >
<option value="0">{i18n.t("all")}</option> <option value="0">{i18n.t("all")}</option>
{this.state.creatorDetails.match({ {creatorPv && (
some: creator => ( <option value={creatorPv.person.id}>
<option value={creator.person_view.person.id}> {personSelectName(creatorPv)}
{personSelectName(creator.person_view)}
</option> </option>
), )}
none: <></>,
})}
</select> </select>
</div> </div>
</div> </div>
@ -827,19 +746,24 @@ export class Search extends Component<any, SearchState> {
} }
resultsCount(): number { resultsCount(): number {
let searchCount = this.state.searchResponse let r = this.state.searchResponse;
.map(
r => let searchCount = r
r.posts?.length + ? r.posts?.length +
r.comments?.length + r.comments?.length +
r.communities?.length + r.communities?.length +
r.users?.length r.users?.length
) : 0;
.unwrapOr(0);
let resObjCount = this.state.resolveObjectResponse let resolveRes = this.state.resolveObjectResponse;
.map(r => (r.post || r.person || r.community || r.comment ? 1 : 0)) let resObjCount = resolveRes
.unwrapOr(0); ? resolveRes.post ||
resolveRes.person ||
resolveRes.community ||
resolveRes.comment
? 1
: 0
: 0;
return resObjCount + searchCount; return resObjCount + searchCount;
} }
@ -849,33 +773,33 @@ export class Search extends Component<any, SearchState> {
} }
search() { search() {
let community_id: Option<number> = let community_id =
this.state.communityId == 0 ? None : Some(this.state.communityId); this.state.communityId == 0 ? undefined : this.state.communityId;
let creator_id: Option<number> = let creator_id =
this.state.creatorId == 0 ? None : Some(this.state.creatorId); this.state.creatorId == 0 ? undefined : this.state.creatorId;
let form = new SearchForm({ let auth = myAuth(false);
let form: SearchForm = {
q: this.state.q, q: this.state.q,
community_id, community_id,
community_name: None,
creator_id, creator_id,
type_: Some(this.state.type_), type_: this.state.type_,
sort: Some(this.state.sort), sort: this.state.sort,
listing_type: Some(this.state.listingType), listing_type: this.state.listingType,
page: Some(this.state.page), page: this.state.page,
limit: Some(fetchLimit), limit: fetchLimit,
auth: auth(false).ok(), auth,
}); };
let resolveObjectForm = new ResolveObject({ let resolveObjectForm: ResolveObject = {
q: this.state.q, q: this.state.q,
auth: auth(false).ok(), auth,
}); };
if (this.state.q != "") { if (this.state.q != "") {
this.setState({ this.setState({
searchResponse: None, searchResponse: undefined,
resolveObjectResponse: None, resolveObjectResponse: undefined,
loading: true, loading: true,
}); });
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
@ -1019,12 +943,7 @@ export class Search extends Component<any, SearchState> {
if (msg.error) { if (msg.error) {
if (msg.error == "couldnt_find_object") { if (msg.error == "couldnt_find_object") {
this.setState({ this.setState({
resolveObjectResponse: Some({ resolveObjectResponse: {},
comment: None,
post: None,
community: None,
person: None,
}),
}); });
this.checkFinishedLoading(); this.checkFinishedLoading();
} else { } else {
@ -1032,44 +951,35 @@ export class Search extends Component<any, SearchState> {
return; return;
} }
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg);
this.setState({ searchResponse: Some(data) }); this.setState({ searchResponse: data });
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.checkFinishedLoading(); this.checkFinishedLoading();
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes( createCommentLikeRes(
data.comment_view, data.comment_view,
this.state.searchResponse.map(r => r.comments).unwrapOr([]) this.state.searchResponse?.comments
); );
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes( createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
data.post_view,
this.state.searchResponse.map(r => r.posts).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>( let data = wsJsonToRes<ListCommunitiesResponse>(msg);
msg,
ListCommunitiesResponse
);
this.setState({ communities: data.communities }); this.setState({ communities: data.communities });
this.setupCommunityFilter(); this.setupCommunityFilter();
} else if (op == UserOperation.ResolveObject) { } else if (op == UserOperation.ResolveObject) {
let data = wsJsonToRes<ResolveObjectResponse>(msg, ResolveObjectResponse); let data = wsJsonToRes<ResolveObjectResponse>(msg);
this.setState({ resolveObjectResponse: Some(data) }); this.setState({ resolveObjectResponse: data });
this.checkFinishedLoading(); this.checkFinishedLoading();
} }
} }
checkFinishedLoading() { checkFinishedLoading() {
if ( if (this.state.searchResponse && this.state.resolveObjectResponse) {
this.state.searchResponse.isSome() &&
this.state.resolveObjectResponse.isSome()
) {
this.setState({ loading: false }); this.setState({ loading: false });
} }
} }

View file

@ -1,4 +1,3 @@
import { Either, Option } from "@sniptt/monads";
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
/** /**
@ -22,16 +21,16 @@ declare global {
} }
export interface InitialFetchRequest { export interface InitialFetchRequest {
auth: Option<string>; auth?: string;
client: LemmyHttp; client: LemmyHttp;
path: string; path: string;
} }
export interface PostFormParams { export interface PostFormParams {
name: Option<string>; name?: string;
url: Option<string>; url?: string;
body: Option<string>; body?: string;
nameOrId: Option<Either<string, number>>; nameOrId?: string | number;
} }
export enum CommentViewType { export enum CommentViewType {

View file

@ -1,3 +1,4 @@
import { Inferno } from "inferno";
import { IRouteProps } from "inferno-router/dist/Route"; import { IRouteProps } from "inferno-router/dist/Route";
import { Communities } from "./components/community/communities"; import { Communities } from "./components/community/communities";
import { Community } from "./components/community/community"; import { Community } from "./components/community/community";
@ -24,6 +25,8 @@ import { Search } from "./components/search";
import { InitialFetchRequest } from "./interfaces"; import { InitialFetchRequest } from "./interfaces";
interface IRoutePropsWithFetch extends IRouteProps { interface IRoutePropsWithFetch extends IRouteProps {
// TODO Make sure this one is good.
component: Inferno.ComponentClass;
fetchInitialData?(req: InitialFetchRequest): Promise<any>[]; fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
} }

View file

@ -1,5 +1,4 @@
// import Cookies from 'js-cookie'; // import Cookies from 'js-cookie';
import { Err, None, Ok, Option, Result, Some } from "@sniptt/monads";
import IsomorphicCookie from "isomorphic-cookie"; import IsomorphicCookie from "isomorphic-cookie";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { LoginResponse, MyUserInfo } from "lemmy-js-client"; import { LoginResponse, MyUserInfo } from "lemmy-js-client";
@ -21,8 +20,8 @@ interface JwtInfo {
export class UserService { export class UserService {
private static _instance: UserService; private static _instance: UserService;
public myUserInfo: Option<MyUserInfo> = None; public myUserInfo?: MyUserInfo;
public jwtInfo: Option<JwtInfo> = None; public jwtInfo?: JwtInfo;
public unreadInboxCountSub: BehaviorSubject<number> = public unreadInboxCountSub: BehaviorSubject<number> =
new BehaviorSubject<number>(0); new BehaviorSubject<number>(0);
public unreadReportCountSub: BehaviorSubject<number> = public unreadReportCountSub: BehaviorSubject<number> =
@ -37,45 +36,41 @@ export class UserService {
public login(res: LoginResponse) { public login(res: LoginResponse) {
let expires = new Date(); let expires = new Date();
expires.setDate(expires.getDate() + 365); expires.setDate(expires.getDate() + 365);
res.jwt.match({ if (res.jwt) {
some: jwt => {
toast(i18n.t("logged_in")); toast(i18n.t("logged_in"));
IsomorphicCookie.save("jwt", jwt, { expires, secure: isHttps }); IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps });
this.setJwtInfo(); this.setJwtInfo();
}, }
none: void 0,
});
} }
public logout() { public logout() {
this.jwtInfo = None; this.jwtInfo = undefined;
this.myUserInfo = None; this.myUserInfo = undefined;
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.hostname; document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.hostname;
location.reload(); location.reload();
} }
public auth(throwErr = true): Result<string, string> { public auth(throwErr = true): string | undefined {
// Can't use match to convert to result for some reason let jwt = this.jwtInfo?.jwt;
let jwt = this.jwtInfo.map(j => j.jwt); if (jwt) {
if (jwt.isSome()) { return jwt;
return Ok(jwt.unwrap());
} else { } else {
let msg = "No JWT cookie found"; let msg = "No JWT cookie found";
if (throwErr && isBrowser()) { if (throwErr && isBrowser()) {
console.log(msg); console.error(msg);
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
} }
return Err(msg); return undefined;
// throw msg;
} }
} }
private setJwtInfo() { private setJwtInfo() {
let jwt = IsomorphicCookie.load("jwt"); let jwt: string | undefined = IsomorphicCookie.load("jwt");
if (jwt) { if (jwt) {
let jwtInfo: JwtInfo = { jwt, claims: jwt_decode(jwt) }; this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
this.jwtInfo = Some(jwtInfo);
} }
} }

View file

@ -1,5 +1,3 @@
import { Err, None, Ok, Option, Result, Some } from "@sniptt/monads";
import { ClassConstructor, deserialize, serialize } from "class-transformer";
import emojiShortName from "emoji-short-name"; import emojiShortName from "emoji-short-name";
import { import {
BlockCommunityResponse, BlockCommunityResponse,
@ -9,7 +7,6 @@ import {
CommentReportView, CommentReportView,
CommentSortType, CommentSortType,
CommentView, CommentView,
CommunityBlockView,
CommunityModeratorView, CommunityModeratorView,
CommunityView, CommunityView,
GetSiteMetadata, GetSiteMetadata,
@ -18,8 +15,6 @@ import {
LemmyHttp, LemmyHttp,
LemmyWebsocket, LemmyWebsocket,
ListingType, ListingType,
MyUserInfo,
PersonBlockView,
PersonSafe, PersonSafe,
PersonViewSafe, PersonViewSafe,
PostReportView, PostReportView,
@ -30,7 +25,6 @@ import {
Search, Search,
SearchType, SearchType,
SortType, SortType,
toUndefined,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { default as MarkdownIt } from "markdown-it"; import { default as MarkdownIt } from "markdown-it";
import markdown_it_container from "markdown-it-container"; import markdown_it_container from "markdown-it-container";
@ -190,11 +184,11 @@ export function mdToHtmlInline(text: string) {
return { __html: md.renderInline(text) }; return { __html: md.renderInline(text) };
} }
export function getUnixTime(text: string): number { export function getUnixTime(text?: string): number | undefined {
return text ? new Date(text).getTime() / 1000 : undefined; return text ? new Date(text).getTime() / 1000 : undefined;
} }
export function futureDaysToUnixTime(days: number): number { export function futureDaysToUnixTime(days?: number): number | undefined {
return days return days
? Math.trunc( ? Math.trunc(
new Date(Date.now() + 1000 * 60 * 60 * 24 * days).getTime() / 1000 new Date(Date.now() + 1000 * 60 * 60 * 24 * days).getTime() / 1000
@ -203,22 +197,21 @@ export function futureDaysToUnixTime(days: number): number {
} }
export function canMod( export function canMod(
mods: Option<CommunityModeratorView[]>,
admins: Option<PersonViewSafe[]>,
creator_id: number, creator_id: number,
mods?: CommunityModeratorView[],
admins?: PersonViewSafe[],
myUserInfo = UserService.Instance.myUserInfo, myUserInfo = UserService.Instance.myUserInfo,
onSelf = false onSelf = false
): boolean { ): boolean {
// You can do moderator actions only on the mods added after you. // You can do moderator actions only on the mods added after you.
let adminsThenMods = admins let adminsThenMods =
.unwrapOr([]) admins
.map(a => a.person.id) ?.map(a => a.person.id)
.concat(mods.unwrapOr([]).map(m => m.moderator.id)); .concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
return myUserInfo.match({ if (myUserInfo) {
some: me => {
let myIndex = adminsThenMods.findIndex( let myIndex = adminsThenMods.findIndex(
id => id == me.local_user_view.person.id id => id == myUserInfo.local_user_view.person.id
); );
if (myIndex == -1) { if (myIndex == -1) {
return false; return false;
@ -227,108 +220,66 @@ export function canMod(
adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1)); adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1));
return !adminsThenMods.includes(creator_id); return !adminsThenMods.includes(creator_id);
} }
}, } else {
none: false, return false;
}); }
} }
export function canAdmin( export function canAdmin(
admins: Option<PersonViewSafe[]>, creatorId: number,
creator_id: number, admins?: PersonViewSafe[],
myUserInfo = UserService.Instance.myUserInfo, myUserInfo = UserService.Instance.myUserInfo,
onSelf = false onSelf = false
): boolean { ): boolean {
return canMod(None, admins, creator_id, myUserInfo, onSelf); return canMod(creatorId, undefined, admins, myUserInfo, onSelf);
} }
export function isMod( export function isMod(
mods: Option<CommunityModeratorView[]>, creatorId: number,
creator_id: number mods?: CommunityModeratorView[]
): boolean { ): boolean {
return mods.match({ return mods?.map(m => m.moderator.id).includes(creatorId) ?? false;
some: mods => mods.map(m => m.moderator.id).includes(creator_id),
none: false,
});
} }
export function amMod( export function amMod(
mods: Option<CommunityModeratorView[]>, mods?: CommunityModeratorView[],
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return myUserInfo.match({ return myUserInfo ? isMod(myUserInfo.local_user_view.person.id, mods) : false;
some: mui => isMod(mods, mui.local_user_view.person.id),
none: false,
});
} }
export function isAdmin( export function isAdmin(creatorId: number, admins?: PersonViewSafe[]): boolean {
admins: Option<PersonViewSafe[]>, return admins?.map(a => a.person.id).includes(creatorId) ?? false;
creator_id: number
): boolean {
return admins.match({
some: admins => admins.map(a => a.person.id).includes(creator_id),
none: false,
});
} }
export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean { export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean {
return myUserInfo return myUserInfo?.local_user_view.person.admin ?? false;
.map(mui => mui.local_user_view.person.admin)
.unwrapOr(false);
} }
export function amCommunityCreator( export function amCommunityCreator(
mods: Option<CommunityModeratorView[]>,
creator_id: number, creator_id: number,
mods?: CommunityModeratorView[],
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return mods.match({ let myId = myUserInfo?.local_user_view.person.id;
some: mods =>
myUserInfo
.map(mui => mui.local_user_view.person.id)
.match({
some: myId =>
myId == mods[0].moderator.id &&
// Don't allow mod actions on yourself // Don't allow mod actions on yourself
myId != creator_id, return myId == mods?.at(0)?.moderator.id && myId != creator_id;
none: false,
}),
none: false,
});
} }
export function amSiteCreator( export function amSiteCreator(
admins: Option<PersonViewSafe[]>,
creator_id: number, creator_id: number,
admins?: PersonViewSafe[],
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return admins.match({ let myId = myUserInfo?.local_user_view.person.id;
some: admins => return myId == admins?.at(0)?.person.id && myId != creator_id;
myUserInfo
.map(mui => mui.local_user_view.person.id)
.match({
some: myId =>
myId == admins[0].person.id &&
// Don't allow mod actions on yourself
myId != creator_id,
none: false,
}),
none: false,
});
} }
export function amTopMod( export function amTopMod(
mods: Option<CommunityModeratorView[]>, mods: CommunityModeratorView[],
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return mods.match({ return mods.at(0)?.moderator.id == myUserInfo?.local_user_view.person.id;
some: mods =>
myUserInfo.match({
some: mui => mods[0].moderator.id == mui.local_user_view.person.id,
none: false,
}),
none: false,
});
} }
const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/; const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
@ -386,9 +337,7 @@ export function routeSearchTypeToEnum(type: string): SearchType {
} }
export async function getSiteMetadata(url: string) { export async function getSiteMetadata(url: string) {
let form = new GetSiteMetadata({ let form: GetSiteMetadata = { url };
url,
});
let client = new LemmyHttp(httpBase); let client = new LemmyHttp(httpBase);
return client.getSiteMetadata(form); return client.getSiteMetadata(form);
} }
@ -438,10 +387,8 @@ export function getLanguages(
override?: string, override?: string,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): string[] { ): string[] {
let myLang = myUserInfo let myLang = myUserInfo?.local_user_view.local_user.interface_language;
.map(m => m.local_user_view.local_user.interface_language) let lang = override || myLang || "browser";
.unwrapOr("browser");
let lang = override || myLang;
if (lang == "browser" && isBrowser()) { if (lang == "browser" && isBrowser()) {
return getBrowserLanguages(); return getBrowserLanguages();
@ -496,7 +443,7 @@ export async function setTheme(theme: string, forceReload = false) {
let cssLoc = `/css/themes/${theme}.css`; let cssLoc = `/css/themes/${theme}.css`;
loadCss(theme, cssLoc); loadCss(theme, cssLoc);
document.getElementById(theme).removeAttribute("disabled"); document.getElementById(theme)?.removeAttribute("disabled");
} }
export function loadCss(id: string, loc: string) { export function loadCss(id: string, loc: string) {
@ -521,19 +468,15 @@ export function objectFlip(obj: any) {
} }
export function showAvatars( export function showAvatars(
myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return myUserInfo return myUserInfo?.local_user_view.local_user.show_avatars ?? true;
.map(m => m.local_user_view.local_user.show_avatars)
.unwrapOr(true);
} }
export function showScores( export function showScores(
myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return myUserInfo return myUserInfo?.local_user_view.local_user.show_scores ?? true;
.map(m => m.local_user_view.local_user.show_scores)
.unwrapOr(true);
} }
export function isCakeDay(published: string): boolean { export function isCakeDay(published: string): boolean {
@ -595,9 +538,9 @@ export function pictrsDeleteToast(
interface NotifyInfo { interface NotifyInfo {
name: string; name: string;
icon: Option<string>; icon?: string;
link: string; link: string;
body: string; body?: string;
} }
export function messageToastify(info: NotifyInfo, router: any) { export function messageToastify(info: NotifyInfo, router: any) {
@ -607,7 +550,7 @@ export function messageToastify(info: NotifyInfo, router: any) {
let toast = Toastify({ let toast = Toastify({
text: `${htmlBody}<br />${info.name}`, text: `${htmlBody}<br />${info.name}`,
avatar: toUndefined(info.icon), avatar: info.icon,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
className: "text-dark", className: "text-dark",
close: true, close: true,
@ -663,7 +606,7 @@ function notify(info: NotifyInfo, router: any) {
else { else {
var notification = new Notification(info.name, { var notification = new Notification(info.name, {
...{ body: info.body }, ...{ body: info.body },
...(info.icon.isSome() && { icon: info.icon.unwrap() }), ...(info.icon && { icon: info.icon }),
}); });
notification.onclick = (ev: Event): any => { notification.onclick = (ev: Event): any => {
@ -790,15 +733,12 @@ export function getListingTypeFromProps(
defaultListingType: ListingType, defaultListingType: ListingType,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): ListingType { ): ListingType {
let myLt = myUserInfo?.local_user_view.local_user.default_listing_type;
return props.match.params.listing_type return props.match.params.listing_type
? routeListingTypeToEnum(props.match.params.listing_type) ? routeListingTypeToEnum(props.match.params.listing_type)
: myUserInfo.match({ : myLt
some: me => ? Object.values(ListingType)[myLt]
Object.values(ListingType)[ : defaultListingType;
me.local_user_view.local_user.default_listing_type
],
none: defaultListingType,
});
} }
export function getListingTypeFromPropsNoDefault(props: any): ListingType { export function getListingTypeFromPropsNoDefault(props: any): ListingType {
@ -817,15 +757,12 @@ export function getSortTypeFromProps(
props: any, props: any,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): SortType { ): SortType {
let mySortType = myUserInfo?.local_user_view.local_user.default_sort_type;
return props.match.params.sort return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort) ? routeSortTypeToEnum(props.match.params.sort)
: myUserInfo.match({ : mySortType
some: mui => ? Object.values(SortType)[mySortType]
Object.values(SortType)[ : SortType.Active;
mui.local_user_view.local_user.default_sort_type
],
none: SortType.Active,
});
} }
export function getPageFromProps(props: any): number { export function getPageFromProps(props: any): number {
@ -838,22 +775,22 @@ export function getRecipientIdFromProps(props: any): number {
: 1; : 1;
} }
export function getIdFromProps(props: any): Option<number> { export function getIdFromProps(props: any): number | undefined {
let id: string = props.match.params.post_id; let id = props.match.params.post_id;
return id ? Some(Number(id)) : None; return id ? Number(id) : undefined;
} }
export function getCommentIdFromProps(props: any): Option<number> { export function getCommentIdFromProps(props: any): number | undefined {
let id: string = props.match.params.comment_id; let id = props.match.params.comment_id;
return id ? Some(Number(id)) : None; return id ? Number(id) : undefined;
} }
export function getUsernameFromProps(props: any): string { export function getUsernameFromProps(props: any): string {
return props.match.params.username; return props.match.params.username;
} }
export function editCommentRes(data: CommentView, comments: CommentView[]) { export function editCommentRes(data: CommentView, comments?: CommentView[]) {
let found = comments.find(c => c.comment.id == data.comment.id); let found = comments?.find(c => c.comment.id == data.comment.id);
if (found) { if (found) {
found.comment.content = data.comment.content; found.comment.content = data.comment.content;
found.comment.distinguished = data.comment.distinguished; found.comment.distinguished = data.comment.distinguished;
@ -866,20 +803,19 @@ export function editCommentRes(data: CommentView, comments: CommentView[]) {
} }
} }
export function saveCommentRes(data: CommentView, comments: CommentView[]) { export function saveCommentRes(data: CommentView, comments?: CommentView[]) {
let found = comments.find(c => c.comment.id == data.comment.id); let found = comments?.find(c => c.comment.id == data.comment.id);
if (found) { if (found) {
found.saved = data.saved; found.saved = data.saved;
} }
} }
// TODO Should only use the return now, no state?
export function updatePersonBlock( export function updatePersonBlock(
data: BlockPersonResponse, data: BlockPersonResponse,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): Option<PersonBlockView[]> { ) {
return myUserInfo.match({ let mui = myUserInfo;
some: (mui: MyUserInfo) => { if (mui) {
if (data.blocked) { if (data.blocked) {
mui.person_blocks.push({ mui.person_blocks.push({
person: mui.local_user_view.person, person: mui.local_user_view.person,
@ -892,18 +828,15 @@ export function updatePersonBlock(
); );
toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`); toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
} }
return Some(mui.person_blocks); }
},
none: None,
});
} }
export function updateCommunityBlock( export function updateCommunityBlock(
data: BlockCommunityResponse, data: BlockCommunityResponse,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): Option<CommunityBlockView[]> { ) {
return myUserInfo.match({ let mui = myUserInfo;
some: (mui: MyUserInfo) => { if (mui) {
if (data.blocked) { if (data.blocked) {
mui.community_blocks.push({ mui.community_blocks.push({
person: mui.local_user_view.person, person: mui.local_user_view.person,
@ -916,17 +849,14 @@ export function updateCommunityBlock(
); );
toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`); toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
} }
return Some(mui.community_blocks); }
},
none: None,
});
} }
export function createCommentLikeRes( export function createCommentLikeRes(
data: CommentView, data: CommentView,
comments: CommentView[] comments?: CommentView[]
) { ) {
let found = comments.find(c => c.comment.id === data.comment.id); let found = comments?.find(c => c.comment.id === data.comment.id);
if (found) { if (found) {
found.counts.score = data.counts.score; found.counts.score = data.counts.score;
found.counts.upvotes = data.counts.upvotes; found.counts.upvotes = data.counts.upvotes;
@ -937,14 +867,14 @@ export function createCommentLikeRes(
} }
} }
export function createPostLikeFindRes(data: PostView, posts: PostView[]) { export function createPostLikeFindRes(data: PostView, posts?: PostView[]) {
let found = posts.find(p => p.post.id == data.post.id); let found = posts?.find(p => p.post.id == data.post.id);
if (found) { if (found) {
createPostLikeRes(data, found); createPostLikeRes(data, found);
} }
} }
export function createPostLikeRes(data: PostView, post_view: PostView) { export function createPostLikeRes(data: PostView, post_view?: PostView) {
if (post_view) { if (post_view) {
post_view.counts.score = data.counts.score; post_view.counts.score = data.counts.score;
post_view.counts.upvotes = data.counts.upvotes; post_view.counts.upvotes = data.counts.upvotes;
@ -955,8 +885,8 @@ export function createPostLikeRes(data: PostView, post_view: PostView) {
} }
} }
export function editPostFindRes(data: PostView, posts: PostView[]) { export function editPostFindRes(data: PostView, posts?: PostView[]) {
let found = posts.find(p => p.post.id == data.post.id); let found = posts?.find(p => p.post.id == data.post.id);
if (found) { if (found) {
editPostRes(data, found); editPostRes(data, found);
} }
@ -980,9 +910,9 @@ export function editPostRes(data: PostView, post: PostView) {
// TODO possible to make these generic? // TODO possible to make these generic?
export function updatePostReportRes( export function updatePostReportRes(
data: PostReportView, data: PostReportView,
reports: PostReportView[] reports?: PostReportView[]
) { ) {
let found = reports.find(p => p.post_report.id == data.post_report.id); let found = reports?.find(p => p.post_report.id == data.post_report.id);
if (found) { if (found) {
found.post_report = data.post_report; found.post_report = data.post_report;
} }
@ -990,9 +920,9 @@ export function updatePostReportRes(
export function updateCommentReportRes( export function updateCommentReportRes(
data: CommentReportView, data: CommentReportView,
reports: CommentReportView[] reports?: CommentReportView[]
) { ) {
let found = reports.find(c => c.comment_report.id == data.comment_report.id); let found = reports?.find(c => c.comment_report.id == data.comment_report.id);
if (found) { if (found) {
found.comment_report = data.comment_report; found.comment_report = data.comment_report;
} }
@ -1000,9 +930,9 @@ export function updateCommentReportRes(
export function updatePrivateMessageReportRes( export function updatePrivateMessageReportRes(
data: PrivateMessageReportView, data: PrivateMessageReportView,
reports: PrivateMessageReportView[] reports?: PrivateMessageReportView[]
) { ) {
let found = reports.find( let found = reports?.find(
c => c.private_message_report.id == data.private_message_report.id c => c.private_message_report.id == data.private_message_report.id
); );
if (found) { if (found) {
@ -1012,9 +942,9 @@ export function updatePrivateMessageReportRes(
export function updateRegistrationApplicationRes( export function updateRegistrationApplicationRes(
data: RegistrationApplicationView, data: RegistrationApplicationView,
applications: RegistrationApplicationView[] applications?: RegistrationApplicationView[]
) { ) {
let found = applications.find( let found = applications?.find(
ra => ra.registration_application.id == data.registration_application.id ra => ra.registration_application.id == data.registration_application.id
); );
if (found) { if (found) {
@ -1057,13 +987,15 @@ export function buildCommentsTree(
let map = new Map<number, CommentNodeI>(); let map = new Map<number, CommentNodeI>();
let depthOffset = !parentComment let depthOffset = !parentComment
? 0 ? 0
: getDepthFromComment(comments[0].comment); : getDepthFromComment(comments[0].comment) ?? 0;
for (let comment_view of comments) { for (let comment_view of comments) {
let depthI = getDepthFromComment(comment_view.comment) ?? 0;
let depth = depthI ? depthI - depthOffset : 0;
let node: CommentNodeI = { let node: CommentNodeI = {
comment_view: comment_view, comment_view,
children: [], children: [],
depth: getDepthFromComment(comment_view.comment) - depthOffset, depth,
}; };
map.set(comment_view.comment.id, { ...node }); map.set(comment_view.comment.id, { ...node });
} }
@ -1072,45 +1004,46 @@ export function buildCommentsTree(
// if its a parent comment fetch, then push the first comment to the top node. // if its a parent comment fetch, then push the first comment to the top node.
if (parentComment) { if (parentComment) {
tree.push(map.get(comments[0].comment.id)); let cNode = map.get(comments[0].comment.id);
if (cNode) {
tree.push(cNode);
}
} }
for (let comment_view of comments) { for (let comment_view of comments) {
let child = map.get(comment_view.comment.id); let child = map.get(comment_view.comment.id);
if (child) {
let parent_id = getCommentParentId(comment_view.comment); let parent_id = getCommentParentId(comment_view.comment);
parent_id.match({ if (parent_id) {
some: parentId => { let parent = map.get(parent_id);
let parent = map.get(parentId);
// Necessary because blocked comment might not exist // Necessary because blocked comment might not exist
if (parent) { if (parent) {
parent.children.push(child); parent.children.push(child);
} }
}, } else {
none: () => {
if (!parentComment) { if (!parentComment) {
tree.push(child); tree.push(child);
} }
}, }
}); }
} }
return tree; return tree;
} }
export function getCommentParentId(comment: CommentI): Option<number> { export function getCommentParentId(comment?: CommentI): number | undefined {
let split = comment.path.split("."); let split = comment?.path.split(".");
// remove the 0 // remove the 0
split.shift(); split?.shift();
if (split.length > 1) { return split && split.length > 1
return Some(Number(split[split.length - 2])); ? Number(split.at(split.length - 2))
} else { : undefined;
return None;
}
} }
export function getDepthFromComment(comment: CommentI): number { export function getDepthFromComment(comment?: CommentI): number | undefined {
return comment.path.split(".").length - 2; let len = comment?.path.split(".").length;
return len ? len - 2 : undefined;
} }
export function insertCommentIntoTree( export function insertCommentIntoTree(
@ -1125,43 +1058,36 @@ export function insertCommentIntoTree(
depth: 0, depth: 0,
}; };
getCommentParentId(cv.comment).match({ let parentId = getCommentParentId(cv.comment);
some: parentId => { if (parentId) {
let parentComment = searchCommentTree(tree, parentId); let parent_comment = searchCommentTree(tree, parentId);
parentComment.match({ if (parent_comment) {
some: pComment => { node.depth = parent_comment.depth + 1;
node.depth = pComment.depth + 1; parent_comment.children.unshift(node);
pComment.children.unshift(node); }
}, } else if (!parentComment) {
none: void 0,
});
},
none: () => {
if (!parentComment) {
tree.unshift(node); tree.unshift(node);
} }
},
});
} }
export function searchCommentTree( export function searchCommentTree(
tree: CommentNodeI[], tree: CommentNodeI[],
id: number id: number
): Option<CommentNodeI> { ): CommentNodeI | undefined {
for (let node of tree) { for (let node of tree) {
if (node.comment_view.comment.id === id) { if (node.comment_view.comment.id === id) {
return Some(node); return node;
} }
for (const child of node.children) { for (const child of node.children) {
let res = searchCommentTree([child], id); let res = searchCommentTree([child], id);
if (res.isSome()) { if (res) {
return res; return res;
} }
} }
} }
return None; return undefined;
} }
export const colorList: string[] = [ export const colorList: string[] = [
@ -1209,55 +1135,23 @@ export function isBrowser() {
return typeof window !== "undefined"; return typeof window !== "undefined";
} }
export function setIsoData<Type1, Type2, Type3, Type4, Type5>( export function setIsoData(context: any): IsoData {
context: any,
cls1?: ClassConstructor<Type1>,
cls2?: ClassConstructor<Type2>,
cls3?: ClassConstructor<Type3>,
cls4?: ClassConstructor<Type4>,
cls5?: ClassConstructor<Type5>
): IsoData {
// If its the browser, you need to deserialize the data from the window // If its the browser, you need to deserialize the data from the window
if (isBrowser()) { if (isBrowser()) {
let json = window.isoData; let json = window.isoData;
let routeData = json.routeData; let routeData = json.routeData;
let routeDataOut: any[] = []; let site_res = json.site_res;
// Can't do array looping because of specific type constructor required
if (routeData[0]) {
routeDataOut[0] = convertWindowJson(cls1, routeData[0]);
}
if (routeData[1]) {
routeDataOut[1] = convertWindowJson(cls2, routeData[1]);
}
if (routeData[2]) {
routeDataOut[2] = convertWindowJson(cls3, routeData[2]);
}
if (routeData[3]) {
routeDataOut[3] = convertWindowJson(cls4, routeData[3]);
}
if (routeData[4]) {
routeDataOut[4] = convertWindowJson(cls5, routeData[4]);
}
let site_res = convertWindowJson(GetSiteResponse, json.site_res);
let isoData: IsoData = { let isoData: IsoData = {
path: json.path, path: json.path,
site_res, site_res,
routeData: routeDataOut, routeData,
}; };
return isoData; return isoData;
} else return context.router.staticContext; } else return context.router.staticContext;
} }
/** export function wsSubscribe(parseMessage: any): Subscription | undefined {
* Necessary since window ISOData can't store function types like Option
*/
export function convertWindowJson<T>(cls: ClassConstructor<T>, data: any): T {
return deserialize(cls, serialize(data));
}
export function wsSubscribe(parseMessage: any): Subscription {
if (isBrowser()) { if (isBrowser()) {
return WebSocketService.Instance.subject return WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@ -1267,7 +1161,7 @@ export function wsSubscribe(parseMessage: any): Subscription {
() => console.log("complete") () => console.log("complete")
); );
} else { } else {
return null; return undefined;
} }
} }
@ -1305,9 +1199,8 @@ export function restoreScrollPosition(context: any) {
} }
export function showLocal(isoData: IsoData): boolean { export function showLocal(isoData: IsoData): boolean {
return isoData.site_res.federated_instances let linked = isoData.site_res.federated_instances?.linked;
.map(f => f.linked.length > 0) return linked ? linked.length > 0 : false;
.unwrapOr(false);
} }
export interface ChoicesValue { export interface ChoicesValue {
@ -1332,35 +1225,29 @@ export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
} }
export async function fetchCommunities(q: string) { export async function fetchCommunities(q: string) {
let form = new Search({ let form: Search = {
q, q,
type_: Some(SearchType.Communities), type_: SearchType.Communities,
sort: Some(SortType.TopAll), sort: SortType.TopAll,
listing_type: Some(ListingType.All), listing_type: ListingType.All,
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
community_id: None, auth: myAuth(false),
community_name: None, };
creator_id: None,
auth: auth(false).ok(),
});
let client = new LemmyHttp(httpBase); let client = new LemmyHttp(httpBase);
return client.search(form); return client.search(form);
} }
export async function fetchUsers(q: string) { export async function fetchUsers(q: string) {
let form = new Search({ let form: Search = {
q, q,
type_: Some(SearchType.Users), type_: SearchType.Users,
sort: Some(SortType.TopAll), sort: SortType.TopAll,
listing_type: Some(ListingType.All), listing_type: ListingType.All,
page: Some(1), page: 1,
limit: Some(fetchLimit), limit: fetchLimit,
community_id: None, auth: myAuth(false),
community_name: None, };
creator_id: None,
auth: auth(false).ok(),
});
let client = new LemmyHttp(httpBase); let client = new LemmyHttp(httpBase);
return client.search(form); return client.search(form);
} }
@ -1406,7 +1293,7 @@ export function communitySelectName(cv: CommunityView): string {
} }
export function personSelectName(pvs: PersonViewSafe): string { export function personSelectName(pvs: PersonViewSafe): string {
let pName = pvs.person.display_name.unwrapOr(pvs.person.name); let pName = pvs.person.display_name ?? pvs.person.name;
return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`; return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
} }
@ -1430,8 +1317,8 @@ export function isBanned(ps: PersonSafe): boolean {
let expires = ps.ban_expires; let expires = ps.ban_expires;
// Add Z to convert from UTC date // Add Z to convert from UTC date
// TODO this check probably isn't necessary anymore // TODO this check probably isn't necessary anymore
if (expires.isSome()) { if (expires) {
if (ps.banned && new Date(expires.unwrap() + "Z") > new Date()) { if (ps.banned && new Date(expires + "Z") > new Date()) {
return true; return true;
} else { } else {
return false; return false;
@ -1447,7 +1334,7 @@ export function pushNotNull(array: any[], new_item?: any) {
} }
} }
export function auth(throwErr = true): Result<string, string> { export function myAuth(throwErr = true): string | undefined {
return UserService.Instance.auth(throwErr); return UserService.Instance.auth(throwErr);
} }
@ -1471,26 +1358,18 @@ export function postToCommentSortType(sort: SortType): CommentSortType {
} }
} }
export function arrayGet<T>(arr: Array<T>, index: number): Result<T, string> {
let out = arr.at(index);
if (out == undefined) {
return Err("Index undefined");
} else {
return Ok(out);
}
}
export function myFirstDiscussionLanguageId( export function myFirstDiscussionLanguageId(
allLanguages: Language[], allLanguages: Language[],
siteLanguages: number[], siteLanguages: number[],
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): Option<number> { ): number | undefined {
return arrayGet( return selectableLanguages(
selectableLanguages(allLanguages, siteLanguages, false, false, myUserInfo), allLanguages,
0 siteLanguages,
) false,
.map(l => l.id) false,
.ok(); myUserInfo
).at(0)?.id;
} }
export function canCreateCommunity( export function canCreateCommunity(
@ -1505,15 +1384,15 @@ export function isPostBlocked(
pv: PostView, pv: PostView,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
return myUserInfo return (
.map( (myUserInfo?.community_blocks
mui =>
mui.community_blocks
.map(c => c.community.id) .map(c => c.community.id)
.includes(pv.community.id) || .includes(pv.community.id) ||
mui.person_blocks.map(p => p.target.id).includes(pv.creator.id) myUserInfo?.person_blocks
) .map(p => p.target.id)
.unwrapOr(false); .includes(pv.creator.id)) ??
false
);
} }
/// Checks to make sure you can view NSFW posts. Returns true if you can. /// Checks to make sure you can view NSFW posts. Returns true if you can.
@ -1522,17 +1401,12 @@ export function nsfwCheck(
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): boolean { ): boolean {
let nsfw = pv.post.nsfw || pv.community.nsfw; let nsfw = pv.post.nsfw || pv.community.nsfw;
return ( let myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
!nsfw || return !nsfw || (nsfw && myShowNsfw);
(nsfw &&
myUserInfo
.map(m => m.local_user_view.local_user.show_nsfw)
.unwrapOr(false))
);
} }
export function getRandomFromList<T>(list: T[]): T { export function getRandomFromList<T>(list?: T[]): T | undefined {
return list[Math.floor(Math.random() * list.length)]; return list?.at(Math.floor(Math.random() * list.length));
} }
/** /**
@ -1545,14 +1419,12 @@ export function getRandomFromList<T>(list: T[]): T {
export function selectableLanguages( export function selectableLanguages(
allLanguages: Language[], allLanguages: Language[],
siteLanguages: number[], siteLanguages: number[],
showAll: boolean, showAll?: boolean,
showSite: boolean, showSite?: boolean,
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): Language[] { ): Language[] {
let allLangIds = allLanguages.map(l => l.id); let allLangIds = allLanguages.map(l => l.id);
let myLangs = myUserInfo let myLangs = myUserInfo?.discussion_languages ?? allLangIds;
.map(mui => mui.discussion_languages)
.unwrapOr(allLangIds);
myLangs = myLangs.length == 0 ? allLangIds : myLangs; myLangs = myLangs.length == 0 ? allLangIds : myLangs;
let siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages; let siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;

View file

@ -19,7 +19,7 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictNullChecks": false, "strictNullChecks": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": [

View file

@ -1105,11 +1105,6 @@
domhandler "^4.2.0" domhandler "^4.2.0"
selderee "^0.6.0" selderee "^0.6.0"
"@sniptt/monads@^0.5.10":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@sniptt/monads/-/monads-0.5.10.tgz#a80cd00738bbd682d36d36dd36bdc0bddc96eb76"
integrity sha512-+agDOv9DpDV+9e2zN/Vmdk+XaqGx5Sykl0fqhqgiJ90r18nsBkxe44DmZ2sA1HYK+MSsBeZBiAr6pq4w+5uhfw==
"@types/autosize@^4.0.0": "@types/autosize@^4.0.0":
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-4.0.1.tgz#999a7c305b96766248044ebaac1a0299961f3b61" resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-4.0.1.tgz#999a7c305b96766248044ebaac1a0299961f3b61"
@ -2263,11 +2258,6 @@ cidr-regex@1.0.6:
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
integrity sha512-vIIQZtDT0y3GmcVqi4Uhd43s7HKn5DtH8/CcmHe/XG1Vb4JpUgOfTynZzYSo1zeB+j4GbA38Eu2P9UTbIzDw5g== integrity sha512-vIIQZtDT0y3GmcVqi4Uhd43s7HKn5DtH8/CcmHe/XG1Vb4JpUgOfTynZzYSo1zeB+j4GbA38Eu2P9UTbIzDw5g==
class-transformer@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
classnames@^2.3.1: classnames@^2.3.1:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
@ -4982,15 +4972,12 @@ lcid@^1.0.0:
dependencies: dependencies:
invert-kv "^1.0.0" invert-kv "^1.0.0"
lemmy-js-client@0.17.0-rc.57: lemmy-js-client@0.17.0-rc.61:
version "0.17.0-rc.57" version "0.17.0-rc.61"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.57.tgz#f7a243ed53542810e7446b0a28ad162f3e913254" resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.61.tgz#c01e129a3d4c3483ecf337f1e4acf0ad91f9684f"
integrity sha512-7kZHi0B+jiKc50itTwngkS5Vxcuvux3LjgD28IXZ049cWQgZDqer6BCmudcbViP+dAoyWs9Fh2SyWkYFhv7bwQ== integrity sha512-xauBCD5i4vlUEWqsTMIXLCXeIjAK7ivVIN3C/g+RMAM7mD3CTcRkDZUerwnvLipIfr7V/4iYLWZW0orBaiV1CQ==
dependencies: dependencies:
"@sniptt/monads" "^0.5.10"
class-transformer "^0.5.1"
node-fetch "2.6.6" node-fetch "2.6.6"
reflect-metadata "^0.1.13"
levn@^0.4.1: levn@^0.4.1:
version "0.4.1" version "0.4.1"
@ -6969,11 +6956,6 @@ redux@^4.1.2:
dependencies: dependencies:
"@babel/runtime" "^7.9.2" "@babel/runtime" "^7.9.2"
reflect-metadata@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regenerate-unicode-properties@^10.1.0: regenerate-unicode-properties@^10.1.0:
version "10.1.0" version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"