]> Untitled Git - lemmy-ui.git/commitdiff
Adding option types 2 (#689)
authorDessalines <dessalines@users.noreply.github.com>
Tue, 21 Jun 2022 21:42:29 +0000 (17:42 -0400)
committerGitHub <noreply@github.com>
Tue, 21 Jun 2022 21:42:29 +0000 (17:42 -0400)
* Not working, because of wrong API types.

* Adding Rust-style Result and Option types.

- Fixes #646

* Updating to use new lemmy-js-client with Options.

60 files changed:
package.json
src/client/index.tsx
src/server/index.tsx
src/shared/components/app/app.tsx
src/shared/components/app/footer.tsx
src/shared/components/app/navbar.tsx
src/shared/components/app/theme.tsx
src/shared/components/comment/comment-form.tsx
src/shared/components/comment/comment-node.tsx
src/shared/components/comment/comment-nodes.tsx
src/shared/components/comment/comment-report.tsx
src/shared/components/common/banner-icon-header.tsx
src/shared/components/common/html-tags.tsx
src/shared/components/common/image-upload-form.tsx
src/shared/components/common/listing-type-select.tsx
src/shared/components/common/markdown-textarea.tsx
src/shared/components/common/moment-time.tsx
src/shared/components/common/registration-application.tsx
src/shared/components/community/communities.tsx
src/shared/components/community/community-form.tsx
src/shared/components/community/community-link.tsx
src/shared/components/community/community.tsx
src/shared/components/community/create-community.tsx
src/shared/components/community/sidebar.tsx
src/shared/components/home/admin-settings.tsx
src/shared/components/home/home.tsx
src/shared/components/home/instances.tsx
src/shared/components/home/legal.tsx
src/shared/components/home/login.tsx
src/shared/components/home/setup.tsx
src/shared/components/home/signup.tsx
src/shared/components/home/site-form.tsx
src/shared/components/home/site-sidebar.tsx
src/shared/components/modlog.tsx
src/shared/components/person/inbox.tsx
src/shared/components/person/password-change.tsx
src/shared/components/person/person-details.tsx
src/shared/components/person/person-listing.tsx
src/shared/components/person/profile.tsx
src/shared/components/person/registration-applications.tsx
src/shared/components/person/reports.tsx
src/shared/components/person/settings.tsx
src/shared/components/person/verify-email.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/metadata-card.tsx
src/shared/components/post/post-form.tsx
src/shared/components/post/post-listing.tsx
src/shared/components/post/post-listings.tsx
src/shared/components/post/post-report.tsx
src/shared/components/post/post.tsx
src/shared/components/private_message/create-private-message.tsx
src/shared/components/private_message/private-message-form.tsx
src/shared/components/private_message/private-message.tsx
src/shared/components/search.tsx
src/shared/interfaces.ts
src/shared/services/UserService.ts
src/shared/services/WebSocketService.ts
src/shared/utils.ts
tsconfig.json
yarn.lock

index a76700b955a0108c5d42ca69cb3f85b5fac4f6f8..f6ea01cb9ff3752998c4d11edbedb6f5c7c53cf5 100644 (file)
   },
   "devDependencies": {
     "@babel/core": "^7.17.9",
+    "@babel/plugin-proposal-decorators": "^7.18.2",
     "@babel/plugin-transform-runtime": "^7.17.0",
     "@babel/plugin-transform-typescript": "^7.16.1",
     "@babel/preset-env": "7.16.11",
     "@babel/preset-typescript": "^7.16.0",
     "@babel/runtime": "^7.17.9",
+    "@sniptt/monads": "^0.5.10",
     "@types/autosize": "^4.0.0",
     "@types/express": "^4.17.13",
     "@types/node": "^17.0.29",
@@ -67,6 +69,7 @@
     "babel-plugin-inferno": "^6.4.0",
     "bootstrap": "^5.1.3",
     "bootswatch": "^5.1.3",
+    "class-transformer": "^0.5.1",
     "clean-webpack-plugin": "^4.0.0",
     "copy-webpack-plugin": "^10.2.4",
     "css-loader": "^6.7.1",
@@ -74,7 +77,7 @@
     "eslint-plugin-prettier": "^4.0.0",
     "husky": "^7.0.4",
     "import-sort-style-module": "^6.0.0",
-    "lemmy-js-client": "0.16.4",
+    "lemmy-js-client": "0.17.0-rc.30",
     "lint-staged": "^12.4.1",
     "mini-css-extract-plugin": "^2.6.0",
     "node-fetch": "^2.6.1",
@@ -82,6 +85,7 @@
     "prettier-plugin-import-sort": "^0.0.7",
     "prettier-plugin-organize-imports": "^2.3.4",
     "prettier-plugin-packagejson": "^2.2.17",
+    "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "run-node-webpack-plugin": "^1.3.0",
     "sass-loader": "^12.6.0",
index d5773b4455a29827a734eed34dc2063bb96ed8ca..3838dca74eb21d56934f1584a3cb3a59e9e9164d 100644 (file)
@@ -1,14 +1,15 @@
 import { hydrate } from "inferno-hydrate";
 import { BrowserRouter } from "inferno-router";
+import { GetSiteResponse } from "lemmy-js-client";
 import { App } from "../shared/components/app/app";
-import { initializeSite } from "../shared/utils";
+import { convertWindowJson, initializeSite } from "../shared/utils";
 
-const site = window.isoData.site_res;
+const site = convertWindowJson(GetSiteResponse, window.isoData.site_res);
 initializeSite(site);
 
 const wrapper = (
   <BrowserRouter>
-    <App siteRes={window.isoData.site_res} />
+    <App />
   </BrowserRouter>
 );
 
index 65f7308c251a70ba6bd157ae28e7854fb6e1f081..374fb03111c4b99db750d8610e74655bf32fb7e7 100644 (file)
@@ -1,3 +1,5 @@
+import { None, Option } from "@sniptt/monads";
+import { serialize as serializeO } from "class-transformer";
 import express from "express";
 import fs from "fs";
 import { IncomingHttpHeaders } from "http";
@@ -5,7 +7,7 @@ import { Helmet } from "inferno-helmet";
 import { matchPath, StaticRouter } from "inferno-router";
 import { renderToString } from "inferno-server";
 import IsomorphicCookie from "isomorphic-cookie";
-import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
+import { GetSite, GetSiteResponse, LemmyHttp, toOption } from "lemmy-js-client";
 import path from "path";
 import process from "process";
 import serialize from "serialize-javascript";
@@ -18,7 +20,7 @@ import {
   IsoData,
 } from "../shared/interfaces";
 import { routes } from "../shared/routes";
-import { initializeSite, setOptionalAuth } from "../shared/utils";
+import { initializeSite } from "../shared/utils";
 
 const server = express();
 const [hostname, port] = process.env["LEMMY_UI_HOST"]
@@ -115,10 +117,9 @@ server.get("/*", async (req, res) => {
   try {
     const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
     const context = {} as any;
-    let auth: string = IsomorphicCookie.load("jwt", req);
+    let auth: Option<string> = toOption(IsomorphicCookie.load("jwt", req));
 
-    let getSiteForm: GetSite = {};
-    setOptionalAuth(getSiteForm, auth);
+    let getSiteForm = new GetSite({ auth });
 
     let promises: Promise<any>[] = [];
 
@@ -138,8 +139,8 @@ server.get("/*", async (req, res) => {
       console.error(
         "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
       );
-      delete getSiteForm.auth;
-      delete initialFetchReq.auth;
+      getSiteForm.auth = None;
+      initialFetchReq.auth = None;
       try_site = await initialFetchReq.client.getSite(getSiteForm);
     }
     let site: GetSiteResponse = try_site;
@@ -170,7 +171,7 @@ server.get("/*", async (req, res) => {
 
     const wrapper = (
       <StaticRouter location={req.url} context={isoData}>
-        <App siteRes={isoData.site_res} />
+        <App />
       </StaticRouter>
     );
     if (context.url) {
@@ -194,7 +195,7 @@ server.get("/*", async (req, res) => {
            <!DOCTYPE html>
            <html ${helmet.htmlAttributes.toString()} lang="en">
            <head>
-           <script>window.isoData = ${serialize(isoData)}</script>
+           <script>window.isoData = ${serializeO(isoData)}</script>
            <script>window.lemmyConfig = ${serialize(config)}</script>
 
            <!-- A remote debugging utility for mobile -->
index 9ddff69fa5219a334cf1f7ea8033f8dbf9a5c73f..72119a8d601cc9d7f6f21aa98b7e6f933d09e487 100644 (file)
@@ -2,53 +2,46 @@ import { Component } from "inferno";
 import { Helmet } from "inferno-helmet";
 import { Provider } from "inferno-i18next-dess";
 import { Route, Switch } from "inferno-router";
-import { GetSiteResponse } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { routes } from "../../routes";
-import { favIconPngUrl, favIconUrl } from "../../utils";
+import { favIconPngUrl, favIconUrl, setIsoData } from "../../utils";
 import { Footer } from "./footer";
 import { Navbar } from "./navbar";
 import { NoMatch } from "./no-match";
 import "./styles.scss";
 import { Theme } from "./theme";
 
-export interface AppProps {
-  siteRes: GetSiteResponse;
-}
-
-export class App extends Component<AppProps, any> {
+export class App extends Component<any, any> {
+  private isoData = setIsoData(this.context);
   constructor(props: any, context: any) {
     super(props, context);
   }
   render() {
-    let siteRes = this.props.siteRes;
+    let siteRes = this.isoData.site_res;
+    let siteView = siteRes.site_view;
+
     return (
       <>
         <Provider i18next={i18n}>
           <div>
-            <Theme
-              myUserInfo={siteRes.my_user}
-              defaultTheme={siteRes?.site_view?.site?.default_theme}
-            />
-            {siteRes &&
-              siteRes.site_view &&
-              this.props.siteRes.site_view.site.icon && (
-                <Helmet>
-                  <link
-                    id="favicon"
-                    rel="shortcut icon"
-                    type="image/x-icon"
-                    href={this.props.siteRes.site_view.site.icon || favIconUrl}
-                  />
-                  <link
-                    rel="apple-touch-icon"
-                    href={
-                      this.props.siteRes.site_view.site.icon || favIconPngUrl
-                    }
-                  />
-                </Helmet>
-              )}
-            <Navbar site_res={this.props.siteRes} />
+            <Theme defaultTheme={siteView.map(s => s.site.default_theme)} />
+            {siteView
+              .andThen(s => s.site.icon)
+              .match({
+                some: icon => (
+                  <Helmet>
+                    <link
+                      id="favicon"
+                      rel="shortcut icon"
+                      type="image/x-icon"
+                      href={icon || favIconUrl}
+                    />
+                    <link rel="apple-touch-icon" href={icon || favIconPngUrl} />
+                  </Helmet>
+                ),
+                none: <></>,
+              })}
+            <Navbar siteRes={siteRes} />
             <div class="mt-4 p-0 fl-1">
               <Switch>
                 {routes.map(({ path, exact, component: C, ...rest }) => (
@@ -62,7 +55,7 @@ export class App extends Component<AppProps, any> {
                 <Route render={props => <NoMatch {...props} />} />
               </Switch>
             </div>
-            <Footer site={this.props.siteRes} />
+            <Footer site={siteRes} />
           </div>
         </Provider>
       </>
index 601551b5ea9032af57c0da90ddd74026d981904b..e5e1db5a5b0a59eb08ecd08ac818506cd9b42885 100644 (file)
@@ -32,7 +32,9 @@ export class Footer extends Component<FooterProps, any> {
                 {i18n.t("modlog")}
               </NavLink>
             </li>
-            {this.props.site.site_view?.site.legal_information && (
+            {this.props.site.site_view
+              .andThen(s => s.site.legal_information)
+              .isSome() && (
               <li className="nav-item">
                 <NavLink className="nav-link" to="/legal">
                   {i18n.t("legal_information")}
index 9ab3b656ad676b47f09f3d9d980d92e65bb949b4..604a90a57f82b09218e43d33de656e7a0b5273f7 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Some } from "@sniptt/monads";
 import { Component, createRef, linkEvent, RefObject } from "inferno";
 import { NavLink } from "inferno-router";
 import {
@@ -11,12 +12,15 @@ import {
   GetUnreadRegistrationApplicationCountResponse,
   PrivateMessageResponse,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  amAdmin,
+  auth,
   donateLemmyUrl,
   getLanguages,
   isBrowser,
@@ -27,19 +31,16 @@ import {
   showAvatars,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { Icon } from "../common/icon";
 import { PictrsImage } from "../common/pictrs-image";
 
 interface NavbarProps {
-  site_res: GetSiteResponse;
+  siteRes: GetSiteResponse;
 }
 
 interface NavbarState {
-  isLoggedIn: boolean;
   expanded: boolean;
   unreadInboxCount: number;
   unreadReportCount: number;
@@ -58,7 +59,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   private unreadApplicationCountSub: Subscription;
   private searchTextField: RefObject<HTMLInputElement>;
   emptyState: NavbarState = {
-    isLoggedIn: !!this.props.site_res.my_user,
     unreadInboxCount: 0,
     unreadReportCount: 0,
     unreadApplicationCount: 0,
@@ -81,18 +81,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     // Subscribe to jwt changes
     if (isBrowser()) {
       this.searchTextField = createRef();
-      console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
 
       // On the first load, check the unreads
-      if (this.state.isLoggedIn == false) {
-        // setTheme(data.my_user.theme, true);
-        // i18n.changeLanguage(getLanguage());
-        // i18n.changeLanguage('de');
-      } else {
+      if (UserService.Instance.myUserInfo.isSome()) {
         this.requestNotificationPermission();
         WebSocketService.Instance.send(
           wsClient.userJoin({
-            auth: authField(),
+            auth: auth().unwrap(),
           })
         );
         this.fetchUnreads();
@@ -100,13 +95,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
 
       this.userSub = UserService.Instance.jwtSub.subscribe(res => {
         // A login
-        if (res !== undefined) {
+        if (res.isSome()) {
           this.requestNotificationPermission();
           WebSocketService.Instance.send(
-            wsClient.getSite({ auth: authField() })
+            wsClient.getSite({ auth: res.map(r => r.jwt) })
           );
-        } else {
-          this.setState({ isLoggedIn: false });
         }
       });
 
@@ -157,32 +150,28 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
 
   // TODO class active corresponding to current page
   navbar() {
-    let myUserInfo =
-      UserService.Instance.myUserInfo || this.props.site_res.my_user;
-    let person = myUserInfo?.local_user_view.person;
     return (
-      <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
+      <nav class="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
         <div class="container">
-          {this.props.site_res.site_view && (
-            <NavLink
-              to="/"
-              onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-              title={
-                this.props.site_res.site_view.site.description ||
-                this.props.site_res.site_view.site.name
-              }
-              className="d-flex align-items-center navbar-brand mr-md-3"
-            >
-              {this.props.site_res.site_view.site.icon && showAvatars() && (
-                <PictrsImage
-                  src={this.props.site_res.site_view.site.icon}
-                  icon
-                />
-              )}
-              {this.props.site_res.site_view.site.name}
-            </NavLink>
-          )}
-          {this.state.isLoggedIn && (
+          {this.props.siteRes.site_view.match({
+            some: siteView => (
+              <NavLink
+                to="/"
+                onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
+                title={siteView.site.description.unwrapOr(siteView.site.name)}
+                className="d-flex align-items-center navbar-brand mr-md-3"
+              >
+                {siteView.site.icon.match({
+                  some: icon =>
+                    showAvatars() && <PictrsImage src={icon} icon />,
+                  none: <></>,
+                })}
+                {siteView.site.name}
+              </NavLink>
+            ),
+            none: <></>,
+          })}
+          {UserService.Instance.myUserInfo.isSome() && (
             <>
               <ul class="navbar-nav ml-auto">
                 <li className="nav-item">
@@ -204,7 +193,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                   </NavLink>
                 </li>
               </ul>
-              {UserService.Instance.myUserInfo?.moderates.length > 0 && (
+              {this.moderatesSomething && (
                 <ul class="navbar-nav ml-1">
                   <li className="nav-item">
                     <NavLink
@@ -226,8 +215,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                   </li>
                 </ul>
               )}
-              {UserService.Instance.myUserInfo?.local_user_view.person
-                .admin && (
+              {this.amAdmin && (
                 <ul class="navbar-nav ml-1">
                   <li className="nav-item">
                     <NavLink
@@ -312,7 +300,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
               </li>
             </ul>
             <ul class="navbar-nav my-2">
-              {this.canAdmin && (
+              {this.amAdmin && (
                 <li className="nav-item">
                   <NavLink
                     to="/admin"
@@ -358,7 +346,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                 </button>
               </form>
             )}
-            {this.state.isLoggedIn ? (
+            {UserService.Instance.myUserInfo.isSome() ? (
               <>
                 <ul class="navbar-nav my-2">
                   <li className="nav-item">
@@ -380,7 +368,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                     </NavLink>
                   </li>
                 </ul>
-                {UserService.Instance.myUserInfo?.moderates.length > 0 && (
+                {this.moderatesSomething && (
                   <ul class="navbar-nav my-2">
                     <li className="nav-item">
                       <NavLink
@@ -402,8 +390,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                     </li>
                   </ul>
                 )}
-                {UserService.Instance.myUserInfo?.local_user_view.person
-                  .admin && (
+                {this.amAdmin && (
                   <ul class="navbar-nav my-2">
                     <li className="nav-item">
                       <NavLink
@@ -427,69 +414,81 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                     </li>
                   </ul>
                 )}
-                <ul class="navbar-nav">
-                  <li class="nav-item dropdown">
-                    <button
-                      class="nav-link btn btn-link dropdown-toggle"
-                      onClick={linkEvent(this, this.handleToggleDropdown)}
-                      id="navbarDropdown"
-                      role="button"
-                      aria-expanded="false"
-                    >
-                      <span>
-                        {person.avatar && showAvatars() && (
-                          <PictrsImage src={person.avatar} icon />
-                        )}
-                        {person.display_name
-                          ? person.display_name
-                          : person.name}
-                      </span>
-                    </button>
-                    {this.state.showDropdown && (
-                      <div
-                        class="dropdown-content"
-                        onMouseLeave={linkEvent(
-                          this,
-                          this.handleToggleDropdown
-                        )}
-                      >
-                        <li className="nav-item">
-                          <NavLink
-                            to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
-                            className="nav-link"
-                            title={i18n.t("profile")}
-                          >
-                            <Icon icon="user" classes="mr-1" />
-                            {i18n.t("profile")}
-                          </NavLink>
-                        </li>
-                        <li className="nav-item">
-                          <NavLink
-                            to="/settings"
-                            className="nav-link"
-                            title={i18n.t("settings")}
-                          >
-                            <Icon icon="settings" classes="mr-1" />
-                            {i18n.t("settings")}
-                          </NavLink>
-                        </li>
-                        <li>
-                          <hr class="dropdown-divider" />
-                        </li>
-                        <li className="nav-item">
+                {UserService.Instance.myUserInfo
+                  .map(m => m.local_user_view.person)
+                  .match({
+                    some: person => (
+                      <ul class="navbar-nav">
+                        <li class="nav-item dropdown">
                           <button
-                            className="nav-link btn btn-link"
-                            onClick={linkEvent(this, this.handleLogoutClick)}
-                            title="test"
+                            class="nav-link btn btn-link dropdown-toggle"
+                            onClick={linkEvent(this, this.handleToggleDropdown)}
+                            id="navbarDropdown"
+                            role="button"
+                            aria-expanded="false"
                           >
-                            <Icon icon="log-out" classes="mr-1" />
-                            {i18n.t("logout")}
+                            <span>
+                              {showAvatars() &&
+                                person.avatar.match({
+                                  some: avatar => (
+                                    <PictrsImage src={avatar} icon />
+                                  ),
+                                  none: <></>,
+                                })}
+                              {person.display_name.unwrapOr(person.name)}
+                            </span>
                           </button>
+                          {this.state.showDropdown && (
+                            <div
+                              class="dropdown-content"
+                              onMouseLeave={linkEvent(
+                                this,
+                                this.handleToggleDropdown
+                              )}
+                            >
+                              <li className="nav-item">
+                                <NavLink
+                                  to={`/u/${person.name}`}
+                                  className="nav-link"
+                                  title={i18n.t("profile")}
+                                >
+                                  <Icon icon="user" classes="mr-1" />
+                                  {i18n.t("profile")}
+                                </NavLink>
+                              </li>
+                              <li className="nav-item">
+                                <NavLink
+                                  to="/settings"
+                                  className="nav-link"
+                                  title={i18n.t("settings")}
+                                >
+                                  <Icon icon="settings" classes="mr-1" />
+                                  {i18n.t("settings")}
+                                </NavLink>
+                              </li>
+                              <li>
+                                <hr class="dropdown-divider" />
+                              </li>
+                              <li className="nav-item">
+                                <button
+                                  className="nav-link btn btn-link"
+                                  onClick={linkEvent(
+                                    this,
+                                    this.handleLogoutClick
+                                  )}
+                                  title="test"
+                                >
+                                  <Icon icon="log-out" classes="mr-1" />
+                                  {i18n.t("logout")}
+                                </button>
+                              </li>
+                            </div>
+                          )}
                         </li>
-                      </div>
-                    )}
-                  </li>
-                </ul>
+                      </ul>
+                    ),
+                    none: <></>,
+                  })}
               </>
             ) : (
               <ul class="navbar-nav my-2">
@@ -521,6 +520,24 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     );
   }
 
+  get moderatesSomething(): boolean {
+    return (
+      UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
+        .length > 0
+    );
+  }
+
+  get amAdmin(): boolean {
+    return amAdmin(Some(this.props.siteRes.admins));
+  }
+
+  get canCreateCommunity(): boolean {
+    let adminOnly = this.props.siteRes.site_view
+      .map(s => s.site.community_creation_admin_only)
+      .unwrapOr(false);
+    return !adminOnly || this.amAdmin;
+  }
+
   handleToggleExpandNavbar(i: Navbar) {
     i.state.expanded = !i.state.expanded;
     i.setState(i.state);
@@ -561,8 +578,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   handleLogoutClick(i: Navbar) {
     i.setState({ showDropdown: false, expanded: false });
     UserService.Instance.logout();
-    window.location.href = "/";
-    location.reload();
   }
 
   handleToggleDropdown(i: Navbar) {
@@ -576,98 +591,117 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     if (msg.error) {
       if (msg.error == "not_logged_in") {
         UserService.Instance.logout();
-        location.reload();
       }
       return;
     } else if (msg.reconnect) {
       console.log(i18n.t("websocket_reconnected"));
-      WebSocketService.Instance.send(
-        wsClient.userJoin({
-          auth: authField(),
-        })
-      );
-      this.fetchUnreads();
+      if (UserService.Instance.myUserInfo.isSome()) {
+        WebSocketService.Instance.send(
+          wsClient.userJoin({
+            auth: auth().unwrap(),
+          })
+        );
+        this.fetchUnreads();
+      }
     } else if (op == UserOperation.GetUnreadCount) {
-      let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
+      let data = wsJsonToRes<GetUnreadCountResponse>(
+        msg,
+        GetUnreadCountResponse
+      );
       this.state.unreadInboxCount =
         data.replies + data.mentions + data.private_messages;
       this.setState(this.state);
       this.sendUnreadCount();
     } else if (op == UserOperation.GetReportCount) {
-      let data = wsJsonToRes<GetReportCountResponse>(msg).data;
+      let data = wsJsonToRes<GetReportCountResponse>(
+        msg,
+        GetReportCountResponse
+      );
       this.state.unreadReportCount = data.post_reports + data.comment_reports;
       this.setState(this.state);
       this.sendReportUnread();
     } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
-      let data =
-        wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg).data;
+      let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
+        msg,
+        GetUnreadRegistrationApplicationCountResponse
+      );
       this.state.unreadApplicationCount = data.registration_applications;
       this.setState(this.state);
       this.sendApplicationUnread();
     } else if (op == UserOperation.GetSite) {
       // This is only called on a successful login
-      let data = wsJsonToRes<GetSiteResponse>(msg).data;
-      console.log(data.my_user);
+      let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
       UserService.Instance.myUserInfo = data.my_user;
-      setTheme(
-        UserService.Instance.myUserInfo.local_user_view.local_user.theme
-      );
-      i18n.changeLanguage(getLanguages()[0]);
-      this.state.isLoggedIn = true;
-      this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-
-      if (this.state.isLoggedIn) {
-        if (
-          data.recipient_ids.includes(
-            UserService.Instance.myUserInfo.local_user_view.local_user.id
-          )
-        ) {
-          this.state.unreadInboxCount++;
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          setTheme(mui.local_user_view.local_user.theme);
+          i18n.changeLanguage(getLanguages()[0]);
           this.setState(this.state);
-          this.sendUnreadCount();
-          notifyComment(data.comment_view, this.context.router);
-        }
-      }
+        },
+        none: void 0,
+      });
+    } else if (op == UserOperation.CreateComment) {
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
+            this.state.unreadInboxCount++;
+            this.setState(this.state);
+            this.sendUnreadCount();
+            notifyComment(data.comment_view, this.context.router);
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.CreatePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-
-      if (this.state.isLoggedIn) {
-        if (
-          data.private_message_view.recipient.id ==
-          UserService.Instance.myUserInfo.local_user_view.person.id
-        ) {
-          this.state.unreadInboxCount++;
-          this.setState(this.state);
-          this.sendUnreadCount();
-          notifyPrivateMessage(data.private_message_view, this.context.router);
-        }
-      }
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
+
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (
+            data.private_message_view.recipient.id ==
+            mui.local_user_view.person.id
+          ) {
+            this.state.unreadInboxCount++;
+            this.setState(this.state);
+            this.sendUnreadCount();
+            notifyPrivateMessage(
+              data.private_message_view,
+              this.context.router
+            );
+          }
+        },
+        none: void 0,
+      });
     }
   }
 
   fetchUnreads() {
     console.log("Fetching inbox unreads...");
 
-    let unreadForm: GetUnreadCount = {
-      auth: authField(),
-    };
+    let unreadForm = new GetUnreadCount({
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
 
     console.log("Fetching reports...");
 
-    let reportCountForm: GetReportCount = {
-      auth: authField(),
-    };
+    let reportCountForm = new GetReportCount({
+      community_id: None,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
 
-    if (UserService.Instance.myUserInfo?.local_user_view.person.admin) {
+    if (this.amAdmin) {
       console.log("Fetching applications...");
 
-      let applicationCountForm: GetUnreadRegistrationApplicationCount = {
-        auth: authField(),
-      };
+      let applicationCountForm = new GetUnreadRegistrationApplicationCount({
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(
         wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
       );
@@ -694,23 +728,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     );
   }
 
-  get canAdmin(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      this.props.site_res.admins
-        .map(a => a.person.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
-    );
-  }
-
-  get canCreateCommunity(): boolean {
-    let adminOnly =
-      this.props.site_res.site_view?.site.community_creation_admin_only;
-    return !adminOnly || this.canAdmin;
-  }
-
   requestNotificationPermission() {
-    if (UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isSome()) {
       document.addEventListener("DOMContentLoaded", function () {
         if (!Notification) {
           toast(i18n.t("notifications_error"), "danger");
index cdb7f7eb321c530584c53c7c5373dc38c8a022ac..a30358fe9786daa2163f08a3b3fca09f68a541e2 100644 (file)
@@ -1,16 +1,18 @@
+import { Option } from "@sniptt/monads";
 import { Component } from "inferno";
 import { Helmet } from "inferno-helmet";
-import { MyUserInfo } from "lemmy-js-client";
+import { UserService } from "../../services";
 
 interface Props {
-  myUserInfo: MyUserInfo | undefined;
-  defaultTheme?: string;
+  defaultTheme: Option<string>;
 }
 
 export class Theme extends Component<Props> {
   render() {
-    let user = this.props.myUserInfo;
-    let hasTheme = user && user.local_user_view.local_user.theme !== "browser";
+    let user = UserService.Instance.myUserInfo;
+    let hasTheme = user
+      .map(m => m.local_user_view.local_user.theme !== "browser")
+      .unwrapOr(false);
 
     if (hasTheme) {
       return (
@@ -18,20 +20,22 @@ export class Theme extends Component<Props> {
           <link
             rel="stylesheet"
             type="text/css"
-            href={`/css/themes/${user.local_user_view.local_user.theme}.css`}
+            href={`/css/themes/${
+              user.unwrap().local_user_view.local_user.theme
+            }.css`}
           />
         </Helmet>
       );
     } else if (
-      this.props.defaultTheme != null &&
-      this.props.defaultTheme != "browser"
+      this.props.defaultTheme.isSome() &&
+      this.props.defaultTheme.unwrap() != "browser"
     ) {
       return (
         <Helmet>
           <link
             rel="stylesheet"
             type="text/css"
-            href={`/css/themes/${this.props.defaultTheme}.css`}
+            href={`/css/themes/${this.props.defaultTheme.unwrap()}.css`}
           />
         </Helmet>
       );
index 58cabeb60623efc7a0ce0b62036f3536fbccef0f..7abf39b70a2fcbb76560b828f7d19339529be493 100644 (file)
@@ -1,3 +1,4 @@
+import { Either, None, Option, Some } from "@sniptt/monads";
 import { Component } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { Link } from "inferno-router";
@@ -6,25 +7,27 @@ import {
   CreateComment,
   EditComment,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { CommentNode as CommentNodeI } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { Icon } from "../common/icon";
 import { MarkdownTextArea } from "../common/markdown-textarea";
 
 interface CommentFormProps {
-  postId?: number;
-  node?: CommentNodeI; // Can either be the parent, or the editable comment
+  /**
+   * Can either be the parent, or the editable comment. The right side is a postId.
+   */
+  node: Either<CommentNodeI, number>;
   edit?: boolean;
   disabled?: boolean;
   focus?: boolean;
@@ -34,19 +37,19 @@ interface CommentFormProps {
 interface CommentFormState {
   buttonTitle: string;
   finished: boolean;
-  formId: string;
+  formId: Option<string>;
 }
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   private subscription: Subscription;
   private emptyState: CommentFormState = {
-    buttonTitle: !this.props.node
+    buttonTitle: this.props.node.isRight()
       ? capitalizeFirstLetter(i18n.t("post"))
       : this.props.edit
       ? capitalizeFirstLetter(i18n.t("save"))
       : capitalizeFirstLetter(i18n.t("reply")),
     finished: false,
-    formId: "empty_form",
+    formId: None,
   };
 
   constructor(props: any, context: any) {
@@ -66,23 +69,25 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   }
 
   render() {
+    let initialContent = this.props.node.match({
+      left: node =>
+        this.props.edit ? Some(node.comment_view.comment.content) : None,
+      right: () => None,
+    });
     return (
       <div class="mb-3">
-        {UserService.Instance.myUserInfo ? (
+        {UserService.Instance.myUserInfo.isSome() ? (
           <MarkdownTextArea
-            initialContent={
-              this.props.edit
-                ? this.props.node.comment_view.comment.content
-                : null
-            }
-            buttonTitle={this.state.buttonTitle}
+            initialContent={initialContent}
+            buttonTitle={Some(this.state.buttonTitle)}
+            maxLength={None}
             finished={this.state.finished}
-            replyType={!!this.props.node}
+            replyType={this.props.node.isLeft()}
             focus={this.props.focus}
             disabled={this.props.disabled}
             onSubmit={this.handleCommentSubmit}
             onReplyCancel={this.handleReplyCancel}
-            placeholder={i18n.t("comment_here")}
+            placeholder={Some(i18n.t("comment_here"))}
           />
         ) : (
           <div class="alert alert-warning" role="alert">
@@ -101,28 +106,40 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
   handleCommentSubmit(msg: { val: string; formId: string }) {
     let content = msg.val;
-    this.state.formId = msg.formId;
-
-    let node = this.props.node;
-
-    if (this.props.edit) {
-      let form: EditComment = {
-        content,
-        form_id: this.state.formId,
-        comment_id: node.comment_view.comment.id,
-        auth: authField(),
-      };
-      WebSocketService.Instance.send(wsClient.editComment(form));
-    } else {
-      let form: CreateComment = {
-        content,
-        form_id: this.state.formId,
-        post_id: node ? node.comment_view.post.id : this.props.postId,
-        parent_id: node ? node.comment_view.comment.id : null,
-        auth: authField(),
-      };
-      WebSocketService.Instance.send(wsClient.createComment(form));
-    }
+    this.state.formId = Some(msg.formId);
+
+    this.props.node.match({
+      left: node => {
+        if (this.props.edit) {
+          let form = new EditComment({
+            content,
+            form_id: this.state.formId,
+            comment_id: node.comment_view.comment.id,
+            auth: auth().unwrap(),
+          });
+          WebSocketService.Instance.send(wsClient.editComment(form));
+        } else {
+          let form = new CreateComment({
+            content,
+            form_id: this.state.formId,
+            post_id: node.comment_view.post.id,
+            parent_id: Some(node.comment_view.comment.id),
+            auth: auth().unwrap(),
+          });
+          WebSocketService.Instance.send(wsClient.createComment(form));
+        }
+      },
+      right: postId => {
+        let form = new CreateComment({
+          content,
+          form_id: this.state.formId,
+          post_id: postId,
+          parent_id: None,
+          auth: auth().unwrap(),
+        });
+        WebSocketService.Instance.send(wsClient.createComment(form));
+      },
+    });
     this.setState(this.state);
   }
 
@@ -135,15 +152,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     console.log(msg);
 
     // Only do the showing and hiding if logged in
-    if (UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isSome()) {
       if (
         op == UserOperation.CreateComment ||
         op == UserOperation.EditComment
       ) {
-        let data = wsJsonToRes<CommentResponse>(msg).data;
+        let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
 
         // This only finishes this form, if the randomly generated form_id matches the one received
-        if (this.state.formId == data.form_id) {
+        if (this.state.formId.unwrapOr("") == data.form_id.unwrapOr("")) {
           this.setState({ finished: true });
 
           // Necessary because it broke tribute for some reason
index d2a36acc79eda98a04968cd55e7761247d30117b..356a4820387074f8a023721f09fa0509beb68d20 100644 (file)
@@ -1,3 +1,4 @@
+import { Left, None, Option, Some } from "@sniptt/monads";
 import classNames from "classnames";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
@@ -18,6 +19,7 @@ import {
   PersonViewSafe,
   RemoveComment,
   SaveComment,
+  toUndefined,
   TransferCommunity,
 } from "lemmy-js-client";
 import moment from "moment";
@@ -25,10 +27,13 @@ import { i18n } from "../../i18next";
 import { BanType, CommentNode as CommentNodeI } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  amCommunityCreator,
+  auth,
+  canAdmin,
   canMod,
   colorList,
   futureDaysToUnixTime,
+  isAdmin,
   isBanned,
   isMod,
   mdToHtml,
@@ -48,11 +53,11 @@ interface CommentNodeState {
   showReply: boolean;
   showEdit: boolean;
   showRemoveDialog: boolean;
-  removeReason: string;
+  removeReason: Option<string>;
   showBanDialog: boolean;
   removeData: boolean;
-  banReason: string;
-  banExpireDays: number;
+  banReason: Option<string>;
+  banExpireDays: Option<number>;
   banType: BanType;
   showConfirmTransferSite: boolean;
   showConfirmTransferCommunity: boolean;
@@ -63,7 +68,7 @@ interface CommentNodeState {
   showAdvanced: boolean;
   showReportDialog: boolean;
   reportReason: string;
-  my_vote: number;
+  my_vote: Option<number>;
   score: number;
   upvotes: number;
   downvotes: number;
@@ -74,16 +79,14 @@ interface CommentNodeState {
 
 interface CommentNodeProps {
   node: CommentNodeI;
+  moderators: Option<CommunityModeratorView[]>;
+  admins: Option<PersonViewSafe[]>;
   noBorder?: boolean;
   noIndent?: boolean;
   viewOnly?: boolean;
   locked?: boolean;
   markable?: boolean;
   showContext?: boolean;
-  moderators: CommunityModeratorView[];
-  admins: PersonViewSafe[];
-  // TODO is this necessary, can't I get it from the node itself?
-  postCreatorId?: number;
   showCommunity?: boolean;
   enableDownvotes: boolean;
 }
@@ -93,11 +96,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     showReply: false,
     showEdit: false,
     showRemoveDialog: false,
-    removeReason: null,
+    removeReason: None,
     showBanDialog: false,
     removeData: false,
-    banReason: null,
-    banExpireDays: null,
+    banReason: None,
+    banExpireDays: None,
     banType: BanType.Community,
     collapsed: false,
     viewSource: false,
@@ -143,10 +146,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   render() {
     let node = this.props.node;
     let cv = this.props.node.comment_view;
+
+    let canMod_ = canMod(
+      this.props.moderators,
+      this.props.admins,
+      cv.creator.id
+    );
+    let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
+    let isMod_ = isMod(this.props.moderators, cv.creator.id);
+    let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
+    let amCommunityCreator_ = amCommunityCreator(
+      this.props.moderators,
+      cv.creator.id
+    );
+
     return (
       <div
         className={`comment ${
-          cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
+          cv.comment.parent_id.isSome() && !this.props.noIndent ? "ml-1" : ""
         }`}
       >
         <div
@@ -156,24 +173,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
           } ${this.isCommentNew ? "mark" : ""}`}
           style={
             !this.props.noIndent &&
-            cv.comment.parent_id &&
+            cv.comment.parent_id.isSome() &&
             `border-left: 2px ${this.state.borderColor} solid !important`
           }
         >
           <div
-            class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
+            class={`${
+              !this.props.noIndent && cv.comment.parent_id.isSome() && "ml-2"
+            }`}
           >
             <div class="d-flex flex-wrap align-items-center text-muted small">
               <span class="mr-2">
                 <PersonListing person={cv.creator} />
               </span>
 
-              {this.isMod && (
+              {isMod_ && (
                 <div className="badge badge-light d-none d-sm-inline mr-2">
                   {i18n.t("mod")}
                 </div>
               )}
-              {this.isAdmin && (
+              {isAdmin_ && (
                 <div className="badge badge-light d-none d-sm-inline mr-2">
                   {i18n.t("admin")}
                 </div>
@@ -239,13 +258,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 </>
               )}
               <span>
-                <MomentTime data={cv.comment} />
+                <MomentTime
+                  published={cv.comment.published}
+                  updated={cv.comment.updated}
+                />
               </span>
             </div>
             {/* end of user row */}
             {this.state.showEdit && (
               <CommentForm
-                node={node}
+                node={Left(node)}
                 edit
                 onReplyCancel={this.handleReplyCancel}
                 disabled={this.props.locked}
@@ -293,403 +315,412 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                       )}
                     </button>
                   )}
-                  {UserService.Instance.myUserInfo && !this.props.viewOnly && (
-                    <>
-                      <button
-                        className={`btn btn-link btn-animate ${
-                          this.state.my_vote == 1 ? "text-info" : "text-muted"
-                        }`}
-                        onClick={linkEvent(node, this.handleCommentUpvote)}
-                        data-tippy-content={i18n.t("upvote")}
-                        aria-label={i18n.t("upvote")}
-                      >
-                        <Icon icon="arrow-up1" classes="icon-inline" />
-                        {showScores() &&
-                          this.state.upvotes !== this.state.score && (
-                            <span class="ml-1">
-                              {numToSI(this.state.upvotes)}
-                            </span>
-                          )}
-                      </button>
-                      {this.props.enableDownvotes && (
+                  {UserService.Instance.myUserInfo.isSome() &&
+                    !this.props.viewOnly && (
+                      <>
                         <button
                           className={`btn btn-link btn-animate ${
-                            this.state.my_vote == -1
-                              ? "text-danger"
+                            this.state.my_vote.unwrapOr(0) == 1
+                              ? "text-info"
                               : "text-muted"
                           }`}
-                          onClick={linkEvent(node, this.handleCommentDownvote)}
-                          data-tippy-content={i18n.t("downvote")}
-                          aria-label={i18n.t("downvote")}
+                          onClick={linkEvent(node, this.handleCommentUpvote)}
+                          data-tippy-content={i18n.t("upvote")}
+                          aria-label={i18n.t("upvote")}
                         >
-                          <Icon icon="arrow-down1" classes="icon-inline" />
+                          <Icon icon="arrow-up1" classes="icon-inline" />
                           {showScores() &&
                             this.state.upvotes !== this.state.score && (
                               <span class="ml-1">
-                                {numToSI(this.state.downvotes)}
+                                {numToSI(this.state.upvotes)}
                               </span>
                             )}
                         </button>
-                      )}
-                      <button
-                        class="btn btn-link btn-animate text-muted"
-                        onClick={linkEvent(this, this.handleReplyClick)}
-                        data-tippy-content={i18n.t("reply")}
-                        aria-label={i18n.t("reply")}
-                      >
-                        <Icon icon="reply1" classes="icon-inline" />
-                      </button>
-                      {!this.state.showAdvanced ? (
-                        <button
-                          className="btn btn-link btn-animate text-muted"
-                          onClick={linkEvent(this, this.handleShowAdvanced)}
-                          data-tippy-content={i18n.t("more")}
-                          aria-label={i18n.t("more")}
-                        >
-                          <Icon icon="more-vertical" classes="icon-inline" />
-                        </button>
-                      ) : (
-                        <>
-                          {!this.myComment && (
-                            <>
-                              <button class="btn btn-link btn-animate">
-                                <Link
-                                  className="text-muted"
-                                  to={`/create_private_message/recipient/${cv.creator.id}`}
-                                  title={i18n.t("message").toLowerCase()}
-                                >
-                                  <Icon icon="mail" />
-                                </Link>
-                              </button>
-                              <button
-                                class="btn btn-link btn-animate text-muted"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleShowReportDialog
-                                )}
-                                data-tippy-content={i18n.t(
-                                  "show_report_dialog"
-                                )}
-                                aria-label={i18n.t("show_report_dialog")}
-                              >
-                                <Icon icon="flag" />
-                              </button>
-                              <button
-                                class="btn btn-link btn-animate text-muted"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleBlockUserClick
-                                )}
-                                data-tippy-content={i18n.t("block_user")}
-                                aria-label={i18n.t("block_user")}
-                              >
-                                <Icon icon="slash" />
-                              </button>
-                            </>
-                          )}
+                        {this.props.enableDownvotes && (
                           <button
-                            class="btn btn-link btn-animate text-muted"
+                            className={`btn btn-link btn-animate ${
+                              this.state.my_vote.unwrapOr(0) == -1
+                                ? "text-danger"
+                                : "text-muted"
+                            }`}
                             onClick={linkEvent(
-                              this,
-                              this.handleSaveCommentClick
+                              node,
+                              this.handleCommentDownvote
                             )}
-                            data-tippy-content={
-                              cv.saved ? i18n.t("unsave") : i18n.t("save")
-                            }
-                            aria-label={
-                              cv.saved ? i18n.t("unsave") : i18n.t("save")
-                            }
+                            data-tippy-content={i18n.t("downvote")}
+                            aria-label={i18n.t("downvote")}
                           >
-                            {this.state.saveLoading ? (
-                              this.loadingIcon
-                            ) : (
-                              <Icon
-                                icon="star"
-                                classes={`icon-inline ${
-                                  cv.saved && "text-warning"
-                                }`}
-                              />
-                            )}
+                            <Icon icon="arrow-down1" classes="icon-inline" />
+                            {showScores() &&
+                              this.state.upvotes !== this.state.score && (
+                                <span class="ml-1">
+                                  {numToSI(this.state.downvotes)}
+                                </span>
+                              )}
                           </button>
+                        )}
+                        <button
+                          class="btn btn-link btn-animate text-muted"
+                          onClick={linkEvent(this, this.handleReplyClick)}
+                          data-tippy-content={i18n.t("reply")}
+                          aria-label={i18n.t("reply")}
+                        >
+                          <Icon icon="reply1" classes="icon-inline" />
+                        </button>
+                        {!this.state.showAdvanced ? (
                           <button
                             className="btn btn-link btn-animate text-muted"
-                            onClick={linkEvent(this, this.handleViewSource)}
-                            data-tippy-content={i18n.t("view_source")}
-                            aria-label={i18n.t("view_source")}
+                            onClick={linkEvent(this, this.handleShowAdvanced)}
+                            data-tippy-content={i18n.t("more")}
+                            aria-label={i18n.t("more")}
                           >
-                            <Icon
-                              icon="file-text"
-                              classes={`icon-inline ${
-                                this.state.viewSource && "text-success"
-                              }`}
-                            />
+                            <Icon icon="more-vertical" classes="icon-inline" />
                           </button>
-                          {this.myComment && (
-                            <>
-                              <button
-                                class="btn btn-link btn-animate text-muted"
-                                onClick={linkEvent(this, this.handleEditClick)}
-                                data-tippy-content={i18n.t("edit")}
-                                aria-label={i18n.t("edit")}
-                              >
-                                <Icon icon="edit" classes="icon-inline" />
-                              </button>
-                              <button
-                                class="btn btn-link btn-animate text-muted"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleDeleteClick
-                                )}
-                                data-tippy-content={
-                                  !cv.comment.deleted
-                                    ? i18n.t("delete")
-                                    : i18n.t("restore")
-                                }
-                                aria-label={
-                                  !cv.comment.deleted
-                                    ? i18n.t("delete")
-                                    : i18n.t("restore")
-                                }
-                              >
+                        ) : (
+                          <>
+                            {!this.myComment && (
+                              <>
+                                <button class="btn btn-link btn-animate">
+                                  <Link
+                                    className="text-muted"
+                                    to={`/create_private_message/recipient/${cv.creator.id}`}
+                                    title={i18n.t("message").toLowerCase()}
+                                  >
+                                    <Icon icon="mail" />
+                                  </Link>
+                                </button>
+                                <button
+                                  class="btn btn-link btn-animate text-muted"
+                                  onClick={linkEvent(
+                                    this,
+                                    this.handleShowReportDialog
+                                  )}
+                                  data-tippy-content={i18n.t(
+                                    "show_report_dialog"
+                                  )}
+                                  aria-label={i18n.t("show_report_dialog")}
+                                >
+                                  <Icon icon="flag" />
+                                </button>
+                                <button
+                                  class="btn btn-link btn-animate text-muted"
+                                  onClick={linkEvent(
+                                    this,
+                                    this.handleBlockUserClick
+                                  )}
+                                  data-tippy-content={i18n.t("block_user")}
+                                  aria-label={i18n.t("block_user")}
+                                >
+                                  <Icon icon="slash" />
+                                </button>
+                              </>
+                            )}
+                            <button
+                              class="btn btn-link btn-animate text-muted"
+                              onClick={linkEvent(
+                                this,
+                                this.handleSaveCommentClick
+                              )}
+                              data-tippy-content={
+                                cv.saved ? i18n.t("unsave") : i18n.t("save")
+                              }
+                              aria-label={
+                                cv.saved ? i18n.t("unsave") : i18n.t("save")
+                              }
+                            >
+                              {this.state.saveLoading ? (
+                                this.loadingIcon
+                              ) : (
                                 <Icon
-                                  icon="trash"
+                                  icon="star"
                                   classes={`icon-inline ${
-                                    cv.comment.deleted && "text-danger"
+                                    cv.saved && "text-warning"
                                   }`}
                                 />
-                              </button>
-                            </>
-                          )}
-                          {/* Admins and mods can remove comments */}
-                          {(this.canMod || this.canAdmin) && (
-                            <>
-                              {!cv.comment.removed ? (
+                              )}
+                            </button>
+                            <button
+                              className="btn btn-link btn-animate text-muted"
+                              onClick={linkEvent(this, this.handleViewSource)}
+                              data-tippy-content={i18n.t("view_source")}
+                              aria-label={i18n.t("view_source")}
+                            >
+                              <Icon
+                                icon="file-text"
+                                classes={`icon-inline ${
+                                  this.state.viewSource && "text-success"
+                                }`}
+                              />
+                            </button>
+                            {this.myComment && (
+                              <>
                                 <button
                                   class="btn btn-link btn-animate text-muted"
                                   onClick={linkEvent(
                                     this,
-                                    this.handleModRemoveShow
+                                    this.handleEditClick
                                   )}
-                                  aria-label={i18n.t("remove")}
+                                  data-tippy-content={i18n.t("edit")}
+                                  aria-label={i18n.t("edit")}
                                 >
-                                  {i18n.t("remove")}
+                                  <Icon icon="edit" classes="icon-inline" />
                                 </button>
-                              ) : (
                                 <button
                                   class="btn btn-link btn-animate text-muted"
                                   onClick={linkEvent(
                                     this,
-                                    this.handleModRemoveSubmit
+                                    this.handleDeleteClick
                                   )}
-                                  aria-label={i18n.t("restore")}
+                                  data-tippy-content={
+                                    !cv.comment.deleted
+                                      ? i18n.t("delete")
+                                      : i18n.t("restore")
+                                  }
+                                  aria-label={
+                                    !cv.comment.deleted
+                                      ? i18n.t("delete")
+                                      : i18n.t("restore")
+                                  }
                                 >
-                                  {i18n.t("restore")}
+                                  <Icon
+                                    icon="trash"
+                                    classes={`icon-inline ${
+                                      cv.comment.deleted && "text-danger"
+                                    }`}
+                                  />
                                 </button>
-                              )}
-                            </>
-                          )}
-                          {/* Mods can ban from community, and appoint as mods to community */}
-                          {this.canMod && (
-                            <>
-                              {!this.isMod &&
-                                (!cv.creator_banned_from_community ? (
+                              </>
+                            )}
+                            {/* Admins and mods can remove comments */}
+                            {(canMod_ || canAdmin_) && (
+                              <>
+                                {!cv.comment.removed ? (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
                                     onClick={linkEvent(
                                       this,
-                                      this.handleModBanFromCommunityShow
+                                      this.handleModRemoveShow
                                     )}
-                                    aria-label={i18n.t("ban")}
+                                    aria-label={i18n.t("remove")}
                                   >
-                                    {i18n.t("ban")}
+                                    {i18n.t("remove")}
                                   </button>
                                 ) : (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
                                     onClick={linkEvent(
                                       this,
-                                      this.handleModBanFromCommunitySubmit
+                                      this.handleModRemoveSubmit
                                     )}
-                                    aria-label={i18n.t("unban")}
+                                    aria-label={i18n.t("restore")}
                                   >
-                                    {i18n.t("unban")}
+                                    {i18n.t("restore")}
                                   </button>
-                                ))}
-                              {!cv.creator_banned_from_community &&
-                                (!this.state.showConfirmAppointAsMod ? (
-                                  <button
-                                    class="btn btn-link btn-animate text-muted"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleShowConfirmAppointAsMod
-                                    )}
-                                    aria-label={
-                                      this.isMod
-                                        ? i18n.t("remove_as_mod")
-                                        : i18n.t("appoint_as_mod")
-                                    }
-                                  >
-                                    {this.isMod
-                                      ? i18n.t("remove_as_mod")
-                                      : i18n.t("appoint_as_mod")}
-                                  </button>
-                                ) : (
-                                  <>
+                                )}
+                              </>
+                            )}
+                            {/* Mods can ban from community, and appoint as mods to community */}
+                            {canMod_ && (
+                              <>
+                                {!isMod_ &&
+                                  (!cv.creator_banned_from_community ? (
                                     <button
                                       class="btn btn-link btn-animate text-muted"
-                                      aria-label={i18n.t("are_you_sure")}
+                                      onClick={linkEvent(
+                                        this,
+                                        this.handleModBanFromCommunityShow
+                                      )}
+                                      aria-label={i18n.t("ban")}
                                     >
-                                      {i18n.t("are_you_sure")}
+                                      {i18n.t("ban")}
                                     </button>
+                                  ) : (
                                     <button
                                       class="btn btn-link btn-animate text-muted"
                                       onClick={linkEvent(
                                         this,
-                                        this.handleAddModToCommunity
+                                        this.handleModBanFromCommunitySubmit
                                       )}
-                                      aria-label={i18n.t("yes")}
+                                      aria-label={i18n.t("unban")}
                                     >
-                                      {i18n.t("yes")}
+                                      {i18n.t("unban")}
                                     </button>
+                                  ))}
+                                {!cv.creator_banned_from_community &&
+                                  (!this.state.showConfirmAppointAsMod ? (
                                     <button
                                       class="btn btn-link btn-animate text-muted"
                                       onClick={linkEvent(
                                         this,
-                                        this.handleCancelConfirmAppointAsMod
+                                        this.handleShowConfirmAppointAsMod
                                       )}
-                                      aria-label={i18n.t("no")}
+                                      aria-label={
+                                        isMod_
+                                          ? i18n.t("remove_as_mod")
+                                          : i18n.t("appoint_as_mod")
+                                      }
                                     >
-                                      {i18n.t("no")}
+                                      {isMod_
+                                        ? i18n.t("remove_as_mod")
+                                        : i18n.t("appoint_as_mod")}
                                     </button>
-                                  </>
-                                ))}
-                            </>
-                          )}
-                          {/* Community creators and admins can transfer community to another mod */}
-                          {(this.amCommunityCreator || this.canAdmin) &&
-                            this.isMod &&
-                            cv.creator.local &&
-                            (!this.state.showConfirmTransferCommunity ? (
-                              <button
-                                class="btn btn-link btn-animate text-muted"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleShowConfirmTransferCommunity
-                                )}
-                                aria-label={i18n.t("transfer_community")}
-                              >
-                                {i18n.t("transfer_community")}
-                              </button>
-                            ) : (
-                              <>
-                                <button
-                                  class="btn btn-link btn-animate text-muted"
-                                  aria-label={i18n.t("are_you_sure")}
-                                >
-                                  {i18n.t("are_you_sure")}
-                                </button>
-                                <button
-                                  class="btn btn-link btn-animate text-muted"
-                                  onClick={linkEvent(
-                                    this,
-                                    this.handleTransferCommunity
-                                  )}
-                                  aria-label={i18n.t("yes")}
-                                >
-                                  {i18n.t("yes")}
-                                </button>
+                                  ) : (
+                                    <>
+                                      <button
+                                        class="btn btn-link btn-animate text-muted"
+                                        aria-label={i18n.t("are_you_sure")}
+                                      >
+                                        {i18n.t("are_you_sure")}
+                                      </button>
+                                      <button
+                                        class="btn btn-link btn-animate text-muted"
+                                        onClick={linkEvent(
+                                          this,
+                                          this.handleAddModToCommunity
+                                        )}
+                                        aria-label={i18n.t("yes")}
+                                      >
+                                        {i18n.t("yes")}
+                                      </button>
+                                      <button
+                                        class="btn btn-link btn-animate text-muted"
+                                        onClick={linkEvent(
+                                          this,
+                                          this.handleCancelConfirmAppointAsMod
+                                        )}
+                                        aria-label={i18n.t("no")}
+                                      >
+                                        {i18n.t("no")}
+                                      </button>
+                                    </>
+                                  ))}
+                              </>
+                            )}
+                            {/* Community creators and admins can transfer community to another mod */}
+                            {(amCommunityCreator_ || canAdmin_) &&
+                              isMod_ &&
+                              cv.creator.local &&
+                              (!this.state.showConfirmTransferCommunity ? (
                                 <button
                                   class="btn btn-link btn-animate text-muted"
                                   onClick={linkEvent(
                                     this,
-                                    this
-                                      .handleCancelShowConfirmTransferCommunity
+                                    this.handleShowConfirmTransferCommunity
                                   )}
-                                  aria-label={i18n.t("no")}
+                                  aria-label={i18n.t("transfer_community")}
                                 >
-                                  {i18n.t("no")}
+                                  {i18n.t("transfer_community")}
                                 </button>
-                              </>
-                            ))}
-                          {/* Admins can ban from all, and appoint other admins */}
-                          {this.canAdmin && (
-                            <>
-                              {!this.isAdmin &&
-                                (!isBanned(cv.creator) ? (
+                              ) : (
+                                <>
                                   <button
                                     class="btn btn-link btn-animate text-muted"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleModBanShow
-                                    )}
-                                    aria-label={i18n.t("ban_from_site")}
+                                    aria-label={i18n.t("are_you_sure")}
                                   >
-                                    {i18n.t("ban_from_site")}
+                                    {i18n.t("are_you_sure")}
                                   </button>
-                                ) : (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
                                     onClick={linkEvent(
                                       this,
-                                      this.handleModBanSubmit
+                                      this.handleTransferCommunity
                                     )}
-                                    aria-label={i18n.t("unban_from_site")}
+                                    aria-label={i18n.t("yes")}
                                   >
-                                    {i18n.t("unban_from_site")}
+                                    {i18n.t("yes")}
                                   </button>
-                                ))}
-                              {!isBanned(cv.creator) &&
-                                cv.creator.local &&
-                                (!this.state.showConfirmAppointAsAdmin ? (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
                                     onClick={linkEvent(
                                       this,
-                                      this.handleShowConfirmAppointAsAdmin
+                                      this
+                                        .handleCancelShowConfirmTransferCommunity
                                     )}
-                                    aria-label={
-                                      this.isAdmin
-                                        ? i18n.t("remove_as_admin")
-                                        : i18n.t("appoint_as_admin")
-                                    }
+                                    aria-label={i18n.t("no")}
                                   >
-                                    {this.isAdmin
-                                      ? i18n.t("remove_as_admin")
-                                      : i18n.t("appoint_as_admin")}
+                                    {i18n.t("no")}
                                   </button>
-                                ) : (
-                                  <>
-                                    <button class="btn btn-link btn-animate text-muted">
-                                      {i18n.t("are_you_sure")}
+                                </>
+                              ))}
+                            {/* Admins can ban from all, and appoint other admins */}
+                            {canAdmin_ && (
+                              <>
+                                {!isAdmin_ &&
+                                  (!isBanned(cv.creator) ? (
+                                    <button
+                                      class="btn btn-link btn-animate text-muted"
+                                      onClick={linkEvent(
+                                        this,
+                                        this.handleModBanShow
+                                      )}
+                                      aria-label={i18n.t("ban_from_site")}
+                                    >
+                                      {i18n.t("ban_from_site")}
                                     </button>
+                                  ) : (
                                     <button
                                       class="btn btn-link btn-animate text-muted"
                                       onClick={linkEvent(
                                         this,
-                                        this.handleAddAdmin
+                                        this.handleModBanSubmit
                                       )}
-                                      aria-label={i18n.t("yes")}
+                                      aria-label={i18n.t("unban_from_site")}
                                     >
-                                      {i18n.t("yes")}
+                                      {i18n.t("unban_from_site")}
                                     </button>
+                                  ))}
+                                {!isBanned(cv.creator) &&
+                                  cv.creator.local &&
+                                  (!this.state.showConfirmAppointAsAdmin ? (
                                     <button
                                       class="btn btn-link btn-animate text-muted"
                                       onClick={linkEvent(
                                         this,
-                                        this.handleCancelConfirmAppointAsAdmin
+                                        this.handleShowConfirmAppointAsAdmin
                                       )}
-                                      aria-label={i18n.t("no")}
+                                      aria-label={
+                                        isAdmin_
+                                          ? i18n.t("remove_as_admin")
+                                          : i18n.t("appoint_as_admin")
+                                      }
                                     >
-                                      {i18n.t("no")}
+                                      {isAdmin_
+                                        ? i18n.t("remove_as_admin")
+                                        : i18n.t("appoint_as_admin")}
                                     </button>
-                                  </>
-                                ))}
-                            </>
-                          )}
-                        </>
-                      )}
-                    </>
-                  )}
+                                  ) : (
+                                    <>
+                                      <button class="btn btn-link btn-animate text-muted">
+                                        {i18n.t("are_you_sure")}
+                                      </button>
+                                      <button
+                                        class="btn btn-link btn-animate text-muted"
+                                        onClick={linkEvent(
+                                          this,
+                                          this.handleAddAdmin
+                                        )}
+                                        aria-label={i18n.t("yes")}
+                                      >
+                                        {i18n.t("yes")}
+                                      </button>
+                                      <button
+                                        class="btn btn-link btn-animate text-muted"
+                                        onClick={linkEvent(
+                                          this,
+                                          this.handleCancelConfirmAppointAsAdmin
+                                        )}
+                                        aria-label={i18n.t("no")}
+                                      >
+                                        {i18n.t("no")}
+                                      </button>
+                                    </>
+                                  ))}
+                              </>
+                            )}
+                          </>
+                        )}
+                      </>
+                    )}
                 </div>
                 {/* end of button group */}
               </div>
@@ -713,7 +744,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               id={`mod-remove-reason-${cv.comment.id}`}
               class="form-control mr-2"
               placeholder={i18n.t("reason")}
-              value={this.state.removeReason}
+              value={toUndefined(this.state.removeReason)}
               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
             />
             <button
@@ -765,7 +796,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 id={`mod-ban-reason-${cv.comment.id}`}
                 class="form-control mr-2"
                 placeholder={i18n.t("reason")}
-                value={this.state.banReason}
+                value={toUndefined(this.state.banReason)}
                 onInput={linkEvent(this, this.handleModBanReasonChange)}
               />
               <label
@@ -779,7 +810,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 id={`mod-ban-expires-${cv.comment.id}`}
                 class="form-control mr-2"
                 placeholder={i18n.t("number_of_days")}
-                value={this.state.banExpireDays}
+                value={toUndefined(this.state.banExpireDays)}
                 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
               />
               <div class="form-group">
@@ -819,19 +850,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
         )}
         {this.state.showReply && (
           <CommentForm
-            node={node}
+            node={Left(node)}
             onReplyCancel={this.handleReplyCancel}
             disabled={this.props.locked}
             focus
           />
         )}
-        {node.children && !this.state.collapsed && (
+        {!this.state.collapsed && node.children && (
           <CommentNodes
             nodes={node.children}
             locked={this.props.locked}
             moderators={this.props.moderators}
             admins={this.props.admins}
-            postCreatorId={this.props.postCreatorId}
+            maxCommentsShown={None}
             enableDownvotes={this.props.enableDownvotes}
           />
         )}
@@ -881,82 +912,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   get myComment(): boolean {
-    return (
-      this.props.node.comment_view.creator.id ==
-      UserService.Instance.myUserInfo?.local_user_view.person.id
-    );
-  }
-
-  get isMod(): boolean {
-    return (
-      this.props.moderators &&
-      isMod(
-        this.props.moderators.map(m => m.moderator.id),
-        this.props.node.comment_view.creator.id
-      )
-    );
-  }
-
-  get isAdmin(): boolean {
-    return (
-      this.props.admins &&
-      isMod(
-        this.props.admins.map(a => a.person.id),
-        this.props.node.comment_view.creator.id
+    return UserService.Instance.myUserInfo
+      .map(
+        m =>
+          m.local_user_view.person.id == this.props.node.comment_view.creator.id
       )
-    );
+      .unwrapOr(false);
   }
 
   get isPostCreator(): boolean {
-    return this.props.node.comment_view.creator.id == this.props.postCreatorId;
-  }
-
-  get canMod(): boolean {
-    if (this.props.admins && this.props.moderators) {
-      let adminsThenMods = this.props.admins
-        .map(a => a.person.id)
-        .concat(this.props.moderators.map(m => m.moderator.id));
-
-      return canMod(
-        UserService.Instance.myUserInfo,
-        adminsThenMods,
-        this.props.node.comment_view.creator.id
-      );
-    } else {
-      return false;
-    }
-  }
-
-  get canAdmin(): boolean {
     return (
-      this.props.admins &&
-      canMod(
-        UserService.Instance.myUserInfo,
-        this.props.admins.map(a => a.person.id),
-        this.props.node.comment_view.creator.id
-      )
-    );
-  }
-
-  get amCommunityCreator(): boolean {
-    return (
-      this.props.moderators &&
-      UserService.Instance.myUserInfo &&
-      this.props.node.comment_view.creator.id !=
-        UserService.Instance.myUserInfo.local_user_view.person.id &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        this.props.moderators[0].moderator.id
-    );
-  }
-
-  get amSiteCreator(): boolean {
-    return (
-      this.props.admins &&
-      UserService.Instance.myUserInfo &&
-      this.props.node.comment_view.creator.id !=
-        UserService.Instance.myUserInfo.local_user_view.person.id &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        this.props.admins[0].person.id
+      this.props.node.comment_view.creator.id ==
+      this.props.node.comment_view.post.creator_id
     );
   }
 
@@ -980,32 +947,32 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleBlockUserClick(i: CommentNode) {
-    let blockUserForm: BlockPerson = {
+    let blockUserForm = new BlockPerson({
       person_id: i.props.node.comment_view.creator.id,
       block: true,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
   }
 
   handleDeleteClick(i: CommentNode) {
     let comment = i.props.node.comment_view.comment;
-    let deleteForm: DeleteComment = {
+    let deleteForm = new DeleteComment({
       comment_id: comment.id,
       deleted: !comment.deleted,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
   }
 
   handleSaveCommentClick(i: CommentNode) {
     let cv = i.props.node.comment_view;
     let save = cv.saved == undefined ? true : !cv.saved;
-    let form: SaveComment = {
+    let form = new SaveComment({
       comment_id: cv.comment.id,
       save,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.saveComment(form));
 
@@ -1021,12 +988,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleCommentUpvote(i: CommentNodeI, event: any) {
     event.preventDefault();
-    let new_vote = this.state.my_vote == 1 ? 0 : 1;
+    let myVote = this.state.my_vote.unwrapOr(0);
+    let newVote = myVote == 1 ? 0 : 1;
 
-    if (this.state.my_vote == 1) {
+    if (myVote == 1) {
       this.state.score--;
       this.state.upvotes--;
-    } else if (this.state.my_vote == -1) {
+    } else if (myVote == -1) {
       this.state.downvotes--;
       this.state.upvotes++;
       this.state.score += 2;
@@ -1035,13 +1003,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       this.state.score++;
     }
 
-    this.state.my_vote = new_vote;
+    this.state.my_vote = Some(newVote);
 
-    let form: CreateCommentLike = {
+    let form = new CreateCommentLike({
       comment_id: i.comment_view.comment.id,
-      score: this.state.my_vote,
-      auth: authField(),
-    };
+      score: newVote,
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.likeComment(form));
     this.setState(this.state);
@@ -1050,13 +1018,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleCommentDownvote(i: CommentNodeI, event: any) {
     event.preventDefault();
-    let new_vote = this.state.my_vote == -1 ? 0 : -1;
+    let myVote = this.state.my_vote.unwrapOr(0);
+    let newVote = myVote == -1 ? 0 : -1;
 
-    if (this.state.my_vote == 1) {
+    if (myVote == 1) {
       this.state.score -= 2;
       this.state.upvotes--;
       this.state.downvotes++;
-    } else if (this.state.my_vote == -1) {
+    } else if (myVote == -1) {
       this.state.downvotes--;
       this.state.score++;
     } else {
@@ -1064,13 +1033,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       this.state.score--;
     }
 
-    this.state.my_vote = new_vote;
+    this.state.my_vote = Some(newVote);
 
-    let form: CreateCommentLike = {
+    let form = new CreateCommentLike({
       comment_id: i.comment_view.comment.id,
-      score: this.state.my_vote,
-      auth: authField(),
-    };
+      score: newVote,
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.likeComment(form));
     this.setState(this.state);
@@ -1089,11 +1058,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleReportSubmit(i: CommentNode) {
     let comment = i.props.node.comment_view.comment;
-    let form: CreateCommentReport = {
+    let form = new CreateCommentReport({
       comment_id: comment.id,
       reason: i.state.reportReason,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.createCommentReport(form));
 
     i.state.showReportDialog = false;
@@ -1107,7 +1076,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleModRemoveReasonChange(i: CommentNode, event: any) {
-    i.state.removeReason = event.target.value;
+    i.state.removeReason = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -1118,12 +1087,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleModRemoveSubmit(i: CommentNode) {
     let comment = i.props.node.comment_view.comment;
-    let form: RemoveComment = {
+    let form = new RemoveComment({
       comment_id: comment.id,
       removed: !comment.removed,
       reason: i.state.removeReason,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.removeComment(form));
 
     i.state.showRemoveDialog = false;
@@ -1138,18 +1107,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleMarkRead(i: CommentNode) {
     if (i.isPersonMentionType(i.props.node.comment_view)) {
-      let form: MarkPersonMentionAsRead = {
+      let form = new MarkPersonMentionAsRead({
         person_mention_id: i.props.node.comment_view.person_mention.id,
         read: !i.props.node.comment_view.person_mention.read,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
     } else {
-      let form: MarkCommentAsRead = {
+      let form = new MarkCommentAsRead({
         comment_id: i.props.node.comment_view.comment.id,
         read: !i.props.node.comment_view.comment.read,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
     }
 
@@ -1172,12 +1141,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleModBanReasonChange(i: CommentNode, event: any) {
-    i.state.banReason = event.target.value;
+    i.state.banReason = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleModBanExpireDaysChange(i: CommentNode, event: any) {
-    i.state.banExpireDays = event.target.value;
+    i.state.banExpireDays = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -1202,15 +1171,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       if (ban == false) {
         i.state.removeData = false;
       }
-      let form: BanFromCommunity = {
+      let form = new BanFromCommunity({
         person_id: cv.creator.id,
         community_id: cv.community.id,
         ban,
-        remove_data: i.state.removeData,
+        remove_data: Some(i.state.removeData),
         reason: i.state.banReason,
-        expires: futureDaysToUnixTime(i.state.banExpireDays),
-        auth: authField(),
-      };
+        expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.banFromCommunity(form));
     } else {
       // If its an unban, restore all their data
@@ -1218,14 +1187,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       if (ban == false) {
         i.state.removeData = false;
       }
-      let form: BanPerson = {
+      let form = new BanPerson({
         person_id: cv.creator.id,
         ban,
-        remove_data: i.state.removeData,
+        remove_data: Some(i.state.removeData),
         reason: i.state.banReason,
-        expires: futureDaysToUnixTime(i.state.banExpireDays),
-        auth: authField(),
-      };
+        expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.banPerson(form));
     }
 
@@ -1245,12 +1214,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleAddModToCommunity(i: CommentNode) {
     let cv = i.props.node.comment_view;
-    let form: AddModToCommunity = {
+    let form = new AddModToCommunity({
       person_id: cv.creator.id,
       community_id: cv.community.id,
-      added: !i.isMod,
-      auth: authField(),
-    };
+      added: !isMod(i.props.moderators, cv.creator.id),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
     i.state.showConfirmAppointAsMod = false;
     i.setState(i.state);
@@ -1267,11 +1236,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleAddAdmin(i: CommentNode) {
-    let form: AddAdmin = {
-      person_id: i.props.node.comment_view.creator.id,
-      added: !i.isAdmin,
-      auth: authField(),
-    };
+    let creatorId = i.props.node.comment_view.creator.id;
+    let form = new AddAdmin({
+      person_id: creatorId,
+      added: !isAdmin(i.props.admins, creatorId),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.addAdmin(form));
     i.state.showConfirmAppointAsAdmin = false;
     i.setState(i.state);
@@ -1289,11 +1259,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   handleTransferCommunity(i: CommentNode) {
     let cv = i.props.node.comment_view;
-    let form: TransferCommunity = {
+    let form = new TransferCommunity({
       community_id: cv.community.id,
       person_id: cv.creator.id,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.transferCommunity(form));
     i.state.showConfirmTransferCommunity = false;
     i.setState(i.state);
@@ -1333,9 +1303,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   get scoreColor() {
-    if (this.state.my_vote == 1) {
+    if (this.state.my_vote.unwrapOr(0) == 1) {
       return "text-info";
-    } else if (this.state.my_vote == -1) {
+    } else if (this.state.my_vote.unwrapOr(0) == -1) {
       return "text-danger";
     } else {
       return "text-muted";
index af1610a20aa8278793e87fea2135e4518701af45..62167ec800ae4cfbd066e5ecc6f0f8ed6487acf3 100644 (file)
@@ -1,3 +1,4 @@
+import { Option } from "@sniptt/monads";
 import { Component } from "inferno";
 import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client";
 import { CommentNode as CommentNodeI } from "../../interfaces";
@@ -5,9 +6,9 @@ import { CommentNode } from "./comment-node";
 
 interface CommentNodesProps {
   nodes: CommentNodeI[];
-  moderators?: CommunityModeratorView[];
-  admins?: PersonViewSafe[];
-  postCreatorId?: number;
+  moderators: Option<CommunityModeratorView[]>;
+  admins: Option<PersonViewSafe[]>;
+  maxCommentsShown: Option<number>;
   noBorder?: boolean;
   noIndent?: boolean;
   viewOnly?: boolean;
@@ -15,8 +16,7 @@ interface CommentNodesProps {
   markable?: boolean;
   showContext?: boolean;
   showCommunity?: boolean;
-  maxCommentsShown?: number;
-  enableDownvotes: boolean;
+  enableDownvotes?: boolean;
 }
 
 export class CommentNodes extends Component<CommentNodesProps, any> {
@@ -25,9 +25,9 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
   }
 
   render() {
-    let maxComments = this.props.maxCommentsShown
-      ? this.props.maxCommentsShown
-      : this.props.nodes.length;
+    let maxComments = this.props.maxCommentsShown.unwrapOr(
+      this.props.nodes.length
+    );
 
     return (
       <div className="comments">
@@ -41,7 +41,6 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
             locked={this.props.locked}
             moderators={this.props.moderators}
             admins={this.props.admins}
-            postCreatorId={this.props.postCreatorId}
             markable={this.props.markable}
             showContext={this.props.showContext}
             showCommunity={this.props.showCommunity}
index 8a45648f68fac4f350ce855d71e093f1002a60b0..98668e763147d2490b55f454875b06f4876a3308 100644 (file)
@@ -1,3 +1,4 @@
+import { None } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import {
@@ -8,7 +9,7 @@ import {
 import { i18n } from "../../i18next";
 import { CommentNode as CommentNodeI } from "../../interfaces";
 import { WebSocketService } from "../../services";
-import { authField, wsClient } from "../../utils";
+import { auth, wsClient } from "../../utils";
 import { Icon } from "../common/icon";
 import { PersonListing } from "../person/person-listing";
 import { CommentNode } from "./comment-node";
@@ -42,6 +43,7 @@ export class CommentReport extends Component<CommentReportProps, any> {
       subscribed: false,
       saved: false,
       creator_blocked: false,
+      recipient: None,
       my_vote: r.my_vote,
     };
 
@@ -53,8 +55,8 @@ export class CommentReport extends Component<CommentReportProps, any> {
       <div>
         <CommentNode
           node={node}
-          moderators={[]}
-          admins={[]}
+          moderators={None}
+          admins={None}
           enableDownvotes={true}
           viewOnly={true}
           showCommunity={true}
@@ -65,21 +67,24 @@ export class CommentReport extends Component<CommentReportProps, any> {
         <div>
           {i18n.t("reason")}: {r.comment_report.reason}
         </div>
-        {r.resolver && (
-          <div>
-            {r.comment_report.resolved ? (
-              <T i18nKey="resolved_by">
-                #
-                <PersonListing person={r.resolver} />
-              </T>
-            ) : (
-              <T i18nKey="unresolved_by">
-                #
-                <PersonListing person={r.resolver} />
-              </T>
-            )}
-          </div>
-        )}
+        {r.resolver.match({
+          some: resolver => (
+            <div>
+              {r.comment_report.resolved ? (
+                <T i18nKey="resolved_by">
+                  #
+                  <PersonListing person={resolver} />
+                </T>
+              ) : (
+                <T i18nKey="unresolved_by">
+                  #
+                  <PersonListing person={resolver} />
+                </T>
+              )}
+            </div>
+          ),
+          none: <></>,
+        })}
         <button
           className="btn btn-link btn-animate text-muted py-0"
           onClick={linkEvent(this, this.handleResolveReport)}
@@ -98,11 +103,11 @@ export class CommentReport extends Component<CommentReportProps, any> {
   }
 
   handleResolveReport(i: CommentReport) {
-    let form: ResolveCommentReport = {
+    let form = new ResolveCommentReport({
       report_id: i.props.report.comment_report.id,
       resolved: !i.props.report.comment_report.resolved,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
   }
 }
index 362ac6961311220e6a1c9997e230afe3e037f0b3..d3383b5f6f83cf5bc399d4e67be5153f3c85bb1d 100644 (file)
@@ -1,9 +1,10 @@
+import { Option } from "@sniptt/monads";
 import { Component } from "inferno";
 import { PictrsImage } from "./pictrs-image";
 
 interface BannerIconHeaderProps {
-  banner?: string;
-  icon?: string;
+  banner: Option<string>;
+  icon: Option<string>;
 }
 
 export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
@@ -14,17 +15,21 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
   render() {
     return (
       <div class="position-relative mb-2">
-        {this.props.banner && (
-          <PictrsImage src={this.props.banner} banner alt="" />
-        )}
-        {this.props.icon && (
-          <PictrsImage
-            src={this.props.icon}
-            iconOverlay
-            pushup={!!this.props.banner}
-            alt=""
-          />
-        )}
+        {this.props.banner.match({
+          some: banner => <PictrsImage src={banner} banner alt="" />,
+          none: <></>,
+        })}
+        {this.props.icon.match({
+          some: icon => (
+            <PictrsImage
+              src={icon}
+              iconOverlay
+              pushup={this.props.banner.isSome()}
+              alt=""
+            />
+          ),
+          none: <></>,
+        })}
       </div>
     );
   }
index 9efed1ff5106e6be838897cd78fb01613a5e7487..1997b4f8cee16e62e191f15558d0444c262d4761 100644 (file)
@@ -1,3 +1,4 @@
+import { Option } from "@sniptt/monads";
 import { Component } from "inferno";
 import { Helmet } from "inferno-helmet";
 import { httpExternalPath } from "../../env";
@@ -6,8 +7,8 @@ import { md } from "../../utils";
 interface HtmlTagsProps {
   title: string;
   path: string;
-  description?: string;
-  image?: string;
+  description: Option<string>;
+  image: Option<string>;
 }
 
 /// Taken from https://metatags.io/
@@ -31,14 +32,17 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
         <meta property="twitter:card" content="summary_large_image" />
 
         {/* Optional desc and images */}
-        {this.props.description &&
+        {this.props.description.isSome() &&
           ["description", "og:description", "twitter:description"].map(n => (
-            <meta name={n} content={md.renderInline(this.props.description)} />
+            <meta
+              name={n}
+              content={md.renderInline(this.props.description.unwrap())}
+            />
           ))}
 
-        {this.props.image &&
+        {this.props.image.isSome() &&
           ["og:image", "twitter:image"].map(p => (
-            <meta property={p} content={this.props.image} />
+            <meta property={p} content={this.props.image.unwrap()} />
           ))}
       </Helmet>
     );
index 9b0fb84dfa793fad4cfeddc6a7f31dc7acadfa03..5f7a8168f437518a01c78dfaa354ccb4d7547c29 100644 (file)
@@ -1,3 +1,4 @@
+import { Option } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { pictrsUri } from "../../env";
 import { i18n } from "../../i18next";
@@ -7,7 +8,7 @@ import { Icon } from "./icon";
 
 interface ImageUploadFormProps {
   uploadTitle: string;
-  imageSrc: string;
+  imageSrc: Option<string>;
   onUpload(url: string): any;
   onRemove(): any;
   rounded?: boolean;
@@ -38,26 +39,29 @@ export class ImageUploadForm extends Component<
           htmlFor={this.id}
           class="pointer text-muted small font-weight-bold"
         >
-          {!this.props.imageSrc ? (
-            <span class="btn btn-secondary">{this.props.uploadTitle}</span>
-          ) : (
-            <span class="d-inline-block position-relative">
-              <img
-                src={this.props.imageSrc}
-                height={this.props.rounded ? 60 : ""}
-                width={this.props.rounded ? 60 : ""}
-                className={`img-fluid ${
-                  this.props.rounded ? "rounded-circle" : ""
-                }`}
-              />
-              <a
-                onClick={linkEvent(this, this.handleRemoveImage)}
-                aria-label={i18n.t("remove")}
-              >
-                <Icon icon="x" classes="mini-overlay" />
-              </a>
-            </span>
-          )}
+          {this.props.imageSrc.match({
+            some: imageSrc => (
+              <span class="d-inline-block position-relative">
+                <img
+                  src={imageSrc}
+                  height={this.props.rounded ? 60 : ""}
+                  width={this.props.rounded ? 60 : ""}
+                  className={`img-fluid ${
+                    this.props.rounded ? "rounded-circle" : ""
+                  }`}
+                />
+                <a
+                  onClick={linkEvent(this, this.handleRemoveImage)}
+                  aria-label={i18n.t("remove")}
+                >
+                  <Icon icon="x" classes="mini-overlay" />
+                </a>
+              </span>
+            ),
+            none: (
+              <span class="btn btn-secondary">{this.props.uploadTitle}</span>
+            ),
+          })}
         </label>
         <input
           id={this.id}
@@ -65,7 +69,7 @@ export class ImageUploadForm extends Component<
           accept="image/*,video/*"
           name={this.id}
           class="d-none"
-          disabled={!UserService.Instance.myUserInfo}
+          disabled={UserService.Instance.myUserInfo.isNone()}
           onChange={linkEvent(this, this.handleImageUpload)}
         />
       </form>
index 515f2dc801d3bb0586feeaba68f1726ff1de320c..0ddd0e9abfca57ce1f2301ac092958be9c55c38d 100644 (file)
@@ -46,11 +46,7 @@ export class ListingTypeSelect extends Component<
             title={i18n.t("subscribed_description")}
             className={`btn btn-outline-secondary 
             ${this.state.type_ == ListingType.Subscribed && "active"}
-            ${
-              UserService.Instance.myUserInfo == undefined
-                ? "disabled"
-                : "pointer"
-            }
+            ${UserService.Instance.myUserInfo.isNone() ? "disabled" : "pointer"}
           `}
           >
             <input
@@ -59,7 +55,7 @@ export class ListingTypeSelect extends Component<
               value={ListingType.Subscribed}
               checked={this.state.type_ == ListingType.Subscribed}
               onChange={linkEvent(this, this.handleTypeChange)}
-              disabled={UserService.Instance.myUserInfo == undefined}
+              disabled={UserService.Instance.myUserInfo.isNone()}
             />
             {i18n.t("subscribed")}
           </label>
index af284e5b5d8f6690a9590099e36468caa089c527..e369f8fbb41ba842917bcb12dd5b12855cc4e735 100644 (file)
@@ -1,6 +1,8 @@
+import { None, Option, Some } from "@sniptt/monads";
 import autosize from "autosize";
 import { Component, linkEvent } from "inferno";
 import { Prompt } from "inferno-router";
+import { toUndefined } from "lemmy-js-client";
 import { pictrsUri } from "../../env";
 import { i18n } from "../../i18next";
 import { UserService } from "../../services";
@@ -18,22 +20,22 @@ import {
 import { Icon, Spinner } from "./icon";
 
 interface MarkdownTextAreaProps {
-  initialContent?: string;
-  finished?: boolean;
-  buttonTitle?: string;
+  initialContent: Option<string>;
+  placeholder: Option<string>;
+  buttonTitle: Option<string>;
+  maxLength: Option<number>;
   replyType?: boolean;
   focus?: boolean;
   disabled?: boolean;
-  maxLength?: number;
-  onSubmit?(msg: { val: string; formId: string }): any;
+  finished?: boolean;
+  hideNavigationWarnings?: boolean;
   onContentChange?(val: string): any;
   onReplyCancel?(): any;
-  hideNavigationWarnings?: boolean;
-  placeholder?: string;
+  onSubmit?(msg: { val: string; formId: string }): any;
 }
 
 interface MarkdownTextAreaState {
-  content: string;
+  content: Option<string>;
   previewMode: boolean;
   loading: boolean;
   imageLoading: boolean;
@@ -68,7 +70,7 @@ export class MarkdownTextArea extends Component<
       autosize(textarea);
       this.tribute.attach(textarea);
       textarea.addEventListener("tribute-replaced", () => {
-        this.state.content = textarea.value;
+        this.state.content = Some(textarea.value);
         this.setState(this.state);
         autosize.update(textarea);
       });
@@ -85,7 +87,7 @@ export class MarkdownTextArea extends Component<
   }
 
   componentDidUpdate() {
-    if (!this.props.hideNavigationWarnings && this.state.content) {
+    if (!this.props.hideNavigationWarnings && this.state.content.isSome()) {
       window.onbeforeunload = () => true;
     } else {
       window.onbeforeunload = undefined;
@@ -96,7 +98,7 @@ export class MarkdownTextArea extends Component<
     if (nextProps.finished) {
       this.state.previewMode = false;
       this.state.loading = false;
-      this.state.content = "";
+      this.state.content = None;
       this.setState(this.state);
       if (this.props.replyType) {
         this.props.onReplyCancel();
@@ -118,7 +120,9 @@ export class MarkdownTextArea extends Component<
     return (
       <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
         <Prompt
-          when={!this.props.hideNavigationWarnings && this.state.content}
+          when={
+            !this.props.hideNavigationWarnings && this.state.content.isSome()
+          }
           message={i18n.t("block_leaving")}
         />
         <div class="form-group row">
@@ -126,21 +130,25 @@ export class MarkdownTextArea extends Component<
             <textarea
               id={this.id}
               className={`form-control ${this.state.previewMode && "d-none"}`}
-              value={this.state.content}
+              value={toUndefined(this.state.content)}
               onInput={linkEvent(this, this.handleContentChange)}
               onPaste={linkEvent(this, this.handleImageUploadPaste)}
               required
               disabled={this.props.disabled}
               rows={2}
-              maxLength={this.props.maxLength || 10000}
-              placeholder={this.props.placeholder}
+              maxLength={this.props.maxLength.unwrapOr(10000)}
+              placeholder={toUndefined(this.props.placeholder)}
             />
-            {this.state.previewMode && (
-              <div
-                className="card border-secondary card-body md-div"
-                dangerouslySetInnerHTML={mdToHtml(this.state.content)}
-              />
-            )}
+            {this.state.previewMode &&
+              this.state.content.match({
+                some: content => (
+                  <div
+                    className="card border-secondary card-body md-div"
+                    dangerouslySetInnerHTML={mdToHtml(content)}
+                  />
+                ),
+                none: <></>,
+              })}
           </div>
           <label class="sr-only" htmlFor={this.id}>
             {i18n.t("body")}
@@ -148,19 +156,22 @@ export class MarkdownTextArea extends Component<
         </div>
         <div class="row">
           <div class="col-sm-12 d-flex flex-wrap">
-            {this.props.buttonTitle && (
-              <button
-                type="submit"
-                class="btn btn-sm btn-secondary mr-2"
-                disabled={this.props.disabled || this.state.loading}
-              >
-                {this.state.loading ? (
-                  <Spinner />
-                ) : (
-                  <span>{this.props.buttonTitle}</span>
-                )}
-              </button>
-            )}
+            {this.props.buttonTitle.match({
+              some: buttonTitle => (
+                <button
+                  type="submit"
+                  class="btn btn-sm btn-secondary mr-2"
+                  disabled={this.props.disabled || this.state.loading}
+                >
+                  {this.state.loading ? (
+                    <Spinner />
+                  ) : (
+                    <span>{buttonTitle}</span>
+                  )}
+                </button>
+              ),
+              none: <></>,
+            })}
             {this.props.replyType && (
               <button
                 type="button"
@@ -170,7 +181,7 @@ export class MarkdownTextArea extends Component<
                 {i18n.t("cancel")}
               </button>
             )}
-            {this.state.content && (
+            {this.state.content.isSome() && (
               <button
                 className={`btn btn-sm btn-secondary mr-2 ${
                   this.state.previewMode && "active"
@@ -210,7 +221,7 @@ export class MarkdownTextArea extends Component<
               <label
                 htmlFor={`file-upload-${this.id}`}
                 className={`mb-0 ${
-                  UserService.Instance.myUserInfo && "pointer"
+                  UserService.Instance.myUserInfo.isSome() && "pointer"
                 }`}
                 data-tippy-content={i18n.t("upload_image")}
               >
@@ -226,7 +237,7 @@ export class MarkdownTextArea extends Component<
                 accept="image/*,video/*"
                 name="file"
                 class="d-none"
-                disabled={!UserService.Instance.myUserInfo}
+                disabled={UserService.Instance.myUserInfo.isNone()}
                 onChange={linkEvent(this, this.handleImageUpload)}
               />
             </form>
@@ -344,9 +355,12 @@ export class MarkdownTextArea extends Component<
           let deleteToken = res.files[0].delete_token;
           let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
           let imageMarkdown = `![](${url})`;
-          let content = i.state.content;
-          content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
-          i.state.content = content;
+          i.state.content = Some(
+            i.state.content.match({
+              some: content => `${content}\n${imageMarkdown}`,
+              none: imageMarkdown,
+            })
+          );
           i.state.imageLoading = false;
           i.contentChange();
           i.setState(i.state);
@@ -373,12 +387,12 @@ export class MarkdownTextArea extends Component<
 
   contentChange() {
     if (this.props.onContentChange) {
-      this.props.onContentChange(this.state.content);
+      this.props.onContentChange(toUndefined(this.state.content));
     }
   }
 
   handleContentChange(i: MarkdownTextArea, event: any) {
-    i.state.content = event.target.value;
+    i.state.content = Some(event.target.value);
     i.contentChange();
     i.setState(i.state);
   }
@@ -393,7 +407,7 @@ export class MarkdownTextArea extends Component<
     event.preventDefault();
     i.state.loading = true;
     i.setState(i.state);
-    let msg = { val: i.state.content, formId: i.formId };
+    let msg = { val: toUndefined(i.state.content), formId: i.formId };
     i.props.onSubmit(msg);
   }
 
@@ -403,23 +417,28 @@ export class MarkdownTextArea extends Component<
 
   handleInsertLink(i: MarkdownTextArea, event: any) {
     event.preventDefault();
-    if (!i.state.content) {
-      i.state.content = "";
-    }
+
     let textarea: any = document.getElementById(i.id);
     let start: number = textarea.selectionStart;
     let end: number = textarea.selectionEnd;
 
+    if (i.state.content.isNone()) {
+      i.state.content = Some("");
+    }
+
+    let content = i.state.content.unwrap();
+
     if (start !== end) {
-      let selectedText = i.state.content.substring(start, end);
-      i.state.content = `${i.state.content.substring(
-        0,
-        start
-      )}[${selectedText}]()${i.state.content.substring(end)}`;
+      let selectedText = content.substring(start, end);
+      i.state.content = Some(
+        `${content.substring(0, start)}[${selectedText}]()${content.substring(
+          end
+        )}`
+      );
       textarea.focus();
       setTimeout(() => (textarea.selectionEnd = end + 3), 10);
     } else {
-      i.state.content += "[]()";
+      i.state.content = Some(`${content} []()`);
       textarea.focus();
       setTimeout(() => (textarea.selectionEnd -= 1), 10);
     }
@@ -432,7 +451,7 @@ export class MarkdownTextArea extends Component<
   }
 
   simpleBeginningofLine(chars: string) {
-    this.simpleSurroundBeforeAfter(`${chars} `, "", "");
+    this.simpleSurroundBeforeAfter(`${chars}`, "", "");
   }
 
   simpleSurroundBeforeAfter(
@@ -440,23 +459,27 @@ export class MarkdownTextArea extends Component<
     afterChars: string,
     emptyChars = "___"
   ) {
-    if (!this.state.content) {
-      this.state.content = "";
+    if (this.state.content.isNone()) {
+      this.state.content = Some("");
     }
     let textarea: any = document.getElementById(this.id);
     let start: number = textarea.selectionStart;
     let end: number = textarea.selectionEnd;
 
+    let content = this.state.content.unwrap();
+
     if (start !== end) {
-      let selectedText = this.state.content.substring(start, end);
-      this.state.content = `${this.state.content.substring(
-        0,
-        start
-      )}${beforeChars}${selectedText}${afterChars}${this.state.content.substring(
-        end
-      )}`;
+      let selectedText = content.substring(start, end);
+      this.state.content = Some(
+        `${content.substring(
+          0,
+          start
+        )}${beforeChars}${selectedText}${afterChars}${content.substring(end)}`
+      );
     } else {
-      this.state.content += `${beforeChars}${emptyChars}${afterChars}`;
+      this.state.content = Some(
+        `${content}${beforeChars}${emptyChars}${afterChars}`
+      );
     }
     this.contentChange();
     this.setState(this.state);
@@ -530,10 +553,10 @@ export class MarkdownTextArea extends Component<
   }
 
   simpleInsert(chars: string) {
-    if (!this.state.content) {
-      this.state.content = `${chars} `;
+    if (this.state.content.isNone()) {
+      this.state.content = Some(`${chars} `);
     } else {
-      this.state.content += `\n${chars} `;
+      this.state.content = Some(`${this.state.content.unwrap()}\n${chars} `);
     }
 
     let textarea: any = document.getElementById(this.id);
@@ -561,12 +584,12 @@ export class MarkdownTextArea extends Component<
           .split("\n")
           .map(t => `> ${t}`)
           .join("\n") + "\n\n";
-      if (this.state.content == null) {
-        this.state.content = "";
+      if (this.state.content.isNone()) {
+        this.state.content = Some("");
       } else {
-        this.state.content += "\n";
+        this.state.content = Some(`${this.state.content.unwrap()}\n`);
       }
-      this.state.content += quotedText;
+      this.state.content = Some(`${this.state.content.unwrap()}${quotedText}`);
       this.contentChange();
       this.setState(this.state);
       // Not sure why this needs a delay
@@ -578,6 +601,8 @@ export class MarkdownTextArea extends Component<
     let textarea: any = document.getElementById(this.id);
     let start: number = textarea.selectionStart;
     let end: number = textarea.selectionEnd;
-    return start !== end ? this.state.content.substring(start, end) : "";
+    return start !== end
+      ? this.state.content.unwrap().substring(start, end)
+      : "";
   }
 }
index 1afc979628086e1284877f8cde02ccf8eb6b2cf8..3d3c5485c9058176b276477a34a2d149fb1ef9a6 100644 (file)
@@ -1,3 +1,4 @@
+import { Option } from "@sniptt/monads";
 import { Component } from "inferno";
 import moment from "moment";
 import { i18n } from "../../i18next";
@@ -5,11 +6,8 @@ import { capitalizeFirstLetter, getLanguages } from "../../utils";
 import { Icon } from "./icon";
 
 interface MomentTimeProps {
-  data: {
-    published?: string;
-    when_?: string;
-    updated?: string;
-  };
+  published: string;
+  updated: Option<string>;
   showAgo?: boolean;
   ignoreUpdated?: boolean;
 }
@@ -24,40 +22,32 @@ export class MomentTime extends Component<MomentTimeProps, any> {
   }
 
   createdAndModifiedTimes() {
-    let created = this.props.data.published || this.props.data.when_;
-    return `
-      <div>
-        <div>
-          ${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(created)}
-        </div>
-        <div>
-          ${capitalizeFirstLetter(i18n.t("modified"))} ${this.format(
-      this.props.data.updated
-    )}
-        </div>
-        </div>`;
+    return `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
+      this.props.published
+    )}\n\n\n${
+      this.props.updated.isSome() && capitalizeFirstLetter(i18n.t("modified"))
+    } ${this.format(this.props.updated.unwrap())}`;
   }
 
   render() {
-    if (!this.props.ignoreUpdated && this.props.data.updated) {
+    if (!this.props.ignoreUpdated && this.props.updated.isSome()) {
       return (
         <span
           data-tippy-content={this.createdAndModifiedTimes()}
-          data-tippy-allowHtml={true}
           className="font-italics pointer unselectable"
         >
           <Icon icon="edit-2" classes="icon-inline mr-1" />
-          {moment.utc(this.props.data.updated).fromNow(!this.props.showAgo)}
+          {moment.utc(this.props.updated.unwrap()).fromNow(!this.props.showAgo)}
         </span>
       );
     } else {
-      let created = this.props.data.published || this.props.data.when_;
+      let published = this.props.published;
       return (
         <span
           className="pointer unselectable"
-          data-tippy-content={this.format(created)}
+          data-tippy-content={this.format(published)}
         >
-          {moment.utc(created).fromNow(!this.props.showAgo)}
+          {moment.utc(published).fromNow(!this.props.showAgo)}
         </span>
       );
     }
index cad47b832536a89949c34d9192f600e89a92d2a9..36875b85db8b5a799778fc5e55f6551f82ce9282 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import {
@@ -6,7 +7,7 @@ import {
 } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
-import { authField, mdToHtml, wsClient } from "../../utils";
+import { auth, mdToHtml, wsClient } from "../../utils";
 import { PersonListing } from "../person/person-listing";
 import { MarkdownTextArea } from "./markdown-textarea";
 import { MomentTime } from "./moment-time";
@@ -16,7 +17,7 @@ interface RegistrationApplicationProps {
 }
 
 interface RegistrationApplicationState {
-  denyReason?: string;
+  denyReason: Option<string>;
   denyExpanded: boolean;
 }
 
@@ -47,35 +48,44 @@ export class RegistrationApplication extends Component<
           {i18n.t("applicant")}: <PersonListing person={a.creator} />
         </div>
         <div>
-          {i18n.t("created")}: <MomentTime showAgo data={ra} />
+          {i18n.t("created")}:{" "}
+          <MomentTime showAgo published={ra.published} updated={None} />
         </div>
         <div>{i18n.t("answer")}:</div>
         <div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} />
 
-        {a.admin && (
-          <div>
-            {accepted ? (
-              <T i18nKey="approved_by">
-                #
-                <PersonListing person={a.admin} />
-              </T>
-            ) : (
-              <div>
-                <T i18nKey="denied_by">
+        {a.admin.match({
+          some: admin => (
+            <div>
+              {accepted ? (
+                <T i18nKey="approved_by">
                   #
-                  <PersonListing person={a.admin} />
+                  <PersonListing person={admin} />
                 </T>
+              ) : (
                 <div>
-                  {i18n.t("deny_reason")}:{" "}
-                  <div
-                    className="md-div d-inline-flex"
-                    dangerouslySetInnerHTML={mdToHtml(ra.deny_reason || "")}
-                  />
+                  <T i18nKey="denied_by">
+                    #
+                    <PersonListing person={admin} />
+                  </T>
+                  {ra.deny_reason.match({
+                    some: deny_reason => (
+                      <div>
+                        {i18n.t("deny_reason")}:{" "}
+                        <div
+                          className="md-div d-inline-flex"
+                          dangerouslySetInnerHTML={mdToHtml(deny_reason)}
+                        />
+                      </div>
+                    ),
+                    none: <></>,
+                  })}
                 </div>
-              </div>
-            )}
-          </div>
-        )}
+              )}
+            </div>
+          ),
+          none: <></>,
+        })}
 
         {this.state.denyExpanded && (
           <div class="form-group row">
@@ -86,6 +96,9 @@ export class RegistrationApplication extends Component<
               <MarkdownTextArea
                 initialContent={this.state.denyReason}
                 onContentChange={this.handleDenyReasonChange}
+                placeholder={None}
+                buttonTitle={None}
+                maxLength={None}
                 hideNavigationWarnings
               />
             </div>
@@ -115,12 +128,12 @@ export class RegistrationApplication extends Component<
 
   handleApprove(i: RegistrationApplication) {
     i.setState({ denyExpanded: false });
-    let form: ApproveRegistrationApplication = {
+    let form = new ApproveRegistrationApplication({
       id: i.props.application.registration_application.id,
-      deny_reason: "",
+      deny_reason: None,
       approve: true,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(
       wsClient.approveRegistrationApplication(form)
     );
@@ -129,12 +142,12 @@ export class RegistrationApplication extends Component<
   handleDeny(i: RegistrationApplication) {
     if (i.state.denyExpanded) {
       i.setState({ denyExpanded: false });
-      let form: ApproveRegistrationApplication = {
+      let form = new ApproveRegistrationApplication({
         id: i.props.application.registration_application.id,
         approve: false,
         deny_reason: i.state.denyReason,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(
         wsClient.approveRegistrationApplication(form)
       );
@@ -144,7 +157,7 @@ export class RegistrationApplication extends Component<
   }
 
   handleDenyReasonChange(val: string) {
-    this.state.denyReason = val;
+    this.state.denyReason = Some(val);
     this.setState(this.state);
   }
 }
index edc27303c88eb2cb089157706cd8666e3aa712ce..a9d4a6c1e84bde3081387d9941263122289decf9 100644 (file)
@@ -1,33 +1,32 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   CommunityResponse,
-  CommunityView,
   FollowCommunity,
+  GetSiteResponse,
   ListCommunities,
   ListCommunitiesResponse,
   ListingType,
-  SiteView,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { InitialFetchRequest } from "shared/interfaces";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   getListingTypeFromPropsNoDefault,
   getPageFromProps,
   isBrowser,
   numToSI,
   setIsoData,
-  setOptionalAuth,
   showLocal,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -38,10 +37,10 @@ import { CommunityLink } from "./community-link";
 const communityLimit = 100;
 
 interface CommunitiesState {
-  communities: CommunityView[];
+  listCommunitiesResponse: Option<ListCommunitiesResponse>;
   page: number;
   loading: boolean;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
   searchText: string;
   listingType: ListingType;
 }
@@ -53,13 +52,13 @@ interface CommunitiesProps {
 
 export class Communities extends Component<any, CommunitiesState> {
   private subscription: Subscription;
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, ListCommunitiesResponse);
   private emptyState: CommunitiesState = {
-    communities: [],
+    listCommunitiesResponse: None,
     loading: true,
     page: getPageFromProps(this.props),
     listingType: getListingTypeFromPropsNoDefault(this.props),
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
     searchText: "",
   };
 
@@ -74,7 +73,8 @@ export class Communities extends Component<any, CommunitiesState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.communities = this.isoData.routeData[0].communities;
+      let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse);
+      this.state.listCommunitiesResponse = listRes;
       this.state.loading = false;
     } else {
       this.refetch();
@@ -105,7 +105,10 @@ export class Communities extends Component<any, CommunitiesState> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("communities")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("communities")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -114,6 +117,8 @@ export class Communities extends Component<any, CommunitiesState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         {this.state.loading ? (
           <h5>
@@ -157,48 +162,51 @@ export class Communities extends Component<any, CommunitiesState> {
                   </tr>
                 </thead>
                 <tbody>
-                  {this.state.communities.map(cv => (
-                    <tr>
-                      <td>
-                        <CommunityLink community={cv.community} />
-                      </td>
-                      <td class="text-right">
-                        {numToSI(cv.counts.subscribers)}
-                      </td>
-                      <td class="text-right">
-                        {numToSI(cv.counts.users_active_month)}
-                      </td>
-                      <td class="text-right d-none d-lg-table-cell">
-                        {numToSI(cv.counts.posts)}
-                      </td>
-                      <td class="text-right d-none d-lg-table-cell">
-                        {numToSI(cv.counts.comments)}
-                      </td>
-                      <td class="text-right">
-                        {cv.subscribed ? (
-                          <button
-                            class="btn btn-link d-inline-block"
-                            onClick={linkEvent(
-                              cv.community.id,
-                              this.handleUnsubscribe
-                            )}
-                          >
-                            {i18n.t("unsubscribe")}
-                          </button>
-                        ) : (
-                          <button
-                            class="btn btn-link d-inline-block"
-                            onClick={linkEvent(
-                              cv.community.id,
-                              this.handleSubscribe
-                            )}
-                          >
-                            {i18n.t("subscribe")}
-                          </button>
-                        )}
-                      </td>
-                    </tr>
-                  ))}
+                  {this.state.listCommunitiesResponse
+                    .map(l => l.communities)
+                    .unwrapOr([])
+                    .map(cv => (
+                      <tr>
+                        <td>
+                          <CommunityLink community={cv.community} />
+                        </td>
+                        <td class="text-right">
+                          {numToSI(cv.counts.subscribers)}
+                        </td>
+                        <td class="text-right">
+                          {numToSI(cv.counts.users_active_month)}
+                        </td>
+                        <td class="text-right d-none d-lg-table-cell">
+                          {numToSI(cv.counts.posts)}
+                        </td>
+                        <td class="text-right d-none d-lg-table-cell">
+                          {numToSI(cv.counts.comments)}
+                        </td>
+                        <td class="text-right">
+                          {cv.subscribed ? (
+                            <button
+                              class="btn btn-link d-inline-block"
+                              onClick={linkEvent(
+                                cv.community.id,
+                                this.handleUnsubscribe
+                              )}
+                            >
+                              {i18n.t("unsubscribe")}
+                            </button>
+                          ) : (
+                            <button
+                              class="btn btn-link d-inline-block"
+                              onClick={linkEvent(
+                                cv.community.id,
+                                this.handleSubscribe
+                              )}
+                            >
+                              {i18n.t("subscribe")}
+                            </button>
+                          )}
+                        </td>
+                      </tr>
+                    ))}
                 </tbody>
               </table>
             </div>
@@ -258,20 +266,20 @@ export class Communities extends Component<any, CommunitiesState> {
   }
 
   handleUnsubscribe(communityId: number) {
-    let form: FollowCommunity = {
+    let form = new FollowCommunity({
       community_id: communityId,
       follow: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.followCommunity(form));
   }
 
   handleSubscribe(communityId: number) {
-    let form: FollowCommunity = {
+    let form = new FollowCommunity({
       community_id: communityId,
       follow: true,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.followCommunity(form));
   }
 
@@ -287,13 +295,13 @@ export class Communities extends Component<any, CommunitiesState> {
   }
 
   refetch() {
-    let listCommunitiesForm: ListCommunities = {
-      type_: this.state.listingType,
-      sort: SortType.TopMonth,
-      limit: communityLimit,
-      page: this.state.page,
-      auth: authField(false),
-    };
+    let listCommunitiesForm = new ListCommunities({
+      type_: Some(this.state.listingType),
+      sort: Some(SortType.TopMonth),
+      limit: Some(communityLimit),
+      page: Some(this.state.page),
+      auth: auth(false).ok(),
+    });
 
     WebSocketService.Instance.send(
       wsClient.listCommunities(listCommunitiesForm)
@@ -302,17 +310,17 @@ export class Communities extends Component<any, CommunitiesState> {
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let pathSplit = req.path.split("/");
-    let type_: ListingType = pathSplit[3]
-      ? ListingType[pathSplit[3]]
-      : ListingType.Local;
-    let page = pathSplit[5] ? Number(pathSplit[5]) : 1;
-    let listCommunitiesForm: ListCommunities = {
+    let type_: Option<ListingType> = Some(
+      pathSplit[3] ? ListingType[pathSplit[3]] : ListingType.Local
+    );
+    let page = Some(pathSplit[5] ? Number(pathSplit[5]) : 1);
+    let listCommunitiesForm = new ListCommunities({
       type_,
-      sort: SortType.TopMonth,
-      limit: communityLimit,
+      sort: Some(SortType.TopMonth),
+      limit: Some(communityLimit),
       page,
-    };
-    setOptionalAuth(listCommunitiesForm, req.auth);
+      auth: req.auth,
+    });
 
     return [req.client.listCommunities(listCommunitiesForm)];
   }
@@ -324,18 +332,26 @@ export class Communities extends Component<any, CommunitiesState> {
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.ListCommunities) {
-      let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
-      this.state.communities = data.communities;
+      let data = wsJsonToRes<ListCommunitiesResponse>(
+        msg,
+        ListCommunitiesResponse
+      );
+      this.state.listCommunitiesResponse = Some(data);
       this.state.loading = false;
       window.scrollTo(0, 0);
       this.setState(this.state);
     } else if (op == UserOperation.FollowCommunity) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
-      let found = this.state.communities.find(
-        c => c.community.id == data.community_view.community.id
-      );
-      found.subscribed = data.community_view.subscribed;
-      found.counts.subscribers = data.community_view.counts.subscribers;
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
+      this.state.listCommunitiesResponse.match({
+        some: res => {
+          let found = res.communities.find(
+            c => c.community.id == data.community_view.community.id
+          );
+          found.subscribed = data.community_view.subscribed;
+          found.counts.subscribers = data.community_view.counts.subscribers;
+        },
+        none: void 0,
+      });
       this.setState(this.state);
     }
   }
index 0f12a252dccc12d1a23aca7616899ae38ee79e5e..d1f5f7534c18f6eebeafc0806509d7d66f62d46a 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Prompt } from "inferno-router";
 import {
@@ -5,30 +6,31 @@ import {
   CommunityView,
   CreateCommunity,
   EditCommunity,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   randomStr,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { Icon, Spinner } from "../common/icon";
 import { ImageUploadForm } from "../common/image-upload-form";
 import { MarkdownTextArea } from "../common/markdown-textarea";
 
 interface CommunityFormProps {
-  community_view?: CommunityView; // If a community is given, that means this is an edit
+  community_view: Option<CommunityView>; // If a community is given, that means this is an edit
   onCancel?(): any;
   onCreate?(community: CommunityView): any;
   onEdit?(community: CommunityView): any;
-  enableNsfw: boolean;
+  enableNsfw?: boolean;
 }
 
 interface CommunityFormState {
@@ -44,15 +46,16 @@ export class CommunityForm extends Component<
   private subscription: Subscription;
 
   private emptyState: CommunityFormState = {
-    communityForm: {
-      name: null,
-      title: null,
-      nsfw: false,
-      icon: null,
-      banner: null,
-      posting_restricted_to_mods: false,
-      auth: authField(false),
-    },
+    communityForm: new CreateCommunity({
+      name: undefined,
+      title: undefined,
+      description: None,
+      nsfw: None,
+      icon: None,
+      banner: None,
+      posting_restricted_to_mods: None,
+      auth: undefined,
+    }),
     loading: false,
   };
 
@@ -70,31 +73,34 @@ export class CommunityForm extends Component<
     this.handleBannerUpload = this.handleBannerUpload.bind(this);
     this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
-    let cv = this.props.community_view;
-    if (cv) {
-      this.state.communityForm = {
-        name: cv.community.name,
-        title: cv.community.title,
-        description: cv.community.description,
-        nsfw: cv.community.nsfw,
-        icon: cv.community.icon,
-        banner: cv.community.banner,
-        posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
-        auth: authField(),
-      };
-    }
+    this.props.community_view.match({
+      some: cv => {
+        this.state.communityForm = new CreateCommunity({
+          name: cv.community.name,
+          title: cv.community.title,
+          description: cv.community.description,
+          nsfw: Some(cv.community.nsfw),
+          icon: cv.community.icon,
+          banner: cv.community.banner,
+          posting_restricted_to_mods: Some(
+            cv.community.posting_restricted_to_mods
+          ),
+          auth: auth().unwrap(),
+        });
+      },
+      none: void 0,
+    });
 
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
   }
 
-  // TODO this should be checked out
   componentDidUpdate() {
     if (
       !this.state.loading &&
       (this.state.communityForm.name ||
         this.state.communityForm.title ||
-        this.state.communityForm.description)
+        this.state.communityForm.description.isSome())
     ) {
       window.onbeforeunload = () => true;
     } else {
@@ -115,12 +121,12 @@ export class CommunityForm extends Component<
             !this.state.loading &&
             (this.state.communityForm.name ||
               this.state.communityForm.title ||
-              this.state.communityForm.description)
+              this.state.communityForm.description.isSome())
           }
           message={i18n.t("block_leaving")}
         />
         <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
-          {!this.props.community_view && (
+          {this.props.community_view.isNone() && (
             <div class="form-group row">
               <label
                 class="col-12 col-sm-2 col-form-label"
@@ -205,6 +211,9 @@ export class CommunityForm extends Component<
             <div class="col-12 col-sm-10">
               <MarkdownTextArea
                 initialContent={this.state.communityForm.description}
+                placeholder={Some("description")}
+                buttonTitle={None}
+                maxLength={None}
                 onContentChange={this.handleCommunityDescriptionChange}
               />
             </div>
@@ -221,7 +230,7 @@ export class CommunityForm extends Component<
                     class="form-check-input position-static"
                     id="community-nsfw"
                     type="checkbox"
-                    checked={this.state.communityForm.nsfw}
+                    checked={toUndefined(this.state.communityForm.nsfw)}
                     onChange={linkEvent(this, this.handleCommunityNsfwChange)}
                   />
                 </div>
@@ -238,7 +247,9 @@ export class CommunityForm extends Component<
                   class="form-check-input position-static"
                   id="community-only-mods-can-post"
                   type="checkbox"
-                  checked={this.state.communityForm.posting_restricted_to_mods}
+                  checked={toUndefined(
+                    this.state.communityForm.posting_restricted_to_mods
+                  )}
                   onChange={linkEvent(
                     this,
                     this.handleCommunityPostingRestrictedToMods
@@ -256,13 +267,13 @@ export class CommunityForm extends Component<
               >
                 {this.state.loading ? (
                   <Spinner />
-                ) : this.props.community_view ? (
+                ) : this.props.community_view.isSome() ? (
                   capitalizeFirstLetter(i18n.t("save"))
                 ) : (
                   capitalizeFirstLetter(i18n.t("create"))
                 )}
               </button>
-              {this.props.community_view && (
+              {this.props.community_view.isSome() && (
                 <button
                   type="button"
                   class="btn btn-secondary"
@@ -281,17 +292,29 @@ export class CommunityForm extends Component<
   handleCreateCommunitySubmit(i: CommunityForm, event: any) {
     event.preventDefault();
     i.state.loading = true;
-    if (i.props.community_view) {
-      let form: EditCommunity = {
-        ...i.state.communityForm,
-        community_id: i.props.community_view.community.id,
-      };
-      WebSocketService.Instance.send(wsClient.editCommunity(form));
-    } else {
-      WebSocketService.Instance.send(
-        wsClient.createCommunity(i.state.communityForm)
-      );
-    }
+    let cForm = i.state.communityForm;
+
+    i.props.community_view.match({
+      some: cv => {
+        let form = new EditCommunity({
+          community_id: cv.community.id,
+          title: Some(cForm.title),
+          description: cForm.description,
+          icon: cForm.icon,
+          banner: cForm.banner,
+          nsfw: cForm.nsfw,
+          posting_restricted_to_mods: cForm.posting_restricted_to_mods,
+          auth: auth().unwrap(),
+        });
+
+        WebSocketService.Instance.send(wsClient.editCommunity(form));
+      },
+      none: () => {
+        WebSocketService.Instance.send(
+          wsClient.createCommunity(i.state.communityForm)
+        );
+      },
+    });
     i.setState(i.state);
   }
 
@@ -306,7 +329,7 @@ export class CommunityForm extends Component<
   }
 
   handleCommunityDescriptionChange(val: string) {
-    this.state.communityForm.description = val;
+    this.state.communityForm.description = Some(val);
     this.setState(this.state);
   }
 
@@ -325,22 +348,22 @@ export class CommunityForm extends Component<
   }
 
   handleIconUpload(url: string) {
-    this.state.communityForm.icon = url;
+    this.state.communityForm.icon = Some(url);
     this.setState(this.state);
   }
 
   handleIconRemove() {
-    this.state.communityForm.icon = "";
+    this.state.communityForm.icon = Some("");
     this.setState(this.state);
   }
 
   handleBannerUpload(url: string) {
-    this.state.communityForm.banner = url;
+    this.state.communityForm.banner = Some(url);
     this.setState(this.state);
   }
 
   handleBannerRemove() {
-    this.state.communityForm.banner = "";
+    this.state.communityForm.banner = Some("");
     this.setState(this.state);
   }
 
@@ -354,42 +377,51 @@ export class CommunityForm extends Component<
       this.setState(this.state);
       return;
     } else if (op == UserOperation.CreateCommunity) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
       this.state.loading = false;
       this.props.onCreate(data.community_view);
 
       // Update myUserInfo
       let community = data.community_view.community;
-      let person = UserService.Instance.myUserInfo.local_user_view.person;
-      UserService.Instance.myUserInfo.follows.push({
-        community,
-        follower: person,
-      });
-      UserService.Instance.myUserInfo.moderates.push({
-        community,
-        moderator: person,
+
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          let person = mui.local_user_view.person;
+          mui.follows.push({
+            community,
+            follower: person,
+          });
+          mui.moderates.push({
+            community,
+            moderator: person,
+          });
+        },
+        none: void 0,
       });
     } else if (op == UserOperation.EditCommunity) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
       this.state.loading = false;
       this.props.onEdit(data.community_view);
       let community = data.community_view.community;
 
-      let followFound = UserService.Instance.myUserInfo.follows.findIndex(
-        f => f.community.id == community.id
-      );
-      if (followFound) {
-        UserService.Instance.myUserInfo.follows[followFound].community =
-          community;
-      }
-
-      let moderatesFound = UserService.Instance.myUserInfo.moderates.findIndex(
-        f => f.community.id == community.id
-      );
-      if (moderatesFound) {
-        UserService.Instance.myUserInfo.moderates[moderatesFound].community =
-          community;
-      }
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          let followFound = mui.follows.findIndex(
+            f => f.community.id == community.id
+          );
+          if (followFound) {
+            mui.follows[followFound].community = community;
+          }
+
+          let moderatesFound = mui.moderates.findIndex(
+            f => f.community.id == community.id
+          );
+          if (moderatesFound) {
+            mui.moderates[moderatesFound].community = community;
+          }
+        },
+        none: void 0,
+      });
     }
   }
 }
index 4ea7f8aefcc9f2bab8965c09c6b7f861277edbf1..cc987053246c4a7ed2db183209ea80b5f5492ff3 100644 (file)
@@ -56,12 +56,14 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
   }
 
   avatarAndName(displayName: string) {
-    let community = this.props.community;
     return (
       <>
-        {!this.props.hideAvatar && community.icon && showAvatars() && (
-          <PictrsImage src={community.icon} icon />
-        )}
+        {!this.props.hideAvatar &&
+          showAvatars() &&
+          this.props.community.icon.match({
+            some: icon => <PictrsImage src={icon} icon />,
+            none: <></>,
+          })}
         <span class="overflow-wrap-anywhere">{displayName}</span>
       </>
     );
index 0dd6f03e4e35be13d762c3205e482b65a845f86d..732cba2f9037faf47f02c2ea4500ea85c09344c3 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   AddModToCommunityResponse,
@@ -19,20 +20,25 @@ import {
   PostResponse,
   PostView,
   SortType,
+  toOption,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { DataType, InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   commentsToFlatNodes,
   communityRSSUrl,
   createCommentLikeRes,
   createPostLikeFindRes,
   editCommentRes,
   editPostFindRes,
+  enableDownvotes,
+  enableNsfw,
   fetchLimit,
   getDataTypeFromProps,
   getPageFromProps,
@@ -43,15 +49,12 @@ import {
   saveCommentRes,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   setupTippy,
   showLocal,
   toast,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentNodes } from "../comment/comment-nodes";
 import { BannerIconHeader } from "../common/banner-icon-header";
@@ -66,7 +69,7 @@ import { PostListings } from "../post/post-listings";
 import { CommunityLink } from "./community-link";
 
 interface State {
-  communityRes: GetCommunityResponse;
+  communityRes: Option<GetCommunityResponse>;
   siteRes: GetSiteResponse;
   communityName: string;
   communityLoading: boolean;
@@ -93,10 +96,15 @@ interface UrlParams {
 }
 
 export class Community extends Component<any, State> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetCommunityResponse,
+    GetPostsResponse,
+    GetCommentsResponse
+  );
   private subscription: Subscription;
   private emptyState: State = {
-    communityRes: undefined,
+    communityRes: None,
     communityName: this.props.match.params.name,
     communityLoading: true,
     postsLoading: true,
@@ -123,12 +131,21 @@ export class Community extends Component<any, State> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.communityRes = this.isoData.routeData[0];
-      if (this.state.dataType == DataType.Post) {
-        this.state.posts = this.isoData.routeData[1].posts;
-      } else {
-        this.state.comments = this.isoData.routeData[1].comments;
-      }
+      this.state.communityRes = Some(
+        this.isoData.routeData[0] as GetCommunityResponse
+      );
+      let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse);
+      let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse);
+
+      postsRes.match({
+        some: pvs => (this.state.posts = pvs.posts),
+        none: void 0,
+      });
+      commentsRes.match({
+        some: cvs => (this.state.comments = cvs.comments),
+        none: void 0,
+      });
+
       this.state.communityLoading = false;
       this.state.postsLoading = false;
       this.state.commentsLoading = false;
@@ -139,10 +156,11 @@ export class Community extends Component<any, State> {
   }
 
   fetchCommunity() {
-    let form: GetCommunity = {
-      name: this.state.communityName ? this.state.communityName : null,
-      auth: authField(false),
-    };
+    let form = new GetCommunity({
+      name: Some(this.state.communityName),
+      id: None,
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getCommunity(form));
   }
 
@@ -169,56 +187,62 @@ export class Community extends Component<any, State> {
     let promises: Promise<any>[] = [];
 
     let communityName = pathSplit[2];
-    let communityForm: GetCommunity = { name: communityName };
-    setOptionalAuth(communityForm, req.auth);
+    let communityForm = new GetCommunity({
+      name: Some(communityName),
+      id: None,
+      auth: req.auth,
+    });
     promises.push(req.client.getCommunity(communityForm));
 
     let dataType: DataType = pathSplit[4]
       ? DataType[pathSplit[4]]
       : DataType.Post;
 
-    let sort: SortType = pathSplit[6]
-      ? SortType[pathSplit[6]]
-      : UserService.Instance.myUserInfo
-      ? Object.values(SortType)[
-          UserService.Instance.myUserInfo.local_user_view.local_user
-            .default_sort_type
-        ]
-      : SortType.Active;
+    let sort: Option<SortType> = toOption(
+      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 = pathSplit[8] ? Number(pathSplit[8]) : 1;
+    let page = toOption(pathSplit[8] ? Number(pathSplit[8]) : 1);
 
     if (dataType == DataType.Post) {
-      let getPostsForm: GetPosts = {
+      let getPostsForm = new GetPosts({
+        community_name: Some(communityName),
+        community_id: None,
         page,
-        limit: fetchLimit,
+        limit: Some(fetchLimit),
         sort,
-        type_: ListingType.Community,
-        saved_only: false,
-      };
-      setOptionalAuth(getPostsForm, req.auth);
-      this.setName(getPostsForm, communityName);
+        type_: Some(ListingType.Community),
+        saved_only: Some(false),
+        auth: req.auth,
+      });
       promises.push(req.client.getPosts(getPostsForm));
+      promises.push(Promise.resolve());
     } else {
-      let getCommentsForm: GetComments = {
+      let getCommentsForm = new GetComments({
+        community_name: Some(communityName),
+        community_id: None,
         page,
-        limit: fetchLimit,
+        limit: Some(fetchLimit),
         sort,
-        type_: ListingType.Community,
-        saved_only: false,
-      };
-      this.setName(getCommentsForm, communityName);
-      setOptionalAuth(getCommentsForm, req.auth);
+        type_: Some(ListingType.Community),
+        saved_only: Some(false),
+        auth: req.auth,
+      });
+      promises.push(Promise.resolve());
       promises.push(req.client.getComments(getCommentsForm));
     }
 
     return promises;
   }
 
-  static setName(obj: any, name_: string) {
-    obj.community_name = name_;
-  }
-
   componentDidUpdate(_: any, lastState: State) {
     if (
       lastState.dataType !== this.state.dataType ||
@@ -231,11 +255,18 @@ export class Community extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${this.state.communityRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`;
+    return this.state.communityRes.match({
+      some: res =>
+        this.state.siteRes.site_view.match({
+          some: siteView =>
+            `${res.community_view.community.title} - ${siteView.site.name}`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
   render() {
-    let cv = this.state.communityRes?.community_view;
     return (
       <div class="container">
         {this.state.communityLoading ? (
@@ -243,83 +274,99 @@ export class Community extends Component<any, State> {
             <Spinner large />
           </h5>
         ) : (
-          <>
-            <HtmlTags
-              title={this.documentTitle}
-              path={this.context.router.route.match.url}
-              description={cv.community.description}
-              image={cv.community.icon}
-            />
+          this.state.communityRes.match({
+            some: res => (
+              <>
+                <HtmlTags
+                  title={this.documentTitle}
+                  path={this.context.router.route.match.url}
+                  description={res.community_view.community.description}
+                  image={res.community_view.community.icon}
+                />
 
-            <div class="row">
-              <div class="col-12 col-md-8">
-                {this.communityInfo()}
-                <div class="d-block d-md-none">
-                  <button
-                    class="btn btn-secondary d-inline-block mb-2 mr-3"
-                    onClick={linkEvent(this, this.handleShowSidebarMobile)}
-                  >
-                    {i18n.t("sidebar")}{" "}
-                    <Icon
-                      icon={
-                        this.state.showSidebarMobile
-                          ? `minus-square`
-                          : `plus-square`
-                      }
-                      classes="icon-inline"
-                    />
-                  </button>
-                  {this.state.showSidebarMobile && (
-                    <>
-                      <Sidebar
-                        community_view={cv}
-                        moderators={this.state.communityRes.moderators}
-                        admins={this.state.siteRes.admins}
-                        online={this.state.communityRes.online}
-                        enableNsfw={
-                          this.state.siteRes.site_view.site.enable_nsfw
-                        }
-                      />
-                      {!cv.community.local && this.state.communityRes.site && (
-                        <SiteSidebar
-                          site={this.state.communityRes.site}
-                          showLocal={showLocal(this.isoData)}
+                <div class="row">
+                  <div class="col-12 col-md-8">
+                    {this.communityInfo()}
+                    <div class="d-block d-md-none">
+                      <button
+                        class="btn btn-secondary d-inline-block mb-2 mr-3"
+                        onClick={linkEvent(this, this.handleShowSidebarMobile)}
+                      >
+                        {i18n.t("sidebar")}{" "}
+                        <Icon
+                          icon={
+                            this.state.showSidebarMobile
+                              ? `minus-square`
+                              : `plus-square`
+                          }
+                          classes="icon-inline"
                         />
+                      </button>
+                      {this.state.showSidebarMobile && (
+                        <>
+                          <Sidebar
+                            community_view={res.community_view}
+                            moderators={res.moderators}
+                            admins={this.state.siteRes.admins}
+                            online={res.online}
+                            enableNsfw={enableNsfw(this.state.siteRes)}
+                          />
+                          {!res.community_view.community.local &&
+                            res.site.match({
+                              some: site => (
+                                <SiteSidebar
+                                  site={site}
+                                  showLocal={showLocal(this.isoData)}
+                                  admins={None}
+                                  counts={None}
+                                  online={None}
+                                />
+                              ),
+                              none: <></>,
+                            })}
+                        </>
                       )}
-                    </>
-                  )}
+                    </div>
+                    {this.selects()}
+                    {this.listings()}
+                    <Paginator
+                      page={this.state.page}
+                      onChange={this.handlePageChange}
+                    />
+                  </div>
+                  <div class="d-none d-md-block col-md-4">
+                    <Sidebar
+                      community_view={res.community_view}
+                      moderators={res.moderators}
+                      admins={this.state.siteRes.admins}
+                      online={res.online}
+                      enableNsfw={enableNsfw(this.state.siteRes)}
+                    />
+                    {!res.community_view.community.local &&
+                      res.site.match({
+                        some: site => (
+                          <SiteSidebar
+                            site={site}
+                            showLocal={showLocal(this.isoData)}
+                            admins={None}
+                            counts={None}
+                            online={None}
+                          />
+                        ),
+                        none: <></>,
+                      })}
+                  </div>
                 </div>
-                {this.selects()}
-                {this.listings()}
-                <Paginator
-                  page={this.state.page}
-                  onChange={this.handlePageChange}
-                />
-              </div>
-              <div class="d-none d-md-block col-md-4">
-                <Sidebar
-                  community_view={cv}
-                  moderators={this.state.communityRes.moderators}
-                  admins={this.state.siteRes.admins}
-                  online={this.state.communityRes.online}
-                  enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
-                />
-                {!cv.community.local && this.state.communityRes.site && (
-                  <SiteSidebar
-                    site={this.state.communityRes.site}
-                    showLocal={showLocal(this.isoData)}
-                  />
-                )}
-              </div>
-            </div>
-          </>
+              </>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
   }
 
   listings() {
-    let site = this.state.siteRes.site_view.site;
     return this.state.dataType == DataType.Post ? (
       this.state.postsLoading ? (
         <h5>
@@ -329,8 +376,8 @@ export class Community extends Component<any, State> {
         <PostListings
           posts={this.state.posts}
           removeDuplicates
-          enableDownvotes={site.enable_downvotes}
-          enableNsfw={site.enable_nsfw}
+          enableDownvotes={enableDownvotes(this.state.siteRes)}
+          enableNsfw={enableNsfw(this.state.siteRes)}
         />
       )
     ) : this.state.commentsLoading ? (
@@ -342,32 +389,38 @@ export class Community extends Component<any, State> {
         nodes={commentsToFlatNodes(this.state.comments)}
         noIndent
         showContext
-        enableDownvotes={site.enable_downvotes}
+        enableDownvotes={enableDownvotes(this.state.siteRes)}
+        moderators={this.state.communityRes.map(r => r.moderators)}
+        admins={Some(this.state.siteRes.admins)}
+        maxCommentsShown={None}
       />
     );
   }
 
   communityInfo() {
-    let community = this.state.communityRes.community_view.community;
-    return (
-      <div class="mb-2">
-        <BannerIconHeader banner={community.banner} icon={community.icon} />
-        <h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5>
-        <CommunityLink
-          community={community}
-          realLink
-          useApubName
-          muted
-          hideAvatar
-        />
-      </div>
-    );
+    return this.state.communityRes
+      .map(r => r.community_view.community)
+      .match({
+        some: community => (
+          <div class="mb-2">
+            <BannerIconHeader banner={community.banner} icon={community.icon} />
+            <h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5>
+            <CommunityLink
+              community={community}
+              realLink
+              useApubName
+              muted
+              hideAvatar
+            />
+          </div>
+        ),
+        none: <></>,
+      });
   }
 
   selects() {
-    let communityRss = communityRSSUrl(
-      this.state.communityRes.community_view.community.actor_id,
-      this.state.sort
+    let communityRss = this.state.communityRes.map(r =>
+      communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
     );
     return (
       <div class="mb-3">
@@ -380,10 +433,17 @@ export class Community extends Component<any, State> {
         <span class="mr-2">
           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
         </span>
-        <a href={communityRss} title="RSS" rel={relTags}>
-          <Icon icon="rss" classes="text-muted small" />
-        </a>
-        <link rel="alternate" type="application/atom+xml" href={communityRss} />
+        {communityRss.match({
+          some: rss => (
+            <>
+              <a href={rss} title="RSS" rel={relTags}>
+                <Icon icon="rss" classes="text-muted small" />
+              </a>
+              <link rel="alternate" type="application/atom+xml" href={rss} />
+            </>
+          ),
+          none: <></>,
+        })}
       </div>
     );
   }
@@ -422,26 +482,28 @@ export class Community extends Component<any, State> {
 
   fetchData() {
     if (this.state.dataType == DataType.Post) {
-      let form: GetPosts = {
-        page: this.state.page,
-        limit: fetchLimit,
-        sort: this.state.sort,
-        type_: ListingType.Community,
-        community_name: this.state.communityName,
-        saved_only: false,
-        auth: authField(false),
-      };
+      let form = new GetPosts({
+        page: Some(this.state.page),
+        limit: Some(fetchLimit),
+        sort: Some(this.state.sort),
+        type_: Some(ListingType.Community),
+        community_name: Some(this.state.communityName),
+        community_id: None,
+        saved_only: Some(false),
+        auth: auth(false).ok(),
+      });
       WebSocketService.Instance.send(wsClient.getPosts(form));
     } else {
-      let form: GetComments = {
-        page: this.state.page,
-        limit: fetchLimit,
-        sort: this.state.sort,
-        type_: ListingType.Community,
-        community_name: this.state.communityName,
-        saved_only: false,
-        auth: authField(false),
-      };
+      let form = new GetComments({
+        page: Some(this.state.page),
+        limit: Some(fetchLimit),
+        sort: Some(this.state.sort),
+        type_: Some(ListingType.Community),
+        community_name: Some(this.state.communityName),
+        community_id: None,
+        saved_only: Some(false),
+        auth: auth(false).ok(),
+      });
       WebSocketService.Instance.send(wsClient.getComments(form));
     }
   }
@@ -454,15 +516,20 @@ export class Community extends Component<any, State> {
       this.context.router.history.push("/");
       return;
     } else if (msg.reconnect) {
-      WebSocketService.Instance.send(
-        wsClient.communityJoin({
-          community_id: this.state.communityRes.community_view.community.id,
-        })
-      );
+      this.state.communityRes.match({
+        some: res => {
+          WebSocketService.Instance.send(
+            wsClient.communityJoin({
+              community_id: res.community_view.community.id,
+            })
+          );
+        },
+        none: void 0,
+      });
       this.fetchData();
     } else if (op == UserOperation.GetCommunity) {
-      let data = wsJsonToRes<GetCommunityResponse>(msg).data;
-      this.state.communityRes = data;
+      let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
+      this.state.communityRes = Some(data);
       this.state.communityLoading = false;
       this.setState(this.state);
       // TODO why is there no auth in this form?
@@ -476,18 +543,25 @@ export class Community extends Component<any, State> {
       op == UserOperation.DeleteCommunity ||
       op == UserOperation.RemoveCommunity
     ) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
-      this.state.communityRes.community_view = data.community_view;
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
+      this.state.communityRes.match({
+        some: res => (res.community_view = data.community_view),
+        none: void 0,
+      });
       this.setState(this.state);
     } else if (op == UserOperation.FollowCommunity) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
-      this.state.communityRes.community_view.subscribed =
-        data.community_view.subscribed;
-      this.state.communityRes.community_view.counts.subscribers =
-        data.community_view.counts.subscribers;
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
+      this.state.communityRes.match({
+        some: res => {
+          res.community_view.subscribed = data.community_view.subscribed;
+          res.community_view.counts.subscribers =
+            data.community_view.counts.subscribers;
+        },
+        none: void 0,
+      });
       this.setState(this.state);
     } else if (op == UserOperation.GetPosts) {
-      let data = wsJsonToRes<GetPostsResponse>(msg).data;
+      let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
       this.state.posts = data.posts;
       this.state.postsLoading = false;
       this.setState(this.state);
@@ -501,29 +575,39 @@ export class Community extends Component<any, State> {
       op == UserOperation.StickyPost ||
       op == UserOperation.SavePost
     ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       editPostFindRes(data.post_view, this.state.posts);
       this.setState(this.state);
     } else if (op == UserOperation.CreatePost) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       this.state.posts.unshift(data.post_view);
       if (
-        UserService.Instance.myUserInfo?.local_user_view.local_user
-          .show_new_post_notifs
+        UserService.Instance.myUserInfo
+          .map(m => m.local_user_view.local_user.show_new_post_notifs)
+          .unwrapOr(false)
       ) {
         notifyPost(data.post_view, this.context.router);
       }
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       createPostLikeFindRes(data.post_view, this.state.posts);
       this.setState(this.state);
     } else if (op == UserOperation.AddModToCommunity) {
-      let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
-      this.state.communityRes.moderators = data.moderators;
+      let data = wsJsonToRes<AddModToCommunityResponse>(
+        msg,
+        AddModToCommunityResponse
+      );
+      this.state.communityRes.match({
+        some: res => (res.moderators = data.moderators),
+        none: void 0,
+      });
       this.setState(this.state);
     } else if (op == UserOperation.BanFromCommunity) {
-      let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
+      let data = wsJsonToRes<BanFromCommunityResponse>(
+        msg,
+        BanFromCommunityResponse
+      );
 
       // TODO this might be incorrect
       this.state.posts
@@ -532,7 +616,7 @@ export class Community extends Component<any, State> {
 
       this.setState(this.state);
     } else if (op == UserOperation.GetComments) {
-      let data = wsJsonToRes<GetCommentsResponse>(msg).data;
+      let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
       this.state.comments = data.comments;
       this.state.commentsLoading = false;
       this.setState(this.state);
@@ -541,11 +625,11 @@ export class Community extends Component<any, State> {
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       editCommentRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
 
       // Necessary since it might be a user reply
       if (data.form_id) {
@@ -553,23 +637,23 @@ export class Community extends Component<any, State> {
         this.setState(this.state);
       }
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       saveCommentRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       createCommentLikeRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
     } else if (op == UserOperation.CreatePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
+      let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
     } else if (op == UserOperation.CreateCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
+      let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
index f40bfd21e87ca993a0bc66f4e4e7e0b2d190d7a8..9a36678433b964492b394e4ea34223f88a2aa15a 100644 (file)
@@ -1,15 +1,22 @@
+import { None } from "@sniptt/monads";
 import { Component } from "inferno";
-import { CommunityView, SiteView } from "lemmy-js-client";
+import { CommunityView, GetSiteResponse } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { UserService } from "../../services";
-import { isBrowser, setIsoData, toast, wsSubscribe } from "../../utils";
+import {
+  enableNsfw,
+  isBrowser,
+  setIsoData,
+  toast,
+  wsSubscribe,
+} from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
 import { CommunityForm } from "./community-form";
 
 interface CreateCommunityState {
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
   loading: boolean;
 }
 
@@ -17,7 +24,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
   private isoData = setIsoData(this.context);
   private subscription: Subscription;
   private emptyState: CreateCommunityState = {
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
     loading: false,
   };
   constructor(props: any, context: any) {
@@ -28,7 +35,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -41,7 +48,10 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("create_community")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("create_community")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -50,6 +60,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         {this.state.loading ? (
           <h5>
@@ -60,8 +72,9 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
             <div class="col-12 col-lg-6 offset-lg-3 mb-4">
               <h5>{i18n.t("create_community")}</h5>
               <CommunityForm
+                community_view={None}
                 onCreate={this.handleCommunityCreate}
-                enableNsfw={this.state.site_view.site.enable_nsfw}
+                enableNsfw={enableNsfw(this.state.siteRes)}
               />
             </div>
           </div>
index baa655fb8f8234fc030d90667db4003f0a0cdf94..159080a1aba9184c8a62184af04040eecf7996ca 100644 (file)
@@ -1,3 +1,4 @@
+import { Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import {
@@ -8,11 +9,15 @@ import {
   FollowCommunity,
   PersonViewSafe,
   RemoveCommunity,
+  toUndefined,
 } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  amAdmin,
+  amMod,
+  amTopMod,
+  auth,
   getUnixTime,
   mdToHtml,
   numToSI,
@@ -29,15 +34,15 @@ interface SidebarProps {
   moderators: CommunityModeratorView[];
   admins: PersonViewSafe[];
   online: number;
-  enableNsfw: boolean;
+  enableNsfw?: boolean;
   showIcon?: boolean;
 }
 
 interface SidebarState {
+  removeReason: Option<string>;
+  removeExpires: Option<string>;
   showEdit: boolean;
   showRemoveDialog: boolean;
-  removeReason: string;
-  removeExpires: string;
   showConfirmLeaveModTeam: boolean;
 }
 
@@ -64,7 +69,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
           this.sidebar()
         ) : (
           <CommunityForm
-            community_view={this.props.community_view}
+            community_view={Some(this.props.community_view)}
             onEdit={this.handleEditCommunity}
             onCancel={this.handleEditCancel}
             enableNsfw={this.props.enableNsfw}
@@ -284,14 +289,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   description() {
     let description = this.props.community_view.community.description;
-    return (
-      description && (
-        <div
-          className="md-div"
-          dangerouslySetInnerHTML={mdToHtml(description)}
-        />
-      )
-    );
+    return description.match({
+      some: desc => (
+        <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
+      ),
+      none: <></>,
+    });
   }
 
   adminButtons() {
@@ -299,7 +302,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     return (
       <>
         <ul class="list-inline mb-1 text-muted font-weight-bold">
-          {this.canMod && (
+          {amMod(Some(this.props.moderators)) && (
             <>
               <li className="list-inline-item-action">
                 <button
@@ -311,7 +314,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                   <Icon icon="edit" classes="icon-inline" />
                 </button>
               </li>
-              {!this.amTopMod &&
+              {!amTopMod(Some(this.props.moderators)) &&
                 (!this.state.showConfirmLeaveModTeam ? (
                   <li className="list-inline-item-action">
                     <button
@@ -350,7 +353,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                     </li>
                   </>
                 ))}
-              {this.amTopMod && (
+              {amTopMod(Some(this.props.moderators)) && (
                 <li className="list-inline-item-action">
                   <button
                     class="btn btn-link text-muted d-inline-block"
@@ -377,7 +380,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
               )}
             </>
           )}
-          {this.canAdmin && (
+          {amAdmin(Some(this.props.admins)) && (
             <li className="list-inline-item">
               {!this.props.community_view.community.removed ? (
                 <button
@@ -408,7 +411,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                 id="remove-reason"
                 class="form-control mr-2"
                 placeholder={i18n.t("optional")}
-                value={this.state.removeReason}
+                value={toUndefined(this.state.removeReason)}
                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
               />
             </div>
@@ -445,11 +448,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   handleDeleteClick(i: Sidebar, event: any) {
     event.preventDefault();
-    let deleteForm: DeleteCommunity = {
+    let deleteForm = new DeleteCommunity({
       community_id: i.props.community_view.community.id,
       deleted: !i.props.community_view.community.deleted,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
   }
 
@@ -459,15 +462,20 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   }
 
   handleLeaveModTeamClick(i: Sidebar) {
-    let form: AddModToCommunity = {
-      person_id: UserService.Instance.myUserInfo.local_user_view.person.id,
-      community_id: i.props.community_view.community.id,
-      added: false,
-      auth: authField(),
-    };
-    WebSocketService.Instance.send(wsClient.addModToCommunity(form));
-    i.state.showConfirmLeaveModTeam = false;
-    i.setState(i.state);
+    UserService.Instance.myUserInfo.match({
+      some: mui => {
+        let form = new AddModToCommunity({
+          person_id: mui.local_user_view.person.id,
+          community_id: i.props.community_view.community.id,
+          added: false,
+          auth: auth().unwrap(),
+        });
+        WebSocketService.Instance.send(wsClient.addModToCommunity(form));
+        i.state.showConfirmLeaveModTeam = false;
+        i.setState(i.state);
+      },
+      none: void 0,
+    });
   }
 
   handleCancelLeaveModTeamClick(i: Sidebar) {
@@ -478,67 +486,47 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   handleUnsubscribe(i: Sidebar, event: any) {
     event.preventDefault();
     let community_id = i.props.community_view.community.id;
-    let form: FollowCommunity = {
+    let form = new FollowCommunity({
       community_id,
       follow: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.followCommunity(form));
 
     // Update myUserInfo
-    UserService.Instance.myUserInfo.follows =
-      UserService.Instance.myUserInfo.follows.filter(
-        i => i.community.id != community_id
-      );
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        (mui.follows = mui.follows.filter(i => i.community.id != community_id)),
+      none: void 0,
+    });
   }
 
   handleSubscribe(i: Sidebar, event: any) {
     event.preventDefault();
     let community_id = i.props.community_view.community.id;
-    let form: FollowCommunity = {
+    let form = new FollowCommunity({
       community_id,
       follow: true,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.followCommunity(form));
 
     // Update myUserInfo
-    UserService.Instance.myUserInfo.follows.push({
-      community: i.props.community_view.community,
-      follower: UserService.Instance.myUserInfo.local_user_view.person,
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        mui.follows.push({
+          community: i.props.community_view.community,
+          follower: mui.local_user_view.person,
+        }),
+      none: void 0,
     });
   }
 
-  private get amTopMod(): boolean {
-    return (
-      this.props.moderators[0].moderator.id ==
-      UserService.Instance.myUserInfo.local_user_view.person.id
-    );
-  }
-
-  get canMod(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      this.props.moderators
-        .map(m => m.moderator.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
-    );
-  }
-
-  get canAdmin(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      this.props.admins
-        .map(a => a.person.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
-    );
-  }
-
   get canPost(): boolean {
     return (
       !this.props.community_view.community.posting_restricted_to_mods ||
-      this.canMod ||
-      this.canAdmin
+      amMod(Some(this.props.moderators)) ||
+      amAdmin(Some(this.props.admins))
     );
   }
 
@@ -560,13 +548,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   handleModRemoveSubmit(i: Sidebar, event: any) {
     event.preventDefault();
-    let removeForm: RemoveCommunity = {
+    let removeForm = new RemoveCommunity({
       community_id: i.props.community_view.community.id,
       removed: !i.props.community_view.community.removed,
       reason: i.state.removeReason,
-      expires: getUnixTime(i.state.removeExpires),
-      auth: authField(),
-    };
+      expires: i.state.removeExpires.map(getUnixTime),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
 
     i.state.showRemoveDialog = false;
index cb7848426c08b58cbf3e20a5dff2123e4a3b0d0a..67a000c5df44e751575ac9d2490f6cb60715fe48 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import autosize from "autosize";
 import { Component, linkEvent } from "inferno";
 import {
@@ -9,14 +10,17 @@ import {
   PersonViewSafe,
   SaveSiteConfig,
   SiteResponse,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { InitialFetchRequest } from "../../interfaces";
 import { WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   isBrowser,
   randomStr,
@@ -24,9 +28,7 @@ import {
   showLocal,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -35,24 +37,26 @@ import { SiteForm } from "./site-form";
 
 interface AdminSettingsState {
   siteRes: GetSiteResponse;
-  siteConfigRes: GetSiteConfigResponse;
-  siteConfigHjson: string;
-  loading: boolean;
+  siteConfigRes: Option<GetSiteConfigResponse>;
+  siteConfigHjson: Option<string>;
   banned: PersonViewSafe[];
+  loading: boolean;
   siteConfigLoading: boolean;
   leaveAdminTeamLoading: boolean;
 }
 
 export class AdminSettings extends Component<any, AdminSettingsState> {
   private siteConfigTextAreaId = `site-config-${randomStr()}`;
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetSiteConfigResponse,
+    BannedPersonsResponse
+  );
   private subscription: Subscription;
   private emptyState: AdminSettingsState = {
     siteRes: this.isoData.site_res,
-    siteConfigHjson: null,
-    siteConfigRes: {
-      config_hjson: null,
-    },
+    siteConfigHjson: None,
+    siteConfigRes: None,
     banned: [],
     loading: true,
     siteConfigLoading: null,
@@ -69,20 +73,26 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.siteConfigRes = this.isoData.routeData[0];
-      this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson;
-      this.state.banned = this.isoData.routeData[1].banned;
+      this.state.siteConfigRes = Some(
+        this.isoData.routeData[0] as GetSiteConfigResponse
+      );
+      this.state.siteConfigHjson = this.state.siteConfigRes.map(
+        s => s.config_hjson
+      );
+      this.state.banned = (
+        this.isoData.routeData[1] as BannedPersonsResponse
+      ).banned;
       this.state.siteConfigLoading = false;
       this.state.loading = false;
     } else {
       WebSocketService.Instance.send(
         wsClient.getSiteConfig({
-          auth: authField(),
+          auth: auth().unwrap(),
         })
       );
       WebSocketService.Instance.send(
         wsClient.getBannedPersons({
-          auth: authField(),
+          auth: auth().unwrap(),
         })
       );
     }
@@ -91,10 +101,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let promises: Promise<any>[] = [];
 
-    let siteConfigForm: GetSiteConfig = { auth: req.auth };
+    let siteConfigForm = new GetSiteConfig({ auth: req.auth.unwrap() });
     promises.push(req.client.getSiteConfig(siteConfigForm));
 
-    let bannedPersonsForm: GetBannedPersons = { auth: req.auth };
+    let bannedPersonsForm = new GetBannedPersons({ auth: req.auth.unwrap() });
     promises.push(req.client.getBannedPersons(bannedPersonsForm));
 
     return promises;
@@ -114,9 +124,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("admin_settings")} - ${
-      this.state.siteRes.site_view.site.name
-    }`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("admin_settings")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -132,13 +143,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
               <HtmlTags
                 title={this.documentTitle}
                 path={this.context.router.route.match.url}
+                description={None}
+                image={None}
               />
-              {this.state.siteRes.site_view.site.id && (
-                <SiteForm
-                  site={this.state.siteRes.site_view.site}
-                  showLocal={showLocal(this.isoData)}
-                />
-              )}
+              {this.state.siteRes.site_view.match({
+                some: siteView => (
+                  <SiteForm
+                    site={Some(siteView.site)}
+                    showLocal={showLocal(this.isoData)}
+                  />
+                ),
+                none: <></>,
+              })}
               {this.admins()}
               {this.bannedUsers()}
             </div>
@@ -210,7 +226,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
             <div class="col-12">
               <textarea
                 id={this.siteConfigTextAreaId}
-                value={this.state.siteConfigHjson}
+                value={toUndefined(this.state.siteConfigHjson)}
                 onInput={linkEvent(this, this.handleSiteConfigHjsonChange)}
                 class="form-control text-monospace"
                 rows={3}
@@ -236,10 +252,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
   handleSiteConfigSubmit(i: AdminSettings, event: any) {
     event.preventDefault();
     i.state.siteConfigLoading = true;
-    let form: SaveSiteConfig = {
-      config_hjson: i.state.siteConfigHjson,
-      auth: authField(),
-    };
+    let form = new SaveSiteConfig({
+      config_hjson: toUndefined(i.state.siteConfigHjson),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.saveSiteConfig(form));
     i.setState(i.state);
   }
@@ -251,7 +267,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
 
   handleLeaveAdminTeam(i: AdminSettings) {
     i.state.leaveAdminTeamLoading = true;
-    WebSocketService.Instance.send(wsClient.leaveAdmin({ auth: authField() }));
+    WebSocketService.Instance.send(
+      wsClient.leaveAdmin({ auth: auth().unwrap() })
+    );
     i.setState(i.state);
   }
 
@@ -265,24 +283,26 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       this.setState(this.state);
       return;
     } else if (op == UserOperation.EditSite) {
-      let data = wsJsonToRes<SiteResponse>(msg).data;
-      this.state.siteRes.site_view = data.site_view;
+      let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
+      this.state.siteRes.site_view = Some(data.site_view);
       this.setState(this.state);
       toast(i18n.t("site_saved"));
     } else if (op == UserOperation.GetBannedPersons) {
-      let data = wsJsonToRes<BannedPersonsResponse>(msg).data;
+      let data = wsJsonToRes<BannedPersonsResponse>(msg, BannedPersonsResponse);
       this.state.banned = data.banned;
       this.setState(this.state);
     } else if (op == UserOperation.GetSiteConfig) {
-      let data = wsJsonToRes<GetSiteConfigResponse>(msg).data;
-      this.state.siteConfigRes = data;
+      let data = wsJsonToRes<GetSiteConfigResponse>(msg, GetSiteConfigResponse);
+      this.state.siteConfigRes = Some(data);
       this.state.loading = false;
-      this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson;
+      this.state.siteConfigHjson = this.state.siteConfigRes.map(
+        s => s.config_hjson
+      );
       this.setState(this.state);
       var textarea: any = document.getElementById(this.siteConfigTextAreaId);
       autosize(textarea);
     } else if (op == UserOperation.LeaveAdmin) {
-      let data = wsJsonToRes<GetSiteResponse>(msg).data;
+      let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
       this.state.siteRes.site_view = data.site_view;
       this.setState(this.state);
       this.state.leaveAdminTeamLoading = false;
@@ -290,9 +310,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       this.setState(this.state);
       this.context.router.history.push("/");
     } else if (op == UserOperation.SaveSiteConfig) {
-      let data = wsJsonToRes<GetSiteConfigResponse>(msg).data;
-      this.state.siteConfigRes = data;
-      this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson;
+      let data = wsJsonToRes<GetSiteConfigResponse>(msg, GetSiteConfigResponse);
+      this.state.siteConfigRes = Some(data);
+      this.state.siteConfigHjson = this.state.siteConfigRes.map(
+        s => s.config_hjson
+      );
       this.state.siteConfigLoading = false;
       toast(i18n.t("site_saved"));
       this.setState(this.state);
index 6a9365c45e7343def72a6b5630f7df9ea458cbd3..5ae35883b4fd7fff885bd8a6ef8fed9753349369 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { Link } from "inferno-router";
@@ -23,38 +24,41 @@ import {
   SiteResponse,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { DataType, InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   commentsToFlatNodes,
   createCommentLikeRes,
   createPostLikeFindRes,
   editCommentRes,
   editPostFindRes,
+  enableDownvotes,
+  enableNsfw,
   fetchLimit,
   getDataTypeFromProps,
   getListingTypeFromProps,
   getPageFromProps,
   getSortTypeFromProps,
+  isBrowser,
   notifyPost,
   relTags,
   restoreScrollPosition,
   saveCommentRes,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   setupTippy,
   showLocal,
   toast,
+  trendingFetchLimit,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentNodes } from "../comment/comment-nodes";
 import { DataTypeSelect } from "../common/data-type-select";
@@ -70,17 +74,17 @@ import { SiteSidebar } from "./site-sidebar";
 interface HomeState {
   trendingCommunities: CommunityView[];
   siteRes: GetSiteResponse;
-  showSubscribedMobile: boolean;
-  showTrendingMobile: boolean;
-  showSidebarMobile: boolean;
-  subscribedCollapsed: boolean;
-  loading: boolean;
   posts: PostView[];
   comments: CommentView[];
   listingType: ListingType;
   dataType: DataType;
   sort: SortType;
   page: number;
+  showSubscribedMobile: boolean;
+  showTrendingMobile: boolean;
+  showSidebarMobile: boolean;
+  subscribedCollapsed: boolean;
+  loading: boolean;
 }
 
 interface HomeProps {
@@ -98,7 +102,12 @@ interface UrlParams {
 }
 
 export class Home extends Component<any, HomeState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetPostsResponse,
+    GetCommentsResponse,
+    ListCommunitiesResponse
+  );
   private subscription: Subscription;
   private emptyState: HomeState = {
     trendingCommunities: [],
@@ -113,7 +122,7 @@ export class Home extends Component<any, HomeState> {
     listingType: getListingTypeFromProps(
       this.props,
       ListingType[
-        this.isoData.site_res.site_view?.site.default_post_listing_type
+        this.isoData.site_res.site_view.unwrap().site.default_post_listing_type
       ]
     ),
     dataType: getDataTypeFromProps(this.props),
@@ -135,12 +144,25 @@ export class Home extends Component<any, HomeState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      if (this.state.dataType == DataType.Post) {
-        this.state.posts = this.isoData.routeData[0].posts;
-      } else {
-        this.state.comments = this.isoData.routeData[0].comments;
+      let postsRes = Some(this.isoData.routeData[0] as GetPostsResponse);
+      let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse);
+      let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse;
+
+      postsRes.match({
+        some: pvs => (this.state.posts = pvs.posts),
+        none: void 0,
+      });
+      commentsRes.match({
+        some: cvs => (this.state.comments = cvs.comments),
+        none: void 0,
+      });
+      this.state.trendingCommunities = trendingRes.communities;
+
+      if (isBrowser()) {
+        WebSocketService.Instance.send(
+          wsClient.communityJoin({ community_id: 0 })
+        );
       }
-      this.state.trendingCommunities = this.isoData.routeData[1].communities;
       this.state.loading = false;
     } else {
       this.fetchTrendingCommunities();
@@ -149,12 +171,13 @@ export class Home extends Component<any, HomeState> {
   }
 
   fetchTrendingCommunities() {
-    let listCommunitiesForm: ListCommunities = {
-      type_: ListingType.Local,
-      sort: SortType.Hot,
-      limit: 6,
-      auth: authField(false),
-    };
+    let listCommunitiesForm = new ListCommunities({
+      type_: Some(ListingType.Local),
+      sort: Some(SortType.Hot),
+      limit: Some(trendingFetchLimit),
+      page: None,
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(
       wsClient.listCommunities(listCommunitiesForm)
     );
@@ -165,8 +188,6 @@ export class Home extends Component<any, HomeState> {
     if (!this.state.siteRes.site_view) {
       this.context.router.history.push("/setup");
     }
-
-    WebSocketService.Instance.send(wsClient.communityJoin({ community_id: 0 }));
     setupTippy();
   }
 
@@ -192,58 +213,69 @@ export class Home extends Component<any, HomeState> {
       : DataType.Post;
 
     // TODO figure out auth default_listingType, default_sort_type
-    let type_: ListingType = pathSplit[5]
-      ? ListingType[pathSplit[5]]
-      : UserService.Instance.myUserInfo
-      ? Object.values(ListingType)[
-          UserService.Instance.myUserInfo.local_user_view.local_user
-            .default_listing_type
-        ]
-      : null;
-    let sort: SortType = pathSplit[7]
-      ? SortType[pathSplit[7]]
-      : UserService.Instance.myUserInfo
-      ? Object.values(SortType)[
-          UserService.Instance.myUserInfo.local_user_view.local_user
-            .default_sort_type
-        ]
-      : SortType.Active;
-
-    let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
+    let type_: Option<ListingType> = Some(
+      pathSplit[5]
+        ? ListingType[pathSplit[5]]
+        : UserService.Instance.myUserInfo.match({
+            some: mui =>
+              Object.values(ListingType)[
+                mui.local_user_view.local_user.default_listing_type
+              ],
+            none: ListingType.Local,
+          })
+    );
+    let sort: Option<SortType> = Some(
+      pathSplit[7]
+        ? SortType[pathSplit[7]]
+        : UserService.Instance.myUserInfo.match({
+            some: mui =>
+              Object.values(SortType)[
+                mui.local_user_view.local_user.default_sort_type
+              ],
+            none: SortType.Active,
+          })
+    );
+
+    let page = Some(pathSplit[9] ? Number(pathSplit[9]) : 1);
 
     let promises: Promise<any>[] = [];
 
     if (dataType == DataType.Post) {
-      let getPostsForm: GetPosts = {
+      let getPostsForm = new GetPosts({
+        community_id: None,
+        community_name: None,
+        type_,
         page,
-        limit: fetchLimit,
+        limit: Some(fetchLimit),
         sort,
-        saved_only: false,
-      };
-      if (type_) {
-        getPostsForm.type_ = type_;
-      }
+        saved_only: Some(false),
+        auth: req.auth,
+      });
 
-      setOptionalAuth(getPostsForm, req.auth);
       promises.push(req.client.getPosts(getPostsForm));
+      promises.push(Promise.resolve());
     } else {
-      let getCommentsForm: GetComments = {
+      let getCommentsForm = new GetComments({
+        community_id: None,
+        community_name: None,
         page,
-        limit: fetchLimit,
+        limit: Some(fetchLimit),
         sort,
-        type_: type_ || ListingType.Local,
-        saved_only: false,
-      };
-      setOptionalAuth(getCommentsForm, req.auth);
+        type_,
+        saved_only: Some(false),
+        auth: req.auth,
+      });
+      promises.push(Promise.resolve());
       promises.push(req.client.getComments(getCommentsForm));
     }
 
-    let trendingCommunitiesForm: ListCommunities = {
-      type_: ListingType.Local,
-      sort: SortType.Hot,
-      limit: 6,
-    };
-    setOptionalAuth(trendingCommunitiesForm, req.auth);
+    let trendingCommunitiesForm = new ListCommunities({
+      type_: Some(ListingType.Local),
+      sort: Some(SortType.Hot),
+      limit: Some(trendingFetchLimit),
+      page: None,
+      auth: req.auth,
+    });
     promises.push(req.client.listCommunities(trendingCommunitiesForm));
 
     return promises;
@@ -262,13 +294,14 @@ export class Home extends Component<any, HomeState> {
   }
 
   get documentTitle(): string {
-    return `${
-      this.state.siteRes.site_view
-        ? this.state.siteRes.site_view.site.description
-          ? `${this.state.siteRes.site_view.site.name} - ${this.state.siteRes.site_view.site.description}`
-          : this.state.siteRes.site_view.site.name
-        : "Lemmy"
-    }`;
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        siteView.site.description.match({
+          some: desc => `${siteView.site.name} - ${desc}`,
+          none: siteView.site.name,
+        }),
+      none: "Lemmy",
+    });
   }
 
   render() {
@@ -277,8 +310,10 @@ export class Home extends Component<any, HomeState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
-        {this.state.siteRes.site_view?.site && (
+        {this.state.siteRes.site_view.isSome() && (
           <div class="row">
             <main role="main" class="col-12 col-md-8">
               <div class="d-block d-md-none">{this.mobileView()}</div>
@@ -291,28 +326,34 @@ export class Home extends Component<any, HomeState> {
     );
   }
 
+  get hasFollows(): boolean {
+    return UserService.Instance.myUserInfo.match({
+      some: mui => mui.follows.length > 0,
+      none: false,
+    });
+  }
+
   mobileView() {
     let siteRes = this.state.siteRes;
     return (
       <div class="row">
         <div class="col-12">
-          {UserService.Instance.myUserInfo &&
-            UserService.Instance.myUserInfo.follows.length > 0 && (
-              <button
-                class="btn btn-secondary d-inline-block mb-2 mr-3"
-                onClick={linkEvent(this, this.handleShowSubscribedMobile)}
-              >
-                {i18n.t("subscribed")}{" "}
-                <Icon
-                  icon={
-                    this.state.showSubscribedMobile
-                      ? `minus-square`
-                      : `plus-square`
-                  }
-                  classes="icon-inline"
-                />
-              </button>
-            )}
+          {this.hasFollows && (
+            <button
+              class="btn btn-secondary d-inline-block mb-2 mr-3"
+              onClick={linkEvent(this, this.handleShowSubscribedMobile)}
+            >
+              {i18n.t("subscribed")}{" "}
+              <Icon
+                icon={
+                  this.state.showSubscribedMobile
+                    ? `minus-square`
+                    : `plus-square`
+                }
+                classes="icon-inline"
+              />
+            </button>
+          )}
           <button
             class="btn btn-secondary d-inline-block mb-2 mr-3"
             onClick={linkEvent(this, this.handleShowTrendingMobile)}
@@ -337,15 +378,19 @@ export class Home extends Component<any, HomeState> {
               classes="icon-inline"
             />
           </button>
-          {this.state.showSidebarMobile && (
-            <SiteSidebar
-              site={siteRes.site_view.site}
-              admins={siteRes.admins}
-              counts={siteRes.site_view.counts}
-              online={siteRes.online}
-              showLocal={showLocal(this.isoData)}
-            />
-          )}
+          {this.state.showSidebarMobile &&
+            siteRes.site_view.match({
+              some: siteView => (
+                <SiteSidebar
+                  site={siteView.site}
+                  admins={Some(siteRes.admins)}
+                  counts={Some(siteView.counts)}
+                  online={Some(siteRes.online)}
+                  showLocal={showLocal(this.isoData)}
+                />
+              ),
+              none: <></>,
+            })}
           {this.state.showTrendingMobile && (
             <div class="col-12 card border-secondary mb-3">
               <div class="card-body">{this.trendingCommunities()}</div>
@@ -374,21 +419,23 @@ export class Home extends Component<any, HomeState> {
                 {this.exploreCommunitiesButton()}
               </div>
             </div>
-
-            <SiteSidebar
-              site={siteRes.site_view.site}
-              admins={siteRes.admins}
-              counts={siteRes.site_view.counts}
-              online={siteRes.online}
-              showLocal={showLocal(this.isoData)}
-            />
-
-            {UserService.Instance.myUserInfo &&
-              UserService.Instance.myUserInfo.follows.length > 0 && (
-                <div class="card border-secondary mb-3">
-                  <div class="card-body">{this.subscribedCommunities()}</div>
-                </div>
-              )}
+            {siteRes.site_view.match({
+              some: siteView => (
+                <SiteSidebar
+                  site={siteView.site}
+                  admins={Some(siteRes.admins)}
+                  counts={Some(siteView.counts)}
+                  online={Some(siteRes.online)}
+                  showLocal={showLocal(this.isoData)}
+                />
+              ),
+              none: <></>,
+            })}
+            {this.hasFollows && (
+              <div class="card border-secondary mb-3">
+                <div class="card-body">{this.subscribedCommunities()}</div>
+              </div>
+            )}
           </div>
         )}
       </div>
@@ -458,11 +505,14 @@ export class Home extends Component<any, HomeState> {
         </h5>
         {!this.state.subscribedCollapsed && (
           <ul class="list-inline mb-0">
-            {UserService.Instance.myUserInfo.follows.map(cfv => (
-              <li class="list-inline-item d-inline-block">
-                <CommunityLink community={cfv.community} />
-              </li>
-            ))}
+            {UserService.Instance.myUserInfo
+              .map(m => m.follows)
+              .unwrapOr([])
+              .map(cfv => (
+                <li class="list-inline-item d-inline-block">
+                  <CommunityLink community={cfv.community} />
+                </li>
+              ))}
           </ul>
         )}
       </div>
@@ -501,22 +551,24 @@ export class Home extends Component<any, HomeState> {
   }
 
   listings() {
-    let site = this.state.siteRes.site_view.site;
     return this.state.dataType == DataType.Post ? (
       <PostListings
         posts={this.state.posts}
         showCommunity
         removeDuplicates
-        enableDownvotes={site.enable_downvotes}
-        enableNsfw={site.enable_nsfw}
+        enableDownvotes={enableDownvotes(this.state.siteRes)}
+        enableNsfw={enableNsfw(this.state.siteRes)}
       />
     ) : (
       <CommentNodes
         nodes={commentsToFlatNodes(this.state.comments)}
+        moderators={None}
+        admins={None}
+        maxCommentsShown={None}
         noIndent
         showCommunity
         showContext
-        enableDownvotes={site.enable_downvotes}
+        enableDownvotes={enableDownvotes(this.state.siteRes)}
       />
     );
   }
@@ -524,9 +576,9 @@ export class Home extends Component<any, HomeState> {
   selects() {
     let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
     let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
-    let frontRss = UserService.Instance.myUserInfo
-      ? `/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`
-      : "";
+    let frontRss = auth(false)
+      .ok()
+      .map(auth => `/feeds/front/${auth}.xml?sort=${this.state.sort}`);
 
     return (
       <div className="mb-3">
@@ -563,19 +615,18 @@ export class Home extends Component<any, HomeState> {
             <link rel="alternate" type="application/atom+xml" href={localRss} />
           </>
         )}
-        {UserService.Instance.myUserInfo &&
-          this.state.listingType == ListingType.Subscribed && (
-            <>
-              <a href={frontRss} title="RSS" rel={relTags}>
-                <Icon icon="rss" classes="text-muted small" />
-              </a>
-              <link
-                rel="alternate"
-                type="application/atom+xml"
-                href={frontRss}
-              />
-            </>
-          )}
+        {this.state.listingType == ListingType.Subscribed &&
+          frontRss.match({
+            some: rss => (
+              <>
+                <a href={rss} title="RSS" rel={relTags}>
+                  <Icon icon="rss" classes="text-muted small" />
+                </a>
+                <link rel="alternate" type="application/atom+xml" href={rss} />
+              </>
+            ),
+            none: <></>,
+          })}
       </div>
     );
   }
@@ -622,27 +673,29 @@ export class Home extends Component<any, HomeState> {
 
   fetchData() {
     if (this.state.dataType == DataType.Post) {
-      let getPostsForm: GetPosts = {
-        page: this.state.page,
-        limit: fetchLimit,
-        sort: this.state.sort,
-        saved_only: false,
-        auth: authField(false),
-      };
-      if (this.state.listingType) {
-        getPostsForm.type_ = this.state.listingType;
-      }
+      let getPostsForm = new GetPosts({
+        community_id: None,
+        community_name: None,
+        page: Some(this.state.page),
+        limit: Some(fetchLimit),
+        sort: Some(this.state.sort),
+        saved_only: Some(false),
+        auth: auth(false).ok(),
+        type_: Some(this.state.listingType),
+      });
 
       WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
     } else {
-      let getCommentsForm: GetComments = {
-        page: this.state.page,
-        limit: fetchLimit,
-        sort: this.state.sort,
-        type_: this.state.listingType,
-        saved_only: false,
-        auth: authField(false),
-      };
+      let getCommentsForm = new GetComments({
+        community_id: None,
+        community_name: None,
+        page: Some(this.state.page),
+        limit: Some(fetchLimit),
+        sort: Some(this.state.sort),
+        saved_only: Some(false),
+        auth: auth(false).ok(),
+        type_: Some(this.state.listingType),
+      });
       WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
     }
   }
@@ -659,46 +712,55 @@ export class Home extends Component<any, HomeState> {
       );
       this.fetchData();
     } else if (op == UserOperation.ListCommunities) {
-      let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
+      let data = wsJsonToRes<ListCommunitiesResponse>(
+        msg,
+        ListCommunitiesResponse
+      );
       this.state.trendingCommunities = data.communities;
       this.setState(this.state);
     } else if (op == UserOperation.EditSite) {
-      let data = wsJsonToRes<SiteResponse>(msg).data;
-      this.state.siteRes.site_view = data.site_view;
+      let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
+      this.state.siteRes.site_view = Some(data.site_view);
       this.setState(this.state);
       toast(i18n.t("site_saved"));
     } else if (op == UserOperation.GetPosts) {
-      let data = wsJsonToRes<GetPostsResponse>(msg).data;
+      let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
       this.state.posts = data.posts;
       this.state.loading = false;
       this.setState(this.state);
+      WebSocketService.Instance.send(
+        wsClient.communityJoin({ community_id: 0 })
+      );
       restoreScrollPosition(this.context);
       setupTippy();
     } else if (op == UserOperation.CreatePost) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       // NSFW check
       let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
       let nsfwCheck =
         !nsfw ||
         (nsfw &&
-          UserService.Instance.myUserInfo &&
-          UserService.Instance.myUserInfo.local_user_view.local_user.show_nsfw);
+          UserService.Instance.myUserInfo
+            .map(m => m.local_user_view.local_user.show_nsfw)
+            .unwrapOr(false));
+
+      let showPostNotifs = UserService.Instance.myUserInfo
+        .map(m => m.local_user_view.local_user.show_new_post_notifs)
+        .unwrapOr(false);
 
       // Only push these if you're on the first page, and you pass the nsfw check
       if (this.state.page == 1 && nsfwCheck) {
         // If you're on subscribed, only push it if you're subscribed.
         if (this.state.listingType == ListingType.Subscribed) {
           if (
-            UserService.Instance.myUserInfo.follows
+            UserService.Instance.myUserInfo
+              .map(m => m.follows)
+              .unwrapOr([])
               .map(c => c.community.id)
               .includes(data.post_view.community.id)
           ) {
             this.state.posts.unshift(data.post_view);
-            if (
-              UserService.Instance.myUserInfo?.local_user_view.local_user
-                .show_new_post_notifs
-            ) {
+            if (showPostNotifs) {
               notifyPost(data.post_view, this.context.router);
             }
           }
@@ -706,19 +768,13 @@ export class Home extends Component<any, HomeState> {
           // If you're on the local view, only push it if its local
           if (data.post_view.post.local) {
             this.state.posts.unshift(data.post_view);
-            if (
-              UserService.Instance.myUserInfo?.local_user_view.local_user
-                .show_new_post_notifs
-            ) {
+            if (showPostNotifs) {
               notifyPost(data.post_view, this.context.router);
             }
           }
         } else {
           this.state.posts.unshift(data.post_view);
-          if (
-            UserService.Instance.myUserInfo?.local_user_view.local_user
-              .show_new_post_notifs
-          ) {
+          if (showPostNotifs) {
             notifyPost(data.post_view, this.context.router);
           }
         }
@@ -732,26 +788,26 @@ export class Home extends Component<any, HomeState> {
       op == UserOperation.StickyPost ||
       op == UserOperation.SavePost
     ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       editPostFindRes(data.post_view, this.state.posts);
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
       createPostLikeFindRes(data.post_view, this.state.posts);
       this.setState(this.state);
     } else if (op == UserOperation.AddAdmin) {
-      let data = wsJsonToRes<AddAdminResponse>(msg).data;
+      let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
       this.state.siteRes.admins = data.admins;
       this.setState(this.state);
     } else if (op == UserOperation.BanPerson) {
-      let data = wsJsonToRes<BanPersonResponse>(msg).data;
+      let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
       this.state.posts
         .filter(p => p.creator.id == data.person_view.person.id)
         .forEach(p => (p.creator.banned = data.banned));
 
       this.setState(this.state);
     } else if (op == UserOperation.GetComments) {
-      let data = wsJsonToRes<GetCommentsResponse>(msg).data;
+      let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
       this.state.comments = data.comments;
       this.state.loading = false;
       this.setState(this.state);
@@ -760,18 +816,20 @@ export class Home extends Component<any, HomeState> {
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       editCommentRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
 
       // Necessary since it might be a user reply
       if (data.form_id) {
         // If you're on subscribed, only push it if you're subscribed.
         if (this.state.listingType == ListingType.Subscribed) {
           if (
-            UserService.Instance.myUserInfo.follows
+            UserService.Instance.myUserInfo
+              .map(m => m.follows)
+              .unwrapOr([])
               .map(c => c.community.id)
               .includes(data.comment_view.community.id)
           ) {
@@ -783,23 +841,23 @@ export class Home extends Component<any, HomeState> {
         this.setState(this.state);
       }
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       saveCommentRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       createCommentLikeRes(data.comment_view, this.state.comments);
       this.setState(this.state);
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
     } else if (op == UserOperation.CreatePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
+      let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
     } else if (op == UserOperation.CreateCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
+      let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
index c713383132ac9e7583d6a7eccaeaff1acab0c655..fe4064dec6a46cb05f9c96c33713e488e91aa6a6 100644 (file)
@@ -1,3 +1,4 @@
+import { None } from "@sniptt/monads";
 import { Component } from "inferno";
 import { GetSiteResponse } from "lemmy-js-client";
 import { i18n } from "../../i18next";
@@ -20,42 +21,56 @@ export class Instances extends Component<any, InstancesState> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("instances")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
-    let federated_instances = this.state.siteRes?.federated_instances;
-    return (
-      federated_instances && (
+    return this.state.siteRes.federated_instances.match({
+      some: federated_instances => (
         <div class="container">
           <HtmlTags
             title={this.documentTitle}
             path={this.context.router.route.match.url}
+            description={None}
+            image={None}
           />
           <div class="row">
             <div class="col-md-6">
               <h5>{i18n.t("linked_instances")}</h5>
               {this.itemList(federated_instances.linked)}
             </div>
-            {federated_instances.allowed?.length > 0 && (
-              <div class="col-md-6">
-                <h5>{i18n.t("allowed_instances")}</h5>
-                {this.itemList(federated_instances.allowed)}
-              </div>
-            )}
-            {federated_instances.blocked?.length > 0 && (
-              <div class="col-md-6">
-                <h5>{i18n.t("blocked_instances")}</h5>
-                {this.itemList(federated_instances.blocked)}
-              </div>
-            )}
+            {federated_instances.allowed.match({
+              some: allowed =>
+                allowed.length > 0 && (
+                  <div class="col-md-6">
+                    <h5>{i18n.t("allowed_instances")}</h5>
+                    {this.itemList(allowed)}
+                  </div>
+                ),
+              none: <></>,
+            })}
+            {federated_instances.blocked.match({
+              some: blocked =>
+                blocked.length > 0 && (
+                  <div class="col-md-6">
+                    <h5>{i18n.t("blocked_instances")}</h5>
+                    {this.itemList(blocked)}
+                  </div>
+                ),
+              none: <></>,
+            })}
           </div>
         </div>
-      )
-    );
+      ),
+      none: <></>,
+    });
   }
 
   itemList(items: string[]) {
+    let noneFound = <div>{i18n.t("none_found")}</div>;
     return items.length > 0 ? (
       <ul>
         {items.map(i => (
@@ -67,7 +82,7 @@ export class Instances extends Component<any, InstancesState> {
         ))}
       </ul>
     ) : (
-      <div>{i18n.t("none_found")}</div>
+      noneFound
     );
   }
 }
index b2904c846ad6c273cfc59e35606dbe3757917637..590d7d84619104077574666597f4c9802320f55c 100644 (file)
@@ -1,3 +1,4 @@
+import { None } from "@sniptt/monads";
 import { Component } from "inferno";
 import { GetSiteResponse } from "lemmy-js-client";
 import { i18n } from "../../i18next";
@@ -29,13 +30,22 @@ export class Legal extends Component<any, LegalState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
-        <div
-          className="md-div"
-          dangerouslySetInnerHTML={mdToHtml(
-            this.state.siteRes.site_view.site.legal_information
-          )}
-        />
+        {this.state.siteRes.site_view.match({
+          some: siteView =>
+            siteView.site.legal_information.match({
+              some: legal => (
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(legal)}
+                />
+              ),
+              none: <></>,
+            }),
+          none: <></>,
+        })}
       </div>
     );
   }
index 5a61d1363a362ac8a5d4e8d2e427fd325577b789..7383a560767bd9186777640259dab772dd0be246 100644 (file)
@@ -1,25 +1,25 @@
+import { None } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   GetSiteResponse,
   Login as LoginForm,
   LoginResponse,
   PasswordReset,
-  SiteView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   isBrowser,
   setIsoData,
   toast,
   validEmail,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -27,7 +27,7 @@ import { Spinner } from "../common/icon";
 interface State {
   loginForm: LoginForm;
   loginLoading: boolean;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
 }
 
 export class Login extends Component<any, State> {
@@ -35,12 +35,12 @@ export class Login extends Component<any, State> {
   private subscription: Subscription;
 
   emptyState: State = {
-    loginForm: {
+    loginForm: new LoginForm({
       username_or_email: undefined,
       password: undefined,
-    },
+    }),
     loginLoading: false,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
   };
 
   constructor(props: any, context: any) {
@@ -58,7 +58,7 @@ export class Login extends Component<any, State> {
 
   componentDidMount() {
     // Navigate to home if already logged in
-    if (UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isSome()) {
       this.context.router.history.push("/");
     }
   }
@@ -70,7 +70,10 @@ export class Login extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("login")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   get isLemmyMl(): boolean {
@@ -83,6 +86,8 @@ export class Login extends Component<any, State> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
@@ -173,9 +178,9 @@ export class Login extends Component<any, State> {
 
   handlePasswordReset(i: Login, event: any) {
     event.preventDefault();
-    let resetForm: PasswordReset = {
+    let resetForm = new PasswordReset({
       email: i.state.loginForm.username_or_email,
-    };
+    });
     WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
   }
 
@@ -189,13 +194,13 @@ export class Login extends Component<any, State> {
       return;
     } else {
       if (op == UserOperation.Login) {
-        let data = wsJsonToRes<LoginResponse>(msg).data;
+        let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
         this.state = this.emptyState;
         this.setState(this.state);
         UserService.Instance.login(data);
         WebSocketService.Instance.send(
           wsClient.userJoin({
-            auth: authField(),
+            auth: auth().unwrap(),
           })
         );
         toast(i18n.t("logged_in"));
@@ -203,8 +208,8 @@ export class Login extends Component<any, State> {
       } else if (op == UserOperation.PasswordReset) {
         toast(i18n.t("reset_password_mail_sent"));
       } else if (op == UserOperation.GetSite) {
-        let data = wsJsonToRes<GetSiteResponse>(msg).data;
-        this.state.site_view = data.site_view;
+        let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
+        this.state.siteRes = data;
         this.setState(this.state);
       }
     }
index 30c6e357a5e24560eb7f91f4dceb2870cb6f4e86..f524f4ceb9d865eb46ebf026d57525a9cf78d1fb 100644 (file)
@@ -1,11 +1,19 @@
+import { None, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Helmet } from "inferno-helmet";
-import { LoginResponse, Register, UserOperation } from "lemmy-js-client";
+import {
+  LoginResponse,
+  Register,
+  toUndefined,
+  UserOperation,
+  wsJsonToRes,
+  wsUserOp,
+} from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { delay, retryWhen, take } from "rxjs/operators";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
-import { toast, wsClient, wsJsonToRes, wsUserOp } from "../../utils";
+import { toast, wsClient } from "../../utils";
 import { Spinner } from "../common/icon";
 import { SiteForm } from "./site-form";
 
@@ -19,15 +27,18 @@ export class Setup extends Component<any, State> {
   private subscription: Subscription;
 
   private emptyState: State = {
-    userForm: {
+    userForm: new Register({
       username: undefined,
       password: undefined,
       password_verify: undefined,
       show_nsfw: true,
       // The first admin signup doesn't need a captcha
-      captcha_uuid: "",
-      captcha_answer: "",
-    },
+      captcha_uuid: None,
+      captcha_answer: None,
+      email: None,
+      honeypot: None,
+      answer: None,
+    }),
     doneRegisteringUser: false,
     userLoading: false,
   };
@@ -64,7 +75,7 @@ export class Setup extends Component<any, State> {
             {!this.state.doneRegisteringUser ? (
               this.registerUser()
             ) : (
-              <SiteForm showLocal />
+              <SiteForm site={None} showLocal />
             )}
           </div>
         </div>
@@ -104,7 +115,7 @@ export class Setup extends Component<any, State> {
               id="email"
               class="form-control"
               placeholder={i18n.t("optional")}
-              value={this.state.userForm.email}
+              value={toUndefined(this.state.userForm.email)}
               onInput={linkEvent(this, this.handleRegisterEmailChange)}
               minLength={3}
             />
@@ -171,7 +182,7 @@ export class Setup extends Component<any, State> {
   }
 
   handleRegisterEmailChange(i: Setup, event: any) {
-    i.state.userForm.email = event.target.value;
+    i.state.userForm.email = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -193,7 +204,7 @@ export class Setup extends Component<any, State> {
       this.setState(this.state);
       return;
     } else if (op == UserOperation.Register) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
+      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
       this.state.userLoading = false;
       this.state.doneRegisteringUser = true;
       UserService.Instance.login(data);
index 14b6aa90f9fb2d558a7ee0cfb7b9de3319bce9e7..31d1e91eb6238da885a2c8502ed96a763a1a65cb 100644 (file)
@@ -1,20 +1,25 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Options, passwordStrength } from "check-password-strength";
 import { I18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import {
+  CaptchaResponse,
   GetCaptchaResponse,
   GetSiteResponse,
   LoginResponse,
   Register,
   SiteView,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   isBrowser,
   joinLemmyUrl,
   mdToHtml,
@@ -22,9 +27,7 @@ import {
   toast,
   validEmail,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
@@ -60,9 +63,9 @@ const passwordStrengthOptions: Options<string> = [
 interface State {
   registerForm: Register;
   registerLoading: boolean;
-  captcha: GetCaptchaResponse;
+  captcha: Option<GetCaptchaResponse>;
   captchaPlaying: boolean;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
 }
 
 export class Signup extends Component<any, State> {
@@ -71,20 +74,21 @@ export class Signup extends Component<any, State> {
   private audio: HTMLAudioElement;
 
   emptyState: State = {
-    registerForm: {
+    registerForm: new Register({
       username: undefined,
       password: undefined,
       password_verify: undefined,
       show_nsfw: false,
-      captcha_uuid: undefined,
-      captcha_answer: undefined,
-      honeypot: undefined,
-      answer: undefined,
-    },
+      captcha_uuid: None,
+      captcha_answer: None,
+      honeypot: None,
+      answer: None,
+      email: None,
+    }),
     registerLoading: false,
-    captcha: undefined,
+    captcha: None,
     captchaPlaying: false,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
   };
 
   constructor(props: any, context: any) {
@@ -108,13 +112,14 @@ export class Signup extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${this.titleName} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${this.titleName(siteView)} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
-  get titleName(): string {
-    return `${i18n.t(
-      this.state.site_view.site.private_instance ? "apply_to_join" : "sign_up"
-    )}`;
+  titleName(siteView: SiteView): string {
+    return i18n.t(siteView.site.private_instance ? "apply_to_join" : "sign_up");
   }
 
   get isLemmyMl(): boolean {
@@ -127,6 +132,8 @@ export class Signup extends Component<any, State> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3">{this.registerForm()}</div>
@@ -136,244 +143,272 @@ export class Signup extends Component<any, State> {
   }
 
   registerForm() {
-    return (
-      <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h5>{this.titleName}</h5>
+    return this.state.siteRes.site_view.match({
+      some: siteView => (
+        <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
+          <h5>{this.titleName(siteView)}</h5>
 
-        {this.isLemmyMl && (
-          <div class="form-group row">
-            <div class="mt-2 mb-0 alert alert-warning" role="alert">
-              <T i18nKey="lemmy_ml_registration_message">
-                #<a href={joinLemmyUrl}>#</a>
-              </T>
+          {this.isLemmyMl && (
+            <div class="form-group row">
+              <div class="mt-2 mb-0 alert alert-warning" role="alert">
+                <T i18nKey="lemmy_ml_registration_message">
+                  #<a href={joinLemmyUrl}>#</a>
+                </T>
+              </div>
             </div>
-          </div>
-        )}
-
-        <div class="form-group row">
-          <label class="col-sm-2 col-form-label" htmlFor="register-username">
-            {i18n.t("username")}
-          </label>
-
-          <div class="col-sm-10">
-            <input
-              type="text"
-              id="register-username"
-              class="form-control"
-              value={this.state.registerForm.username}
-              onInput={linkEvent(this, this.handleRegisterUsernameChange)}
-              required
-              minLength={3}
-              pattern="[a-zA-Z0-9_]+"
-              title={i18n.t("community_reqs")}
-            />
-          </div>
-        </div>
+          )}
 
-        <div class="form-group row">
-          <label class="col-sm-2 col-form-label" htmlFor="register-email">
-            {i18n.t("email")}
-          </label>
-          <div class="col-sm-10">
-            <input
-              type="email"
-              id="register-email"
-              class="form-control"
-              placeholder={
-                this.state.site_view.site.require_email_verification
-                  ? i18n.t("required")
-                  : i18n.t("optional")
-              }
-              value={this.state.registerForm.email}
-              autoComplete="email"
-              onInput={linkEvent(this, this.handleRegisterEmailChange)}
-              required={this.state.site_view.site.require_email_verification}
-              minLength={3}
-            />
-            {!this.state.site_view.site.require_email_verification &&
-              !validEmail(this.state.registerForm.email) && (
-                <div class="mt-2 mb-0 alert alert-warning" role="alert">
-                  <Icon icon="alert-triangle" classes="icon-inline mr-2" />
-                  {i18n.t("no_password_reset")}
-                </div>
-              )}
-          </div>
-        </div>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label" htmlFor="register-username">
+              {i18n.t("username")}
+            </label>
 
-        <div class="form-group row">
-          <label class="col-sm-2 col-form-label" htmlFor="register-password">
-            {i18n.t("password")}
-          </label>
-          <div class="col-sm-10">
-            <input
-              type="password"
-              id="register-password"
-              value={this.state.registerForm.password}
-              autoComplete="new-password"
-              onInput={linkEvent(this, this.handleRegisterPasswordChange)}
-              minLength={10}
-              maxLength={60}
-              class="form-control"
-              required
-            />
-            {this.state.registerForm.password && (
-              <div class={this.passwordColorClass}>
-                {i18n.t(this.passwordStrength as I18nKeys)}
-              </div>
-            )}
+            <div class="col-sm-10">
+              <input
+                type="text"
+                id="register-username"
+                class="form-control"
+                value={this.state.registerForm.username}
+                onInput={linkEvent(this, this.handleRegisterUsernameChange)}
+                required
+                minLength={3}
+                pattern="[a-zA-Z0-9_]+"
+                title={i18n.t("community_reqs")}
+              />
+            </div>
           </div>
-        </div>
 
-        <div class="form-group row">
-          <label
-            class="col-sm-2 col-form-label"
-            htmlFor="register-verify-password"
-          >
-            {i18n.t("verify_password")}
-          </label>
-          <div class="col-sm-10">
-            <input
-              type="password"
-              id="register-verify-password"
-              value={this.state.registerForm.password_verify}
-              autoComplete="new-password"
-              onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
-              maxLength={60}
-              class="form-control"
-              required
-            />
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label" htmlFor="register-email">
+              {i18n.t("email")}
+            </label>
+            <div class="col-sm-10">
+              <input
+                type="email"
+                id="register-email"
+                class="form-control"
+                placeholder={
+                  siteView.site.require_email_verification
+                    ? i18n.t("required")
+                    : i18n.t("optional")
+                }
+                value={toUndefined(this.state.registerForm.email)}
+                autoComplete="email"
+                onInput={linkEvent(this, this.handleRegisterEmailChange)}
+                required={siteView.site.require_email_verification}
+                minLength={3}
+              />
+              {!siteView.site.require_email_verification &&
+                !this.state.registerForm.email
+                  .map(validEmail)
+                  .unwrapOr(true) && (
+                  <div class="mt-2 mb-0 alert alert-warning" role="alert">
+                    <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+                    {i18n.t("no_password_reset")}
+                  </div>
+                )}
+            </div>
           </div>
-        </div>
 
-        {this.state.site_view.site.require_application && (
-          <>
-            <div class="form-group row">
-              <div class="offset-sm-2 col-sm-10">
-                <div class="mt-2 alert alert-warning" role="alert">
-                  <Icon icon="alert-triangle" classes="icon-inline mr-2" />
-                  {i18n.t("fill_out_application")}
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label" htmlFor="register-password">
+              {i18n.t("password")}
+            </label>
+            <div class="col-sm-10">
+              <input
+                type="password"
+                id="register-password"
+                value={this.state.registerForm.password}
+                autoComplete="new-password"
+                onInput={linkEvent(this, this.handleRegisterPasswordChange)}
+                minLength={10}
+                maxLength={60}
+                class="form-control"
+                required
+              />
+              {this.state.registerForm.password && (
+                <div class={this.passwordColorClass}>
+                  {i18n.t(this.passwordStrength as I18nKeys)}
                 </div>
-                <div
-                  className="md-div"
-                  dangerouslySetInnerHTML={mdToHtml(
-                    this.state.site_view.site.application_question || ""
-                  )}
-                />
-              </div>
-            </div>
-
-            <div class="form-group row">
-              <label
-                class="col-sm-2 col-form-label"
-                htmlFor="application_answer"
-              >
-                {i18n.t("answer")}
-              </label>
-              <div class="col-sm-10">
-                <MarkdownTextArea
-                  onContentChange={this.handleAnswerChange}
-                  hideNavigationWarnings
-                />
-              </div>
+              )}
             </div>
-          </>
-        )}
+          </div>
 
-        {this.state.captcha && (
           <div class="form-group row">
-            <label class="col-sm-2" htmlFor="register-captcha">
-              <span class="mr-2">{i18n.t("enter_code")}</span>
-              <button
-                type="button"
-                class="btn btn-secondary"
-                onClick={linkEvent(this, this.handleRegenCaptcha)}
-                aria-label={i18n.t("captcha")}
-              >
-                <Icon icon="refresh-cw" classes="icon-refresh-cw" />
-              </button>
+            <label
+              class="col-sm-2 col-form-label"
+              htmlFor="register-verify-password"
+            >
+              {i18n.t("verify_password")}
             </label>
-            {this.showCaptcha()}
-            <div class="col-sm-6">
+            <div class="col-sm-10">
               <input
-                type="text"
-                class="form-control"
-                id="register-captcha"
-                value={this.state.registerForm.captcha_answer}
+                type="password"
+                id="register-verify-password"
+                value={this.state.registerForm.password_verify}
+                autoComplete="new-password"
                 onInput={linkEvent(
                   this,
-                  this.handleRegisterCaptchaAnswerChange
+                  this.handleRegisterPasswordVerifyChange
                 )}
+                maxLength={60}
+                class="form-control"
                 required
               />
             </div>
           </div>
-        )}
-        {this.state.site_view.site.enable_nsfw && (
-          <div class="form-group row">
-            <div class="col-sm-10">
-              <div class="form-check">
+
+          {siteView.site.require_application && (
+            <>
+              <div class="form-group row">
+                <div class="offset-sm-2 col-sm-10">
+                  <div class="mt-2 alert alert-warning" role="alert">
+                    <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+                    {i18n.t("fill_out_application")}
+                  </div>
+                  {siteView.site.application_question.match({
+                    some: question => (
+                      <div
+                        className="md-div"
+                        dangerouslySetInnerHTML={mdToHtml(question)}
+                      />
+                    ),
+                    none: <></>,
+                  })}
+                </div>
+              </div>
+
+              <div class="form-group row">
+                <label
+                  class="col-sm-2 col-form-label"
+                  htmlFor="application_answer"
+                >
+                  {i18n.t("answer")}
+                </label>
+                <div class="col-sm-10">
+                  <MarkdownTextArea
+                    initialContent={None}
+                    placeholder={None}
+                    buttonTitle={None}
+                    maxLength={None}
+                    onContentChange={this.handleAnswerChange}
+                    hideNavigationWarnings
+                  />
+                </div>
+              </div>
+            </>
+          )}
+
+          {this.state.captcha.isSome() && (
+            <div class="form-group row">
+              <label class="col-sm-2" htmlFor="register-captcha">
+                <span class="mr-2">{i18n.t("enter_code")}</span>
+                <button
+                  type="button"
+                  class="btn btn-secondary"
+                  onClick={linkEvent(this, this.handleRegenCaptcha)}
+                  aria-label={i18n.t("captcha")}
+                >
+                  <Icon icon="refresh-cw" classes="icon-refresh-cw" />
+                </button>
+              </label>
+              {this.showCaptcha()}
+              <div class="col-sm-6">
                 <input
-                  class="form-check-input"
-                  id="register-show-nsfw"
-                  type="checkbox"
-                  checked={this.state.registerForm.show_nsfw}
-                  onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
+                  type="text"
+                  class="form-control"
+                  id="register-captcha"
+                  value={toUndefined(this.state.registerForm.captcha_answer)}
+                  onInput={linkEvent(
+                    this,
+                    this.handleRegisterCaptchaAnswerChange
+                  )}
+                  required
                 />
-                <label class="form-check-label" htmlFor="register-show-nsfw">
-                  {i18n.t("show_nsfw")}
-                </label>
               </div>
             </div>
+          )}
+          {siteView.site.enable_nsfw && (
+            <div class="form-group row">
+              <div class="col-sm-10">
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="register-show-nsfw"
+                    type="checkbox"
+                    checked={this.state.registerForm.show_nsfw}
+                    onChange={linkEvent(
+                      this,
+                      this.handleRegisterShowNsfwChange
+                    )}
+                  />
+                  <label class="form-check-label" htmlFor="register-show-nsfw">
+                    {i18n.t("show_nsfw")}
+                  </label>
+                </div>
+              </div>
+            </div>
+          )}
+          <input
+            tabIndex={-1}
+            autoComplete="false"
+            name="a_password"
+            type="text"
+            class="form-control honeypot"
+            id="register-honey"
+            value={toUndefined(this.state.registerForm.honeypot)}
+            onInput={linkEvent(this, this.handleHoneyPotChange)}
+          />
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <button type="submit" class="btn btn-secondary">
+                {this.state.registerLoading ? (
+                  <Spinner />
+                ) : (
+                  this.titleName(siteView)
+                )}
+              </button>
+            </div>
           </div>
-        )}
-        <input
-          tabIndex={-1}
-          autoComplete="false"
-          name="a_password"
-          type="text"
-          class="form-control honeypot"
-          id="register-honey"
-          value={this.state.registerForm.honeypot}
-          onInput={linkEvent(this, this.handleHoneyPotChange)}
-        />
-        <div class="form-group row">
-          <div class="col-sm-10">
-            <button type="submit" class="btn btn-secondary">
-              {this.state.registerLoading ? <Spinner /> : this.titleName}
-            </button>
-          </div>
-        </div>
-      </form>
-    );
+        </form>
+      ),
+      none: <></>,
+    });
   }
 
   showCaptcha() {
-    return (
-      <div class="col-sm-4">
-        {this.state.captcha.ok && (
-          <>
-            <img
-              class="rounded-top img-fluid"
-              src={this.captchaPngSrc()}
-              style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
-              alt={i18n.t("captcha")}
-            />
-            {this.state.captcha.ok.wav && (
-              <button
-                class="rounded-bottom btn btn-sm btn-secondary btn-block"
-                style="border-top-right-radius: 0; border-top-left-radius: 0;"
-                title={i18n.t("play_captcha_audio")}
-                onClick={linkEvent(this, this.handleCaptchaPlay)}
-                type="button"
-                disabled={this.state.captchaPlaying}
-              >
-                <Icon icon="play" classes="icon-play" />
-              </button>
-            )}
-          </>
-        )}
-      </div>
-    );
+    return this.state.captcha.match({
+      some: captcha => (
+        <div class="col-sm-4">
+          {captcha.ok.match({
+            some: res => (
+              <>
+                <img
+                  class="rounded-top img-fluid"
+                  src={this.captchaPngSrc(res)}
+                  style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+                  alt={i18n.t("captcha")}
+                />
+                {res.wav.isSome() && (
+                  <button
+                    class="rounded-bottom btn btn-sm btn-secondary btn-block"
+                    style="border-top-right-radius: 0; border-top-left-radius: 0;"
+                    title={i18n.t("play_captcha_audio")}
+                    onClick={linkEvent(this, this.handleCaptchaPlay)}
+                    type="button"
+                    disabled={this.state.captchaPlaying}
+                  >
+                    <Icon icon="play" classes="icon-play" />
+                  </button>
+                )}
+              </>
+            ),
+            none: <></>,
+          })}
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   get passwordStrength() {
@@ -408,9 +443,9 @@ export class Signup extends Component<any, State> {
   }
 
   handleRegisterEmailChange(i: Signup, event: any) {
-    i.state.registerForm.email = event.target.value;
-    if (i.state.registerForm.email == "") {
-      i.state.registerForm.email = undefined;
+    i.state.registerForm.email = Some(event.target.value);
+    if (i.state.registerForm.email.unwrap() == "") {
+      i.state.registerForm.email = None;
     }
     i.setState(i.state);
   }
@@ -431,17 +466,17 @@ export class Signup extends Component<any, State> {
   }
 
   handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
-    i.state.registerForm.captcha_answer = event.target.value;
+    i.state.registerForm.captcha_answer = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleAnswerChange(val: string) {
-    this.state.registerForm.answer = val;
+    this.state.registerForm.answer = Some(val);
     this.setState(this.state);
   }
 
   handleHoneyPotChange(i: Signup, event: any) {
-    i.state.registerForm.honeypot = event.target.value;
+    i.state.registerForm.honeypot = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -455,25 +490,34 @@ export class Signup extends Component<any, State> {
   handleCaptchaPlay(i: Signup) {
     // 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.
-    if (i.audio == null) {
-      let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
-      i.audio = new Audio(base64);
-    }
-
-    i.audio.play();
-
-    i.state.captchaPlaying = true;
-    i.setState(i.state);
-
-    i.audio.addEventListener("ended", () => {
-      i.audio.currentTime = 0;
-      i.state.captchaPlaying = false;
-      i.setState(i.state);
+    i.state.captcha.match({
+      some: captcha =>
+        captcha.ok.match({
+          some: res => {
+            if (i.audio == null) {
+              let base64 = `data:audio/wav;base64,${res.wav}`;
+              i.audio = new Audio(base64);
+            }
+
+            i.audio.play();
+
+            i.state.captchaPlaying = true;
+            i.setState(i.state);
+
+            i.audio.addEventListener("ended", () => {
+              i.audio.currentTime = 0;
+              i.state.captchaPlaying = false;
+              i.setState(i.state);
+            });
+          },
+          none: void 0,
+        }),
+      none: void 0,
     });
   }
 
-  captchaPngSrc() {
-    return `data:image/png;base64,${this.state.captcha.ok.png}`;
+  captchaPngSrc(captcha: CaptchaResponse) {
+    return `data:image/png;base64,${captcha.png}`;
   }
 
   parseMessage(msg: any) {
@@ -489,7 +533,7 @@ export class Signup extends Component<any, State> {
       return;
     } else {
       if (op == UserOperation.Register) {
-        let data = wsJsonToRes<LoginResponse>(msg).data;
+        let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
         this.state = this.emptyState;
         this.setState(this.state);
         // Only log them in if a jwt was set
@@ -497,7 +541,7 @@ export class Signup extends Component<any, State> {
           UserService.Instance.login(data);
           WebSocketService.Instance.send(
             wsClient.userJoin({
-              auth: authField(),
+              auth: auth().unwrap(),
             })
           );
           this.props.history.push("/communities");
@@ -511,17 +555,20 @@ export class Signup extends Component<any, State> {
           this.props.history.push("/");
         }
       } else if (op == UserOperation.GetCaptcha) {
-        let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
-        if (data.ok) {
-          this.state.captcha = data;
-          this.state.registerForm.captcha_uuid = data.ok.uuid;
-          this.setState(this.state);
-        }
+        let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse);
+        data.ok.match({
+          some: res => {
+            this.state.captcha = Some(data);
+            this.state.registerForm.captcha_uuid = Some(res.uuid);
+            this.setState(this.state);
+          },
+          none: void 0,
+        });
       } else if (op == UserOperation.PasswordReset) {
         toast(i18n.t("reset_password_mail_sent"));
       } else if (op == UserOperation.GetSite) {
-        let data = wsJsonToRes<GetSiteResponse>(msg).data;
-        this.state.site_view = data.site_view;
+        let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
+        this.state.siteRes = data;
         this.setState(this.state);
       }
     }
index 7016e6f7485cfbe00d2dc67e368d1b870c0a4c13..c6533710229df5f2c7c85033942aa6aa6b1e6149 100644 (file)
@@ -1,10 +1,17 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Prompt } from "inferno-router";
-import { CreateSite, EditSite, ListingType, Site } from "lemmy-js-client";
+import {
+  CreateSite,
+  EditSite,
+  ListingType,
+  Site,
+  toUndefined,
+} from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   fetchThemeList,
   wsClient,
@@ -15,38 +22,41 @@ import { ListingTypeSelect } from "../common/listing-type-select";
 import { MarkdownTextArea } from "../common/markdown-textarea";
 
 interface SiteFormProps {
-  site?: Site; // If a site is given, that means this is an edit
-  showLocal: boolean;
-  onCancel?(): any;
-  onEdit?(): any;
+  site: Option<Site>; // If a site is given, that means this is an edit
+  showLocal?: boolean;
+  onCancel?(): void;
+  onEdit?(): void;
 }
 
 interface SiteFormState {
   siteForm: EditSite;
   loading: boolean;
-  themeList: string[];
+  themeList: Option<string[]>;
 }
 
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   private emptyState: SiteFormState = {
-    siteForm: {
-      enable_downvotes: true,
-      open_registration: true,
-      enable_nsfw: true,
-      name: null,
-      icon: null,
-      banner: null,
-      require_email_verification: null,
-      require_application: null,
-      application_question: null,
-      private_instance: null,
-      default_theme: null,
-      default_post_listing_type: null,
-      legal_information: null,
-      auth: authField(false),
-    },
+    siteForm: new EditSite({
+      enable_downvotes: Some(true),
+      open_registration: Some(true),
+      enable_nsfw: Some(true),
+      name: None,
+      icon: None,
+      banner: None,
+      require_email_verification: None,
+      require_application: None,
+      application_question: None,
+      private_instance: None,
+      default_theme: None,
+      sidebar: None,
+      default_post_listing_type: None,
+      legal_information: None,
+      description: None,
+      community_creation_admin_only: None,
+      auth: undefined,
+    }),
     loading: false,
-    themeList: [],
+    themeList: None,
   };
 
   constructor(props: any, context: any) {
@@ -67,32 +77,36 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     this.handleDefaultPostListingTypeChange =
       this.handleDefaultPostListingTypeChange.bind(this);
 
-    if (this.props.site) {
-      let site = this.props.site;
-      this.state.siteForm = {
-        name: site.name,
-        sidebar: site.sidebar,
-        description: site.description,
-        enable_downvotes: site.enable_downvotes,
-        open_registration: site.open_registration,
-        enable_nsfw: site.enable_nsfw,
-        community_creation_admin_only: site.community_creation_admin_only,
-        icon: site.icon,
-        banner: site.banner,
-        require_email_verification: site.require_email_verification,
-        require_application: site.require_application,
-        application_question: site.application_question,
-        private_instance: site.private_instance,
-        default_theme: site.default_theme,
-        default_post_listing_type: site.default_post_listing_type,
-        legal_information: site.legal_information,
-        auth: authField(false),
-      };
-    }
+    this.props.site.match({
+      some: site => {
+        this.state.siteForm = new EditSite({
+          name: Some(site.name),
+          sidebar: site.sidebar,
+          description: site.description,
+          enable_downvotes: Some(site.enable_downvotes),
+          open_registration: Some(site.open_registration),
+          enable_nsfw: Some(site.enable_nsfw),
+          community_creation_admin_only: Some(
+            site.community_creation_admin_only
+          ),
+          icon: site.icon,
+          banner: site.banner,
+          require_email_verification: Some(site.require_email_verification),
+          require_application: Some(site.require_application),
+          application_question: site.application_question,
+          private_instance: Some(site.private_instance),
+          default_theme: Some(site.default_theme),
+          default_post_listing_type: Some(site.default_post_listing_type),
+          legal_information: site.legal_information,
+          auth: auth(false).unwrap(),
+        });
+      },
+      none: void 0,
+    });
   }
 
   async componentDidMount() {
-    this.state.themeList = await fetchThemeList();
+    this.state.themeList = Some(await fetchThemeList());
     this.setState(this.state);
   }
 
@@ -105,7 +119,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   componentDidUpdate() {
     if (
       !this.state.loading &&
-      !this.props.site &&
+      this.props.site.isNone() &&
       (this.state.siteForm.name ||
         this.state.siteForm.sidebar ||
         this.state.siteForm.application_question ||
@@ -127,7 +141,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
         <Prompt
           when={
             !this.state.loading &&
-            !this.props.site &&
+            this.props.site.isNone() &&
             (this.state.siteForm.name ||
               this.state.siteForm.sidebar ||
               this.state.siteForm.application_question ||
@@ -137,7 +151,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
         />
         <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
           <h5>{`${
-            this.props.site
+            this.props.site.isSome()
               ? capitalizeFirstLetter(i18n.t("save"))
               : capitalizeFirstLetter(i18n.t("name"))
           } ${i18n.t("your_site")}`}</h5>
@@ -150,7 +164,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                 type="text"
                 id="create-site-name"
                 class="form-control"
-                value={this.state.siteForm.name}
+                value={toUndefined(this.state.siteForm.name)}
                 onInput={linkEvent(this, this.handleSiteNameChange)}
                 required
                 minLength={3}
@@ -186,7 +200,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                 type="text"
                 class="form-control"
                 id="site-desc"
-                value={this.state.siteForm.description}
+                value={toUndefined(this.state.siteForm.description)}
                 onInput={linkEvent(this, this.handleSiteDescChange)}
                 maxLength={150}
               />
@@ -197,6 +211,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             <div class="col-12">
               <MarkdownTextArea
                 initialContent={this.state.siteForm.sidebar}
+                placeholder={None}
+                buttonTitle={None}
+                maxLength={None}
                 onContentChange={this.handleSiteSidebarChange}
                 hideNavigationWarnings
               />
@@ -209,12 +226,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             <div class="col-12">
               <MarkdownTextArea
                 initialContent={this.state.siteForm.legal_information}
+                placeholder={None}
+                buttonTitle={None}
+                maxLength={None}
                 onContentChange={this.handleSiteLegalInfoChange}
                 hideNavigationWarnings
               />
             </div>
           </div>
-          {this.state.siteForm.require_application && (
+          {this.state.siteForm.require_application.unwrapOr(false) && (
             <div class="form-group row">
               <label class="col-12 col-form-label">
                 {i18n.t("application_questionnaire")}
@@ -222,6 +242,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               <div class="col-12">
                 <MarkdownTextArea
                   initialContent={this.state.siteForm.application_question}
+                  placeholder={None}
+                  buttonTitle={None}
+                  maxLength={None}
                   onContentChange={this.handleSiteApplicationQuestionChange}
                   hideNavigationWarnings
                 />
@@ -235,7 +258,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-downvotes"
                   type="checkbox"
-                  checked={this.state.siteForm.enable_downvotes}
+                  checked={toUndefined(this.state.siteForm.enable_downvotes)}
                   onChange={linkEvent(
                     this,
                     this.handleSiteEnableDownvotesChange
@@ -254,7 +277,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-enable-nsfw"
                   type="checkbox"
-                  checked={this.state.siteForm.enable_nsfw}
+                  checked={toUndefined(this.state.siteForm.enable_nsfw)}
                   onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
                 />
                 <label
@@ -273,7 +296,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-open-registration"
                   type="checkbox"
-                  checked={this.state.siteForm.open_registration}
+                  checked={toUndefined(this.state.siteForm.open_registration)}
                   onChange={linkEvent(
                     this,
                     this.handleSiteOpenRegistrationChange
@@ -295,7 +318,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-community-creation-admin-only"
                   type="checkbox"
-                  checked={this.state.siteForm.community_creation_admin_only}
+                  checked={toUndefined(
+                    this.state.siteForm.community_creation_admin_only
+                  )}
                   onChange={linkEvent(
                     this,
                     this.handleSiteCommunityCreationAdminOnly
@@ -317,7 +342,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-require-email-verification"
                   type="checkbox"
-                  checked={this.state.siteForm.require_email_verification}
+                  checked={toUndefined(
+                    this.state.siteForm.require_email_verification
+                  )}
                   onChange={linkEvent(
                     this,
                     this.handleSiteRequireEmailVerification
@@ -339,7 +366,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-require-application"
                   type="checkbox"
-                  checked={this.state.siteForm.require_application}
+                  checked={toUndefined(this.state.siteForm.require_application)}
                   onChange={linkEvent(this, this.handleSiteRequireApplication)}
                 />
                 <label
@@ -361,12 +388,12 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               </label>
               <select
                 id="create-site-default-theme"
-                value={this.state.siteForm.default_theme}
+                value={toUndefined(this.state.siteForm.default_theme)}
                 onChange={linkEvent(this, this.handleSiteDefaultTheme)}
                 class="custom-select w-auto"
               >
                 <option value="browser">{i18n.t("browser_default")}</option>
-                {this.state.themeList.map(theme => (
+                {this.state.themeList.unwrapOr([]).map(theme => (
                   <option value={theme}>{theme}</option>
                 ))}
               </select>
@@ -378,7 +405,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               <div class="col-sm-9">
                 <ListingTypeSelect
                   type_={
-                    ListingType[this.state.siteForm.default_post_listing_type]
+                    ListingType[
+                      this.state.siteForm.default_post_listing_type.unwrapOr(
+                        "Local"
+                      )
+                    ]
                   }
                   showLocal
                   showSubscribed={false}
@@ -394,7 +425,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
                   class="form-check-input"
                   id="create-site-private-instance"
                   type="checkbox"
-                  value={this.state.siteForm.default_theme}
+                  value={toUndefined(this.state.siteForm.default_theme)}
                   onChange={linkEvent(this, this.handleSitePrivateInstance)}
                 />
                 <label
@@ -415,13 +446,13 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               >
                 {this.state.loading ? (
                   <Spinner />
-                ) : this.props.site ? (
+                ) : this.props.site.isSome() ? (
                   capitalizeFirstLetter(i18n.t("save"))
                 ) : (
                   capitalizeFirstLetter(i18n.t("create"))
                 )}
               </button>
-              {this.props.site && (
+              {this.props.site.isSome() && (
                 <button
                   type="button"
                   class="btn btn-secondary"
@@ -440,81 +471,98 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   handleCreateSiteSubmit(i: SiteForm, event: any) {
     event.preventDefault();
     i.state.loading = true;
-    if (i.props.site) {
+    i.state.siteForm.auth = auth().unwrap();
+
+    if (i.props.site.isSome()) {
       WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
       i.props.onEdit();
     } else {
-      let form: CreateSite = {
-        name: i.state.siteForm.name || "My site",
-        ...i.state.siteForm,
-      };
+      let sForm = i.state.siteForm;
+      let form = new CreateSite({
+        name: sForm.name.unwrapOr("My site"),
+        sidebar: sForm.sidebar,
+        description: sForm.description,
+        icon: sForm.icon,
+        banner: sForm.banner,
+        community_creation_admin_only: sForm.community_creation_admin_only,
+        enable_nsfw: sForm.enable_nsfw,
+        enable_downvotes: sForm.enable_downvotes,
+        require_application: sForm.require_application,
+        application_question: sForm.application_question,
+        open_registration: sForm.open_registration,
+        require_email_verification: sForm.require_email_verification,
+        private_instance: sForm.private_instance,
+        default_theme: sForm.default_theme,
+        default_post_listing_type: sForm.default_post_listing_type,
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.createSite(form));
     }
     i.setState(i.state);
   }
 
   handleSiteNameChange(i: SiteForm, event: any) {
-    i.state.siteForm.name = event.target.value;
+    i.state.siteForm.name = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleSiteSidebarChange(val: string) {
-    this.state.siteForm.sidebar = val;
+    this.state.siteForm.sidebar = Some(val);
     this.setState(this.state);
   }
 
   handleSiteLegalInfoChange(val: string) {
-    this.state.siteForm.legal_information = val;
+    this.state.siteForm.legal_information = Some(val);
     this.setState(this.state);
   }
 
   handleSiteApplicationQuestionChange(val: string) {
-    this.state.siteForm.application_question = val;
+    this.state.siteForm.application_question = Some(val);
     this.setState(this.state);
   }
 
   handleSiteDescChange(i: SiteForm, event: any) {
-    i.state.siteForm.description = event.target.value;
+    i.state.siteForm.description = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleSiteEnableNsfwChange(i: SiteForm, event: any) {
-    i.state.siteForm.enable_nsfw = event.target.checked;
+    i.state.siteForm.enable_nsfw = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteOpenRegistrationChange(i: SiteForm, event: any) {
-    i.state.siteForm.open_registration = event.target.checked;
+    i.state.siteForm.open_registration = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
-    i.state.siteForm.community_creation_admin_only = event.target.checked;
+    i.state.siteForm.community_creation_admin_only = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
-    i.state.siteForm.enable_downvotes = event.target.checked;
+    i.state.siteForm.enable_downvotes = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteRequireApplication(i: SiteForm, event: any) {
-    i.state.siteForm.require_application = event.target.checked;
+    i.state.siteForm.require_application = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteRequireEmailVerification(i: SiteForm, event: any) {
-    i.state.siteForm.require_email_verification = event.target.checked;
+    i.state.siteForm.require_email_verification = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSitePrivateInstance(i: SiteForm, event: any) {
-    i.state.siteForm.private_instance = event.target.checked;
+    i.state.siteForm.private_instance = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleSiteDefaultTheme(i: SiteForm, event: any) {
-    i.state.siteForm.default_theme = event.target.value;
+    i.state.siteForm.default_theme = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -523,28 +571,29 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   }
 
   handleIconUpload(url: string) {
-    this.state.siteForm.icon = url;
+    this.state.siteForm.icon = Some(url);
     this.setState(this.state);
   }
 
   handleIconRemove() {
-    this.state.siteForm.icon = "";
+    this.state.siteForm.icon = Some("");
     this.setState(this.state);
   }
 
   handleBannerUpload(url: string) {
-    this.state.siteForm.banner = url;
+    this.state.siteForm.banner = Some(url);
     this.setState(this.state);
   }
 
   handleBannerRemove() {
-    this.state.siteForm.banner = "";
+    this.state.siteForm.banner = Some("");
     this.setState(this.state);
   }
 
   handleDefaultPostListingTypeChange(val: ListingType) {
-    this.state.siteForm.default_post_listing_type =
-      ListingType[ListingType[val]];
+    this.state.siteForm.default_post_listing_type = Some(
+      ListingType[ListingType[val]]
+    );
     this.setState(this.state);
   }
 }
index 0e7e73ca7e163a8a632dd8a74268c651f8c128ce..79a8a4306c410eaae00c68520b1799dbabff1762 100644 (file)
@@ -1,9 +1,9 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client";
 import { i18n } from "../../i18next";
-import { UserService } from "../../services";
-import { mdToHtml, numToSI } from "../../utils";
+import { amAdmin, mdToHtml, numToSI } from "../../utils";
 import { BannerIconHeader } from "../common/banner-icon-header";
 import { Icon } from "../common/icon";
 import { PersonListing } from "../person/person-listing";
@@ -12,9 +12,9 @@ import { SiteForm } from "./site-form";
 interface SiteSidebarProps {
   site: Site;
   showLocal: boolean;
-  counts?: SiteAggregates;
-  admins?: PersonViewSafe[];
-  online?: number;
+  counts: Option<SiteAggregates>;
+  admins: Option<PersonViewSafe[]>;
+  online: Option<number>;
 }
 
 interface SiteSidebarState {
@@ -44,18 +44,18 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
             <div>
               <div class="mb-2">
                 {this.siteName()}
-                {this.props.admins && this.adminButtons()}
+                {this.props.admins.isSome() && this.adminButtons()}
               </div>
               {!this.state.collapsed && (
                 <>
-                  <BannerIconHeader banner={site.banner} />
+                  <BannerIconHeader banner={site.banner} icon={None} />
                   {this.siteInfo()}
                 </>
               )}
             </div>
           ) : (
             <SiteForm
-              site={site}
+              site={Some(site)}
               showLocal={this.props.showLocal}
               onEdit={this.handleEditSite}
               onCancel={this.handleEditCancel}
@@ -69,23 +69,21 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
   siteName() {
     let site = this.props.site;
     return (
-      site.name && (
-        <h5 class="mb-0 d-inline">
-          {site.name}
-          <button
-            class="btn btn-sm text-muted"
-            onClick={linkEvent(this, this.handleCollapseSidebar)}
-            aria-label={i18n.t("collapse")}
-            data-tippy-content={i18n.t("collapse")}
-          >
-            {this.state.collapsed ? (
-              <Icon icon="plus-square" classes="icon-inline" />
-            ) : (
-              <Icon icon="minus-square" classes="icon-inline" />
-            )}
-          </button>
-        </h5>
-      )
+      <h5 class="mb-0 d-inline">
+        {site.name}
+        <button
+          class="btn btn-sm text-muted"
+          onClick={linkEvent(this, this.handleCollapseSidebar)}
+          aria-label={i18n.t("collapse")}
+          data-tippy-content={i18n.t("collapse")}
+        >
+          {this.state.collapsed ? (
+            <Icon icon="plus-square" classes="icon-inline" />
+          ) : (
+            <Icon icon="minus-square" classes="icon-inline" />
+          )}
+        </button>
+      </h5>
     );
   }
 
@@ -93,17 +91,29 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
     let site = this.props.site;
     return (
       <div>
-        {site.description && <h6>{site.description}</h6>}
-        {site.sidebar && this.siteSidebar()}
-        {this.props.counts && this.badges()}
-        {this.props.admins && this.admins()}
+        {site.description.match({
+          some: description => <h6>{description}</h6>,
+          none: <></>,
+        })}
+        {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>
     );
   }
 
   adminButtons() {
     return (
-      this.canAdmin && (
+      amAdmin(this.props.admins) && (
         <ul class="list-inline mb-1 text-muted font-weight-bold">
           <li className="list-inline-item-action">
             <button
@@ -120,20 +130,17 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
     );
   }
 
-  siteSidebar() {
+  siteSidebar(sidebar: string) {
     return (
-      <div
-        className="md-div"
-        dangerouslySetInnerHTML={mdToHtml(this.props.site.sidebar)}
-      />
+      <div className="md-div" dangerouslySetInnerHTML={mdToHtml(sidebar)} />
     );
   }
 
-  admins() {
+  admins(admins: PersonViewSafe[]) {
     return (
       <ul class="mt-1 list-inline small mb-0">
         <li class="list-inline-item">{i18n.t("admins")}:</li>
-        {this.props.admins?.map(av => (
+        {admins.map(av => (
           <li class="list-inline-item">
             <PersonListing person={av.person} />
           </li>
@@ -142,9 +149,9 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
     );
   }
 
-  badges() {
-    let counts = this.props.counts;
-    let online = this.props.online;
+  badges(siteAggregates: SiteAggregates) {
+    let counts = siteAggregates;
+    let online = this.props.online.unwrapOr(1);
     return (
       <ul class="my-2 list-inline">
         <li className="list-inline-item badge badge-secondary">
@@ -238,15 +245,6 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
     );
   }
 
-  get canAdmin(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      this.props.admins
-        .map(a => a.person.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
-    );
-  }
-
   handleCollapseSidebar(i: SiteSidebar) {
     i.state.collapsed = !i.state.collapsed;
     i.setState(i.state);
index e01b81e99c9d351dde708e8967831423e5d0296b..0003e54b8d6963bafef3bc61519376458fbfd7d8 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component } from "inferno";
 import { Link } from "inferno-router";
 import {
@@ -6,6 +7,7 @@ import {
   GetCommunityResponse,
   GetModlog,
   GetModlogResponse,
+  GetSiteResponse,
   ModAddCommunityView,
   ModAddView,
   ModBanFromCommunityView,
@@ -17,25 +19,25 @@ import {
   ModStickyPostView,
   ModTransferCommunityView,
   PersonSafe,
-  SiteView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import moment from "moment";
 import { Subscription } from "rxjs";
 import { i18n } from "../i18next";
 import { InitialFetchRequest } from "../interfaces";
-import { UserService, WebSocketService } from "../services";
+import { WebSocketService } from "../services";
 import {
-  authField,
+  amAdmin,
+  amMod,
+  auth,
   fetchLimit,
   isBrowser,
   setIsoData,
-  setOptionalAuth,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../utils";
 import { HtmlTags } from "./common/html-tags";
 import { Spinner } from "./common/icon";
@@ -75,34 +77,28 @@ type ModlogType = {
 };
 
 interface ModlogState {
-  res: GetModlogResponse;
-  communityId?: number;
-  communityName?: string;
-  communityMods?: CommunityModeratorView[];
+  res: Option<GetModlogResponse>;
+  communityId: Option<number>;
+  communityMods: Option<CommunityModeratorView[]>;
   page: number;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
   loading: boolean;
 }
 
 export class Modlog extends Component<any, ModlogState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetModlogResponse,
+    GetCommunityResponse
+  );
   private subscription: Subscription;
   private emptyState: ModlogState = {
-    res: {
-      removed_posts: [],
-      locked_posts: [],
-      stickied_posts: [],
-      removed_comments: [],
-      removed_communities: [],
-      banned_from_community: [],
-      banned: [],
-      added_to_community: [],
-      transferred_to_community: [],
-      added: [],
-    },
+    res: None,
+    communityId: None,
+    communityMods: None,
     page: 1,
     loading: true,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
   };
 
   constructor(props: any, context: any) {
@@ -112,22 +108,23 @@ export class Modlog extends Component<any, ModlogState> {
     this.handlePageChange = this.handlePageChange.bind(this);
 
     this.state.communityId = this.props.match.params.community_id
-      ? Number(this.props.match.params.community_id)
-      : undefined;
+      ? Some(Number(this.props.match.params.community_id))
+      : None;
 
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      let data = this.isoData.routeData[0];
-      this.state.res = data;
-      this.state.loading = false;
+      this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
 
       // Getting the moderators
-      if (this.isoData.routeData[1]) {
-        this.state.communityMods = this.isoData.routeData[1].moderators;
-      }
+      let communityRes = Some(
+        this.isoData.routeData[1] as GetCommunityResponse
+      );
+      this.state.communityMods = communityRes.map(c => c.moderators);
+
+      this.state.loading = false;
     } else {
       this.refetch();
     }
@@ -226,12 +223,6 @@ export class Modlog extends Component<any, ModlogState> {
     combined.push(...added);
     combined.push(...banned);
 
-    if (this.state.communityId && combined.length > 0) {
-      this.state.communityName = (
-        combined[0].view as ModRemovePostView
-      ).community.name;
-    }
-
     // Sort them by time
     combined.sort((a, b) => b.when_.localeCompare(a.when_));
 
@@ -294,11 +285,11 @@ export class Modlog extends Component<any, ModlogState> {
           <span>
             Community <CommunityLink community={mrco.community} />
           </span>,
-          mrco.mod_remove_community.reason &&
-            ` reason: ${mrco.mod_remove_community.reason}`,
-          mrco.mod_remove_community.expires &&
+          mrco.mod_remove_community.reason.isSome() &&
+            ` reason: ${mrco.mod_remove_community.reason.unwrap()}`,
+          mrco.mod_remove_community.expires.isSome() &&
             ` expires: ${moment
-              .utc(mrco.mod_remove_community.expires)
+              .utc(mrco.mod_remove_community.expires.unwrap())
               .fromNow()}`,
         ];
       }
@@ -316,13 +307,13 @@ export class Modlog extends Component<any, ModlogState> {
             <CommunityLink community={mbfc.community} />
           </span>,
           <div>
-            {mbfc.mod_ban_from_community.reason &&
-              ` reason: ${mbfc.mod_ban_from_community.reason}`}
+            {mbfc.mod_ban_from_community.reason.isSome() &&
+              ` reason: ${mbfc.mod_ban_from_community.reason.unwrap()}`}
           </div>,
           <div>
-            {mbfc.mod_ban_from_community.expires &&
+            {mbfc.mod_ban_from_community.expires.isSome() &&
               ` expires: ${moment
-                .utc(mbfc.mod_ban_from_community.expires)
+                .utc(mbfc.mod_ban_from_community.expires.unwrap())
                 .fromNow()}`}
           </div>,
         ];
@@ -364,17 +355,24 @@ export class Modlog extends Component<any, ModlogState> {
           <span>
             <PersonListing person={mb.banned_person} />
           </span>,
-          <div>{mb.mod_ban.reason && ` reason: ${mb.mod_ban.reason}`}</div>,
           <div>
-            {mb.mod_ban.expires &&
-              ` expires: ${moment.utc(mb.mod_ban.expires).fromNow()}`}
+            {mb.mod_ban.reason.isSome() &&
+              ` reason: ${mb.mod_ban.reason.unwrap()}`}
+          </div>,
+          <div>
+            {mb.mod_ban.expires.isSome() &&
+              ` expires: ${moment.utc(mb.mod_ban.expires.unwrap()).fromNow()}`}
           </div>,
         ];
       }
       case ModlogEnum.ModAdd: {
         let ma = i.view as ModAddView;
         return [
-          <span>{ma.mod_add.removed ? "Removed " : "Appointed "} </span>,
+          <span>
+            {ma.mod_add.removed.isSome() && ma.mod_add.removed.unwrap()
+              ? "Removed "
+              : "Appointed "}{" "}
+          </span>,
           <span>
             <PersonListing person={ma.modded_person} />
           </span>,
@@ -387,17 +385,17 @@ export class Modlog extends Component<any, ModlogState> {
   }
 
   combined() {
-    let combined = this.buildCombined(this.state.res);
+    let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
 
     return (
       <tbody>
         {combined.map(i => (
           <tr>
             <td>
-              <MomentTime data={i} />
+              <MomentTime published={i.when_} updated={None} />
             </td>
             <td>
-              {this.isAdminOrMod ? (
+              {this.amAdminOrMod ? (
                 <PersonListing person={i.view.moderator} />
               ) : (
                 <div>{this.modOrAdminText(i.view.moderator)}</div>
@@ -410,19 +408,11 @@ export class Modlog extends Component<any, ModlogState> {
     );
   }
 
-  get isAdminOrMod(): boolean {
-    let isAdmin =
-      UserService.Instance.myUserInfo &&
-      this.isoData.site_res.admins
-        .map(a => a.person.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
-    let isMod =
-      UserService.Instance.myUserInfo &&
-      this.state.communityMods &&
-      this.state.communityMods
-        .map(m => m.moderator.id)
-        .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
-    return isAdmin || isMod;
+  get amAdminOrMod(): boolean {
+    return (
+      amAdmin(Some(this.state.siteRes.admins)) ||
+      amMod(this.state.communityMods)
+    );
   }
 
   modOrAdminText(person: PersonSafe): Text {
@@ -436,7 +426,10 @@ export class Modlog extends Component<any, ModlogState> {
   }
 
   get documentTitle(): string {
-    return `Modlog - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `Modlog - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -445,6 +438,8 @@ export class Modlog extends Component<any, ModlogState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         {this.state.loading ? (
           <h5>
@@ -452,17 +447,6 @@ export class Modlog extends Component<any, ModlogState> {
           </h5>
         ) : (
           <div>
-            <h5>
-              {this.state.communityName && (
-                <Link
-                  className="text-body"
-                  to={`/c/${this.state.communityName}`}
-                >
-                  /c/{this.state.communityName}{" "}
-                </Link>
-              )}
-              <span>{i18n.t("modlog")}</span>
-            </h5>
             <div class="table-responsive">
               <table id="modlog_table" class="table table-sm table-hover">
                 <thead class="pointer">
@@ -491,46 +475,52 @@ export class Modlog extends Component<any, ModlogState> {
   }
 
   refetch() {
-    let modlogForm: GetModlog = {
+    let modlogForm = new GetModlog({
       community_id: this.state.communityId,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(false),
-    };
+      mod_person_id: None,
+      page: Some(this.state.page),
+      limit: Some(fetchLimit),
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
 
-    if (this.state.communityId) {
-      let communityForm: GetCommunity = {
-        id: this.state.communityId,
-        name: this.state.communityName,
-      };
-      WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
-    }
+    this.state.communityId.match({
+      some: id => {
+        let communityForm = new GetCommunity({
+          id: Some(id),
+          name: None,
+          auth: auth(false).ok(),
+        });
+        WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
+      },
+      none: void 0,
+    });
   }
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let pathSplit = req.path.split("/");
-    let communityId = pathSplit[3];
+    let communityId = Some(pathSplit[3]).map(Number);
     let promises: Promise<any>[] = [];
 
-    let modlogForm: GetModlog = {
-      page: 1,
-      limit: fetchLimit,
-    };
-
-    if (communityId) {
-      modlogForm.community_id = Number(communityId);
-    }
-    setOptionalAuth(modlogForm, req.auth);
+    let modlogForm = new GetModlog({
+      page: Some(1),
+      limit: Some(fetchLimit),
+      community_id: communityId,
+      mod_person_id: None,
+      auth: req.auth,
+    });
 
     promises.push(req.client.getModlog(modlogForm));
 
-    if (communityId) {
-      let communityForm: GetCommunity = {
-        id: Number(communityId),
-      };
-      setOptionalAuth(communityForm, req.auth);
+    if (communityId.isSome()) {
+      let communityForm = new GetCommunity({
+        id: communityId,
+        name: None,
+        auth: req.auth,
+      });
       promises.push(req.client.getCommunity(communityForm));
+    } else {
+      promises.push(Promise.resolve());
     }
     return promises;
   }
@@ -542,14 +532,14 @@ export class Modlog extends Component<any, ModlogState> {
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.GetModlog) {
-      let data = wsJsonToRes<GetModlogResponse>(msg).data;
+      let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
       this.state.loading = false;
       window.scrollTo(0, 0);
-      this.state.res = data;
+      this.state.res = Some(data);
       this.setState(this.state);
     } else if (op == UserOperation.GetCommunity) {
-      let data = wsJsonToRes<GetCommunityResponse>(msg).data;
-      this.state.communityMods = data.moderators;
+      let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
+      this.state.communityMods = Some(data.moderators);
     }
   }
 }
index 7995baf7a6b10d8ceb4227ea5d6fe5c0435dff62..e2faffcc9afa0d58ee6da41baf5addf064b0b2b3 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   BlockPersonResponse,
@@ -9,25 +10,28 @@ import {
   GetPrivateMessages,
   GetReplies,
   GetRepliesResponse,
+  GetSiteResponse,
   PersonMentionResponse,
   PersonMentionView,
   PostReportResponse,
   PrivateMessageResponse,
   PrivateMessagesResponse,
   PrivateMessageView,
-  SiteView,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   commentsToFlatNodes,
   createCommentLikeRes,
   editCommentRes,
+  enableDownvotes,
   fetchLimit,
   isBrowser,
   relTags,
@@ -37,9 +41,7 @@ import {
   toast,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentNodes } from "../comment/comment-nodes";
 import { HtmlTags } from "../common/html-tags";
@@ -81,12 +83,17 @@ interface InboxState {
   combined: ReplyType[];
   sort: SortType;
   page: number;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
   loading: boolean;
 }
 
 export class Inbox extends Component<any, InboxState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetRepliesResponse,
+    GetPersonMentionsResponse,
+    PrivateMessagesResponse
+  );
   private subscription: Subscription;
   private emptyState: InboxState = {
     unreadOrAll: UnreadOrAll.Unread,
@@ -97,7 +104,7 @@ export class Inbox extends Component<any, InboxState> {
     combined: [],
     sort: SortType.New,
     page: 1,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
     loading: true,
   };
 
@@ -108,7 +115,7 @@ export class Inbox extends Component<any, InboxState> {
     this.handleSortChange = this.handleSortChange.bind(this);
     this.handlePageChange = this.handlePageChange.bind(this);
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -118,9 +125,13 @@ export class Inbox extends Component<any, InboxState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.replies = this.isoData.routeData[0].replies || [];
-      this.state.mentions = this.isoData.routeData[1].mentions || [];
-      this.state.messages = this.isoData.routeData[2].messages || [];
+      this.state.replies =
+        (this.isoData.routeData[0] as GetRepliesResponse).replies || [];
+      this.state.mentions =
+        (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions || [];
+      this.state.messages =
+        (this.isoData.routeData[2] as PrivateMessagesResponse)
+          .private_messages || [];
       this.state.combined = this.buildCombined();
       this.state.loading = false;
     } else {
@@ -135,13 +146,23 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   get documentTitle(): string {
-    return `@${
-      UserService.Instance.myUserInfo.local_user_view.person.name
-    } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        UserService.Instance.myUserInfo.match({
+          some: mui =>
+            `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
+              siteView.site.name
+            }`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
   render() {
-    let inboxRss = `/feeds/inbox/${UserService.Instance.auth}.xml`;
+    let inboxRss = auth()
+      .ok()
+      .map(a => `/feeds/inbox/${a}.xml`);
     return (
       <div class="container">
         {this.state.loading ? (
@@ -154,19 +175,26 @@ export class Inbox extends Component<any, InboxState> {
               <HtmlTags
                 title={this.documentTitle}
                 path={this.context.router.route.match.url}
+                description={None}
+                image={None}
               />
               <h5 class="mb-2">
                 {i18n.t("inbox")}
-                <small>
-                  <a href={inboxRss} title="RSS" rel={relTags}>
-                    <Icon icon="rss" classes="ml-2 text-muted small" />
-                  </a>
-                  <link
-                    rel="alternate"
-                    type="application/atom+xml"
-                    href={inboxRss}
-                  />
-                </small>
+                {inboxRss.match({
+                  some: rss => (
+                    <small>
+                      <a href={rss} title="RSS" rel={relTags}>
+                        <Icon icon="rss" classes="ml-2 text-muted small" />
+                      </a>
+                      <link
+                        rel="alternate"
+                        type="application/atom+xml"
+                        href={rss}
+                      />
+                    </small>
+                  ),
+                  none: <></>,
+                })}
               </h5>
               {this.state.replies.length +
                 this.state.mentions.length +
@@ -355,11 +383,14 @@ export class Inbox extends Component<any, InboxState> {
           <CommentNodes
             key={i.id}
             nodes={[{ comment_view: i.view as CommentView }]}
+            moderators={None}
+            admins={None}
+            maxCommentsShown={None}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
           />
         );
       case ReplyEnum.Mention:
@@ -367,11 +398,14 @@ export class Inbox extends Component<any, InboxState> {
           <CommentNodes
             key={i.id}
             nodes={[{ comment_view: i.view as PersonMentionView }]}
+            moderators={None}
+            admins={None}
+            maxCommentsShown={None}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
           />
         );
       case ReplyEnum.Message:
@@ -395,11 +429,14 @@ export class Inbox extends Component<any, InboxState> {
       <div>
         <CommentNodes
           nodes={commentsToFlatNodes(this.state.replies)}
+          moderators={None}
+          admins={None}
+          maxCommentsShown={None}
           noIndent
           markable
           showCommunity
           showContext
-          enableDownvotes={this.state.site_view.site.enable_downvotes}
+          enableDownvotes={enableDownvotes(this.state.siteRes)}
         />
       </div>
     );
@@ -412,11 +449,14 @@ export class Inbox extends Component<any, InboxState> {
           <CommentNodes
             key={umv.person_mention.id}
             nodes={[{ comment_view: umv }]}
+            moderators={None}
+            admins={None}
+            maxCommentsShown={None}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.site_view.site.enable_downvotes}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
           />
         ))}
       </div>
@@ -459,62 +499,67 @@ export class Inbox extends Component<any, InboxState> {
     let promises: Promise<any>[] = [];
 
     // It can be /u/me, or /username/1
-    let repliesForm: GetReplies = {
-      sort: SortType.New,
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+    let repliesForm = new GetReplies({
+      sort: Some(SortType.New),
+      unread_only: Some(true),
+      page: Some(1),
+      limit: Some(fetchLimit),
+      auth: req.auth.unwrap(),
+    });
     promises.push(req.client.getReplies(repliesForm));
 
-    let personMentionsForm: GetPersonMentions = {
-      sort: SortType.New,
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+    let personMentionsForm = new GetPersonMentions({
+      sort: Some(SortType.New),
+      unread_only: Some(true),
+      page: Some(1),
+      limit: Some(fetchLimit),
+      auth: req.auth.unwrap(),
+    });
     promises.push(req.client.getPersonMentions(personMentionsForm));
 
-    let privateMessagesForm: GetPrivateMessages = {
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+    let privateMessagesForm = new GetPrivateMessages({
+      unread_only: Some(true),
+      page: Some(1),
+      limit: Some(fetchLimit),
+      auth: req.auth.unwrap(),
+    });
     promises.push(req.client.getPrivateMessages(privateMessagesForm));
 
     return promises;
   }
 
   refetch() {
-    let repliesForm: GetReplies = {
-      sort: this.state.sort,
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+    let sort = Some(this.state.sort);
+    let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
+    let page = Some(this.state.page);
+    let limit = Some(fetchLimit);
+
+    let repliesForm = new GetReplies({
+      sort,
+      unread_only,
+      page,
+      limit,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
 
-    let personMentionsForm: GetPersonMentions = {
-      sort: this.state.sort,
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+    let personMentionsForm = new GetPersonMentions({
+      sort,
+      unread_only,
+      page,
+      limit,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(
       wsClient.getPersonMentions(personMentionsForm)
     );
 
-    let privateMessagesForm: GetPrivateMessages = {
-      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+    let privateMessagesForm = new GetPrivateMessages({
+      unread_only,
+      page,
+      limit,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(
       wsClient.getPrivateMessages(privateMessagesForm)
     );
@@ -530,7 +575,7 @@ export class Inbox extends Component<any, InboxState> {
   markAllAsRead(i: Inbox) {
     WebSocketService.Instance.send(
       wsClient.markAllAsRead({
-        auth: authField(),
+        auth: auth().unwrap(),
       })
     );
     i.state.replies = [];
@@ -559,7 +604,7 @@ export class Inbox extends Component<any, InboxState> {
     } else if (msg.reconnect) {
       this.refetch();
     } else if (op == UserOperation.GetReplies) {
-      let data = wsJsonToRes<GetRepliesResponse>(msg).data;
+      let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
       this.state.replies = data.replies;
       this.state.combined = this.buildCombined();
       this.state.loading = false;
@@ -567,21 +612,30 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.GetPersonMentions) {
-      let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
+      let data = wsJsonToRes<GetPersonMentionsResponse>(
+        msg,
+        GetPersonMentionsResponse
+      );
       this.state.mentions = data.mentions;
       this.state.combined = this.buildCombined();
       window.scrollTo(0, 0);
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.GetPrivateMessages) {
-      let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessagesResponse>(
+        msg,
+        PrivateMessagesResponse
+      );
       this.state.messages = data.private_messages;
       this.state.combined = this.buildCombined();
       window.scrollTo(0, 0);
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.EditPrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
       let found: PrivateMessageView = this.state.messages.find(
         m =>
           m.private_message.id === data.private_message_view.private_message.id
@@ -597,7 +651,10 @@ export class Inbox extends Component<any, InboxState> {
       }
       this.setState(this.state);
     } else if (op == UserOperation.DeletePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
       let found: PrivateMessageView = this.state.messages.find(
         m =>
           m.private_message.id === data.private_message_view.private_message.id
@@ -613,7 +670,10 @@ export class Inbox extends Component<any, InboxState> {
       }
       this.setState(this.state);
     } else if (op == UserOperation.MarkPrivateMessageAsRead) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
       let found: PrivateMessageView = this.state.messages.find(
         m =>
           m.private_message.id === data.private_message_view.private_message.id
@@ -653,11 +713,11 @@ export class Inbox extends Component<any, InboxState> {
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       editCommentRes(data.comment_view, this.state.replies);
       this.setState(this.state);
     } else if (op == UserOperation.MarkCommentAsRead) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
 
       // If youre in the unread view, just remove it from the list
       if (
@@ -685,7 +745,7 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.MarkPersonMentionAsRead) {
-      let data = wsJsonToRes<PersonMentionResponse>(msg).data;
+      let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
 
       // TODO this might not be correct, it might need to use the comment id
       let found = this.state.mentions.find(
@@ -732,85 +792,109 @@ export class Inbox extends Component<any, InboxState> {
       this.sendUnreadCount(data.person_mention_view.person_mention.read);
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-
-      if (
-        data.recipient_ids.includes(
-          UserService.Instance.myUserInfo.local_user_view.local_user.id
-        )
-      ) {
-        this.state.replies.unshift(data.comment_view);
-        this.state.combined.unshift(this.replyToReplyType(data.comment_view));
-        this.setState(this.state);
-      } else if (
-        data.comment_view.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        // If youre in the unread view, just remove it from the list
-        if (this.state.unreadOrAll == UnreadOrAll.Unread) {
-          this.state.replies = this.state.replies.filter(
-            r => r.comment.id !== data.comment_view.comment.parent_id
-          );
-          this.state.mentions = this.state.mentions.filter(
-            m => m.comment.id !== data.comment_view.comment.parent_id
-          );
-          this.state.combined = this.state.combined.filter(r => {
-            if (this.isMention(r.view))
-              return r.view.comment.id !== data.comment_view.comment.parent_id;
-            else return r.id !== data.comment_view.comment.parent_id;
-          });
-        } else {
-          let mention_found = this.state.mentions.find(
-            i => i.comment.id == data.comment_view.comment.parent_id
-          );
-          if (mention_found) {
-            mention_found.person_mention.read = true;
-          }
-          let reply_found = this.state.replies.find(
-            i => i.comment.id == data.comment_view.comment.parent_id
-          );
-          if (reply_found) {
-            reply_found.comment.read = true;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
+            this.state.replies.unshift(data.comment_view);
+            this.state.combined.unshift(
+              this.replyToReplyType(data.comment_view)
+            );
+            this.setState(this.state);
+          } else if (
+            data.comment_view.creator.id == mui.local_user_view.person.id
+          ) {
+            // If youre in the unread view, just remove it from the list
+            if (this.state.unreadOrAll == UnreadOrAll.Unread) {
+              this.state.replies = this.state.replies.filter(
+                r =>
+                  r.comment.id !==
+                  data.comment_view.comment.parent_id.unwrapOr(0)
+              );
+              this.state.mentions = this.state.mentions.filter(
+                m =>
+                  m.comment.id !==
+                  data.comment_view.comment.parent_id.unwrapOr(0)
+              );
+              this.state.combined = this.state.combined.filter(r => {
+                if (this.isMention(r.view))
+                  return (
+                    r.view.comment.id !==
+                    data.comment_view.comment.parent_id.unwrapOr(0)
+                  );
+                else
+                  return (
+                    r.id !== data.comment_view.comment.parent_id.unwrapOr(0)
+                  );
+              });
+            } else {
+              let mention_found = this.state.mentions.find(
+                i =>
+                  i.comment.id ==
+                  data.comment_view.comment.parent_id.unwrapOr(0)
+              );
+              if (mention_found) {
+                mention_found.person_mention.read = true;
+              }
+              let reply_found = this.state.replies.find(
+                i =>
+                  i.comment.id ==
+                  data.comment_view.comment.parent_id.unwrapOr(0)
+              );
+              if (reply_found) {
+                reply_found.comment.read = true;
+              }
+              this.state.combined = this.buildCombined();
+            }
+            this.sendUnreadCount(true);
+            this.setState(this.state);
+            setupTippy();
+            // TODO this seems wrong, you should be using form_id
+            toast(i18n.t("reply_sent"));
           }
-          this.state.combined = this.buildCombined();
-        }
-        this.sendUnreadCount(true);
-        this.setState(this.state);
-        setupTippy();
-        // TODO this seems wrong, you should be using form_id
-        toast(i18n.t("reply_sent"));
-      }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.CreatePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
-      if (
-        data.private_message_view.recipient.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        this.state.messages.unshift(data.private_message_view);
-        this.state.combined.unshift(
-          this.messageToReplyType(data.private_message_view)
-        );
-        this.setState(this.state);
-      }
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (
+            data.private_message_view.recipient.id ==
+            mui.local_user_view.person.id
+          ) {
+            this.state.messages.unshift(data.private_message_view);
+            this.state.combined.unshift(
+              this.messageToReplyType(data.private_message_view)
+            );
+            this.setState(this.state);
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       saveCommentRes(data.comment_view, this.state.replies);
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       createCommentLikeRes(data.comment_view, this.state.replies);
       this.setState(this.state);
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
     } else if (op == UserOperation.CreatePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
+      let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
     } else if (op == UserOperation.CreateCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
+      let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
index 2b23ddcb9282550d266869cf533cbc6798c00eae..8120fe97630e67d60a972b9103fdeb4d5071a7e7 100644 (file)
@@ -1,9 +1,12 @@
+import { None } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
+  GetSiteResponse,
   LoginResponse,
   PasswordChange as PasswordChangeForm,
-  SiteView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
@@ -14,9 +17,7 @@ import {
   setIsoData,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -24,7 +25,7 @@ import { Spinner } from "../common/icon";
 interface State {
   passwordChangeForm: PasswordChangeForm;
   loading: boolean;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
 }
 
 export class PasswordChange extends Component<any, State> {
@@ -32,13 +33,13 @@ export class PasswordChange extends Component<any, State> {
   private subscription: Subscription;
 
   emptyState: State = {
-    passwordChangeForm: {
+    passwordChangeForm: new PasswordChangeForm({
       token: this.props.match.params.token,
       password: undefined,
       password_verify: undefined,
-    },
+    }),
     loading: false,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
   };
 
   constructor(props: any, context: any) {
@@ -57,7 +58,10 @@ export class PasswordChange extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("password_change")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("password_change")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -66,6 +70,8 @@ export class PasswordChange extends Component<any, State> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
@@ -156,7 +162,7 @@ export class PasswordChange extends Component<any, State> {
       this.setState(this.state);
       return;
     } else if (op == UserOperation.PasswordChange) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
+      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
       this.state = this.emptyState;
       this.setState(this.state);
       UserService.Instance.login(data);
index 1e37a58136ee70451bf147379db0e26eb6dbdad3..0dabf2aff9e4c6e5bda5220c281fec4217e01118 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Some } from "@sniptt/monads/build";
 import { Component } from "inferno";
 import {
   CommentView,
@@ -89,7 +90,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
           <CommentNodes
             key={i.id}
             nodes={[{ comment_view: c }]}
-            admins={this.props.admins}
+            admins={Some(this.props.admins)}
+            moderators={None}
+            maxCommentsShown={None}
             noBorder
             noIndent
             showCommunity
@@ -104,7 +107,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
           <PostListing
             key={i.id}
             post_view={p}
-            admins={this.props.admins}
+            admins={Some(this.props.admins)}
+            duplicates={None}
+            moderators={None}
             showCommunity
             enableDownvotes={this.props.enableDownvotes}
             enableNsfw={this.props.enableNsfw}
@@ -154,7 +159,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
       <div>
         <CommentNodes
           nodes={commentsToFlatNodes(this.props.personRes.comments)}
-          admins={this.props.admins}
+          admins={Some(this.props.admins)}
+          moderators={None}
+          maxCommentsShown={None}
           noIndent
           showCommunity
           showContext
@@ -171,8 +178,10 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
           <>
             <PostListing
               post_view={post}
-              admins={this.props.admins}
+              admins={Some(this.props.admins)}
               showCommunity
+              duplicates={None}
+              moderators={None}
               enableDownvotes={this.props.enableDownvotes}
               enableNsfw={this.props.enableNsfw}
             />
index 56db662b24d93d4f241ee0bef9ba0c3b4c45982d..88b88820252d141767be8aa3cd5353c76858ee72 100644 (file)
@@ -21,7 +21,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
 
   render() {
     let person = this.props.person;
-    let local = person.local == null ? true : person.local;
+    let local = person.local;
     let apubName: string, link: string;
 
     if (local) {
@@ -37,11 +37,9 @@ export class PersonListing extends Component<PersonListingProps, any> {
 
     let displayName = this.props.useApubName
       ? apubName
-      : person.display_name
-      ? person.display_name
-      : apubName;
+      : person.display_name.unwrapOr(apubName);
 
-    if (this.props.showApubName && !local && person.display_name) {
+    if (this.props.showApubName && !local && person.display_name.isSome()) {
       displayName = `${displayName} (${apubName})`;
     }
 
@@ -72,12 +70,14 @@ export class PersonListing extends Component<PersonListingProps, any> {
   }
 
   avatarAndName(displayName: string) {
-    let person = this.props.person;
     return (
       <>
-        {!this.props.hideAvatar && person.avatar && showAvatars() && (
-          <PictrsImage src={person.avatar} icon />
-        )}
+        {this.props.person.avatar.match({
+          some: avatar =>
+            !this.props.hideAvatar &&
+            showAvatars() && <PictrsImage src={avatar} icon />,
+          none: <></>,
+        })}
         <span>{displayName}</span>
       </>
     );
index 372a899f521a11731793a5429680580cbb85ae7c..053e4b27b754402375168483221042fd5b0c1ffb 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import {
@@ -12,7 +13,10 @@ import {
   GetSiteResponse,
   PostResponse,
   SortType,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import moment from "moment";
 import { Subscription } from "rxjs";
@@ -20,18 +24,20 @@ import { i18n } from "../../i18next";
 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   canMod,
   capitalizeFirstLetter,
   createCommentLikeRes,
   createPostLikeFindRes,
   editCommentRes,
   editPostFindRes,
+  enableDownvotes,
+  enableNsfw,
   fetchLimit,
   futureDaysToUnixTime,
   getUsernameFromProps,
+  isAdmin,
   isBanned,
-  isMod,
   mdToHtml,
   numToSI,
   relTags,
@@ -40,14 +46,11 @@ import {
   saveCommentRes,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   setupTippy,
   toast,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { BannerIconHeader } from "../common/banner-icon-header";
 import { HtmlTags } from "../common/html-tags";
@@ -59,18 +62,18 @@ import { PersonDetails } from "./person-details";
 import { PersonListing } from "./person-listing";
 
 interface ProfileState {
-  personRes: GetPersonDetailsResponse;
+  personRes: Option<GetPersonDetailsResponse>;
   userName: string;
   view: PersonDetailsView;
   sort: SortType;
   page: number;
   loading: boolean;
   personBlocked: boolean;
-  siteRes: GetSiteResponse;
+  banReason: Option<string>;
+  banExpireDays: Option<number>;
   showBanDialog: boolean;
-  banReason: string;
-  banExpireDays: number;
   removeData: boolean;
+  siteRes: GetSiteResponse;
 }
 
 interface ProfileProps {
@@ -88,10 +91,10 @@ interface UrlParams {
 }
 
 export class Profile extends Component<any, ProfileState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, GetPersonDetailsResponse);
   private subscription: Subscription;
   private emptyState: ProfileState = {
-    personRes: undefined,
+    personRes: None,
     userName: getUsernameFromProps(this.props),
     loading: true,
     view: Profile.getViewFromProps(this.props.match.view),
@@ -117,7 +120,9 @@ export class Profile extends Component<any, ProfileState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.personRes = this.isoData.routeData[0];
+      this.state.personRes = Some(
+        this.isoData.routeData[0] as GetPersonDetailsResponse
+      );
       this.state.loading = false;
     } else {
       this.fetchUserData();
@@ -127,28 +132,44 @@ export class Profile extends Component<any, ProfileState> {
   }
 
   fetchUserData() {
-    let form: GetPersonDetails = {
-      username: this.state.userName,
-      sort: this.state.sort,
-      saved_only: this.state.view === PersonDetailsView.Saved,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(false),
-    };
+    let form = new GetPersonDetails({
+      username: Some(this.state.userName),
+      person_id: None,
+      community_id: None,
+      sort: Some(this.state.sort),
+      saved_only: Some(this.state.view === PersonDetailsView.Saved),
+      page: Some(this.state.page),
+      limit: Some(fetchLimit),
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
   }
 
-  get isCurrentUser() {
-    return (
-      UserService.Instance.myUserInfo?.local_user_view.person.id ==
-      this.state.personRes?.person_view.person.id
-    );
+  get amCurrentUser() {
+    return UserService.Instance.myUserInfo.match({
+      some: mui =>
+        this.state.personRes.match({
+          some: res =>
+            mui.local_user_view.person.id == res.person_view.person.id,
+          none: false,
+        }),
+      none: false,
+    });
   }
 
   setPersonBlock() {
-    this.state.personBlocked = UserService.Instance.myUserInfo?.person_blocks
-      .map(a => a.target.id)
-      .includes(this.state.personRes?.person_view.person.id);
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        this.state.personRes.match({
+          some: res => {
+            this.state.personBlocked = mui.person_blocks
+              .map(a => a.target.id)
+              .includes(res.person_view.person.id);
+          },
+          none: void 0,
+        }),
+      none: void 0,
+    });
   }
 
   static getViewFromProps(view: string): PersonDetailsView {
@@ -165,23 +186,23 @@ export class Profile extends Component<any, ProfileState> {
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let pathSplit = req.path.split("/");
-    let promises: Promise<any>[] = [];
 
     let username = pathSplit[2];
     let view = this.getViewFromProps(pathSplit[4]);
-    let sort = this.getSortTypeFromProps(pathSplit[6]);
-    let page = this.getPageFromProps(Number(pathSplit[8]));
+    let sort = Some(this.getSortTypeFromProps(pathSplit[6]));
+    let page = Some(this.getPageFromProps(Number(pathSplit[8])));
 
-    let form: GetPersonDetails = {
+    let form = new GetPersonDetails({
+      username: Some(username),
+      person_id: None,
+      community_id: None,
       sort,
-      saved_only: view === PersonDetailsView.Saved,
+      saved_only: Some(view === PersonDetailsView.Saved),
       page,
-      limit: fetchLimit,
-      username: username,
-    };
-    setOptionalAuth(form, req.auth);
-    promises.push(req.client.getPersonDetails(form));
-    return promises;
+      limit: Some(fetchLimit),
+      auth: req.auth,
+    });
+    return [req.client.getPersonDetails(form)];
   }
 
   componentDidMount() {
@@ -215,13 +236,15 @@ export class Profile extends Component<any, ProfileState> {
   }
 
   get documentTitle(): string {
-    return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
-  }
-
-  get bioTag(): string {
-    return this.state.personRes.person_view.person.bio
-      ? this.state.personRes.person_view.person.bio
-      : undefined;
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        this.state.personRes.match({
+          some: res =>
+            `@${res.person_view.person.name} - ${siteView.site.name}`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
   render() {
@@ -232,41 +255,44 @@ export class Profile extends Component<any, ProfileState> {
             <Spinner large />
           </h5>
         ) : (
-          <div class="row">
-            <div class="col-12 col-md-8">
-              <>
-                <HtmlTags
-                  title={this.documentTitle}
-                  path={this.context.router.route.match.url}
-                  description={this.bioTag}
-                  image={this.state.personRes.person_view.person.avatar}
-                />
-                {this.userInfo()}
-                <hr />
-              </>
-              {!this.state.loading && this.selects()}
-              <PersonDetails
-                personRes={this.state.personRes}
-                admins={this.state.siteRes.admins}
-                sort={this.state.sort}
-                page={this.state.page}
-                limit={fetchLimit}
-                enableDownvotes={
-                  this.state.siteRes.site_view.site.enable_downvotes
-                }
-                enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
-                view={this.state.view}
-                onPageChange={this.handlePageChange}
-              />
-            </div>
+          this.state.personRes.match({
+            some: res => (
+              <div class="row">
+                <div class="col-12 col-md-8">
+                  <>
+                    <HtmlTags
+                      title={this.documentTitle}
+                      path={this.context.router.route.match.url}
+                      description={res.person_view.person.bio}
+                      image={res.person_view.person.avatar}
+                    />
+                    {this.userInfo()}
+                    <hr />
+                  </>
+                  {!this.state.loading && this.selects()}
+                  <PersonDetails
+                    personRes={res}
+                    admins={this.state.siteRes.admins}
+                    sort={this.state.sort}
+                    page={this.state.page}
+                    limit={fetchLimit}
+                    enableDownvotes={enableDownvotes(this.state.siteRes)}
+                    enableNsfw={enableNsfw(this.state.siteRes)}
+                    view={this.state.view}
+                    onPageChange={this.handlePageChange}
+                  />
+                </div>
 
-            {!this.state.loading && (
-              <div class="col-12 col-md-4">
-                {this.moderates()}
-                {this.isCurrentUser && this.follows()}
+                {!this.state.loading && (
+                  <div class="col-12 col-md-4">
+                    {this.moderates()}
+                    {this.amCurrentUser && this.follows()}
+                  </div>
+                )}
               </div>
-            )}
-          </div>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
@@ -352,286 +378,330 @@ export class Profile extends Component<any, ProfileState> {
   }
   handleBlockPerson(personId: number) {
     if (personId != 0) {
-      let blockUserForm: BlockPerson = {
+      let blockUserForm = new BlockPerson({
         person_id: personId,
         block: true,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
     }
   }
   handleUnblockPerson(recipientId: number) {
-    let blockUserForm: BlockPerson = {
+    let blockUserForm = new BlockPerson({
       person_id: recipientId,
       block: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
   }
 
   userInfo() {
-    let pv = this.state.personRes?.person_view;
-
-    return (
-      <div>
-        <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
-        <div class="mb-3">
-          <div class="">
-            <div class="mb-0 d-flex flex-wrap">
-              <div>
-                {pv.person.display_name && (
-                  <h5 class="mb-0">{pv.person.display_name}</h5>
-                )}
-                <ul class="list-inline mb-2">
-                  <li className="list-inline-item">
-                    <PersonListing
-                      person={pv.person}
-                      realLink
-                      useApubName
-                      muted
-                      hideAvatar
-                    />
-                  </li>
-                  {isBanned(pv.person) && (
-                    <li className="list-inline-item badge badge-danger">
-                      {i18n.t("banned")}
-                    </li>
-                  )}
-                  {pv.person.admin && (
+    return this.state.personRes
+      .map(r => r.person_view)
+      .match({
+        some: pv => (
+          <div>
+            <BannerIconHeader
+              banner={pv.person.banner}
+              icon={pv.person.avatar}
+            />
+            <div class="mb-3">
+              <div class="">
+                <div class="mb-0 d-flex flex-wrap">
+                  <div>
+                    {pv.person.display_name && (
+                      <h5 class="mb-0">{pv.person.display_name}</h5>
+                    )}
+                    <ul class="list-inline mb-2">
+                      <li className="list-inline-item">
+                        <PersonListing
+                          person={pv.person}
+                          realLink
+                          useApubName
+                          muted
+                          hideAvatar
+                        />
+                      </li>
+                      {isBanned(pv.person) && (
+                        <li className="list-inline-item badge badge-danger">
+                          {i18n.t("banned")}
+                        </li>
+                      )}
+                      {pv.person.admin && (
+                        <li className="list-inline-item badge badge-light">
+                          {i18n.t("admin")}
+                        </li>
+                      )}
+                      {pv.person.bot_account && (
+                        <li className="list-inline-item badge badge-light">
+                          {i18n.t("bot_account").toLowerCase()}
+                        </li>
+                      )}
+                    </ul>
+                  </div>
+                  {this.banDialog()}
+                  <div className="flex-grow-1 unselectable pointer mx-2"></div>
+                  {!this.amCurrentUser &&
+                    UserService.Instance.myUserInfo.isSome() && (
+                      <>
+                        <a
+                          className={`d-flex align-self-start btn btn-secondary mr-2 ${
+                            !pv.person.matrix_user_id && "invisible"
+                          }`}
+                          rel={relTags}
+                          href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
+                        >
+                          {i18n.t("send_secure_message")}
+                        </a>
+                        <Link
+                          className={
+                            "d-flex align-self-start btn btn-secondary mr-2"
+                          }
+                          to={`/create_private_message/recipient/${pv.person.id}`}
+                        >
+                          {i18n.t("send_message")}
+                        </Link>
+                        {this.state.personBlocked ? (
+                          <button
+                            className={
+                              "d-flex align-self-start btn btn-secondary mr-2"
+                            }
+                            onClick={linkEvent(
+                              pv.person.id,
+                              this.handleUnblockPerson
+                            )}
+                          >
+                            {i18n.t("unblock_user")}
+                          </button>
+                        ) : (
+                          <button
+                            className={
+                              "d-flex align-self-start btn btn-secondary mr-2"
+                            }
+                            onClick={linkEvent(
+                              pv.person.id,
+                              this.handleBlockPerson
+                            )}
+                          >
+                            {i18n.t("block_user")}
+                          </button>
+                        )}
+                      </>
+                    )}
+
+                  {canMod(
+                    None,
+                    Some(this.state.siteRes.admins),
+                    pv.person.id
+                  ) &&
+                    !isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
+                    !this.state.showBanDialog &&
+                    (!isBanned(pv.person) ? (
+                      <button
+                        className={
+                          "d-flex align-self-start btn btn-secondary mr-2"
+                        }
+                        onClick={linkEvent(this, this.handleModBanShow)}
+                        aria-label={i18n.t("ban")}
+                      >
+                        {capitalizeFirstLetter(i18n.t("ban"))}
+                      </button>
+                    ) : (
+                      <button
+                        className={
+                          "d-flex align-self-start btn btn-secondary mr-2"
+                        }
+                        onClick={linkEvent(this, this.handleModBanSubmit)}
+                        aria-label={i18n.t("unban")}
+                      >
+                        {capitalizeFirstLetter(i18n.t("unban"))}
+                      </button>
+                    ))}
+                </div>
+                {pv.person.bio.match({
+                  some: bio => (
+                    <div className="d-flex align-items-center mb-2">
+                      <div
+                        className="md-div"
+                        dangerouslySetInnerHTML={mdToHtml(bio)}
+                      />
+                    </div>
+                  ),
+                  none: <></>,
+                })}
+                <div>
+                  <ul class="list-inline mb-2">
                     <li className="list-inline-item badge badge-light">
-                      {i18n.t("admin")}
+                      {i18n.t("number_of_posts", {
+                        count: pv.counts.post_count,
+                        formattedCount: numToSI(pv.counts.post_count),
+                      })}
                     </li>
-                  )}
-                  {pv.person.bot_account && (
                     <li className="list-inline-item badge badge-light">
-                      {i18n.t("bot_account").toLowerCase()}
+                      {i18n.t("number_of_comments", {
+                        count: pv.counts.comment_count,
+                        formattedCount: numToSI(pv.counts.comment_count),
+                      })}
                     </li>
-                  )}
-                </ul>
-              </div>
-              {this.banDialog()}
-              <div className="flex-grow-1 unselectable pointer mx-2"></div>
-              {!this.isCurrentUser && UserService.Instance.myUserInfo && (
-                <>
-                  <a
-                    className={`d-flex align-self-start btn btn-secondary mr-2 ${
-                      !pv.person.matrix_user_id && "invisible"
-                    }`}
-                    rel={relTags}
-                    href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
-                  >
-                    {i18n.t("send_secure_message")}
-                  </a>
-                  <Link
-                    className={"d-flex align-self-start btn btn-secondary mr-2"}
-                    to={`/create_private_message/recipient/${pv.person.id}`}
-                  >
-                    {i18n.t("send_message")}
-                  </Link>
-                  {this.state.personBlocked ? (
-                    <button
-                      className={
-                        "d-flex align-self-start btn btn-secondary mr-2"
-                      }
-                      onClick={linkEvent(
-                        pv.person.id,
-                        this.handleUnblockPerson
-                      )}
-                    >
-                      {i18n.t("unblock_user")}
-                    </button>
-                  ) : (
-                    <button
-                      className={
-                        "d-flex align-self-start btn btn-secondary mr-2"
-                      }
-                      onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
-                    >
-                      {i18n.t("block_user")}
-                    </button>
-                  )}
-                </>
-              )}
-
-              {this.canAdmin &&
-                !this.personIsAdmin &&
-                !this.state.showBanDialog &&
-                (!isBanned(pv.person) ? (
-                  <button
-                    className={"d-flex align-self-start btn btn-secondary mr-2"}
-                    onClick={linkEvent(this, this.handleModBanShow)}
-                    aria-label={i18n.t("ban")}
-                  >
-                    {capitalizeFirstLetter(i18n.t("ban"))}
-                  </button>
-                ) : (
-                  <button
-                    className={"d-flex align-self-start btn btn-secondary mr-2"}
-                    onClick={linkEvent(this, this.handleModBanSubmit)}
-                    aria-label={i18n.t("unban")}
-                  >
-                    {capitalizeFirstLetter(i18n.t("unban"))}
-                  </button>
-                ))}
-            </div>
-            {pv.person.bio && (
-              <div className="d-flex align-items-center mb-2">
-                <div
-                  className="md-div"
-                  dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
-                />
+                  </ul>
+                </div>
+                <div class="text-muted">
+                  {i18n.t("joined")}{" "}
+                  <MomentTime
+                    published={pv.person.published}
+                    updated={None}
+                    showAgo
+                    ignoreUpdated
+                  />
+                </div>
+                <div className="d-flex align-items-center text-muted mb-2">
+                  <Icon icon="cake" />
+                  <span className="ml-2">
+                    {i18n.t("cake_day_title")}{" "}
+                    {moment
+                      .utc(pv.person.published)
+                      .local()
+                      .format("MMM DD, YYYY")}
+                  </span>
+                </div>
               </div>
-            )}
-            <div>
-              <ul class="list-inline mb-2">
-                <li className="list-inline-item badge badge-light">
-                  {i18n.t("number_of_posts", {
-                    count: pv.counts.post_count,
-                    formattedCount: numToSI(pv.counts.post_count),
-                  })}
-                </li>
-                <li className="list-inline-item badge badge-light">
-                  {i18n.t("number_of_comments", {
-                    count: pv.counts.comment_count,
-                    formattedCount: numToSI(pv.counts.comment_count),
-                  })}
-                </li>
-              </ul>
-            </div>
-            <div class="text-muted">
-              {i18n.t("joined")}{" "}
-              <MomentTime data={pv.person} showAgo ignoreUpdated />
-            </div>
-            <div className="d-flex align-items-center text-muted mb-2">
-              <Icon icon="cake" />
-              <span className="ml-2">
-                {i18n.t("cake_day_title")}{" "}
-                {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
-              </span>
             </div>
           </div>
-        </div>
-      </div>
-    );
+        ),
+        none: <></>,
+      });
   }
 
   banDialog() {
-    let pv = this.state.personRes?.person_view;
-    return (
-      <>
-        {this.state.showBanDialog && (
-          <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
-            <div class="form-group row col-12">
-              <label class="col-form-label" htmlFor="profile-ban-reason">
-                {i18n.t("reason")}
-              </label>
-              <input
-                type="text"
-                id="profile-ban-reason"
-                class="form-control mr-2"
-                placeholder={i18n.t("reason")}
-                value={this.state.banReason}
-                onInput={linkEvent(this, this.handleModBanReasonChange)}
-              />
-              <label class="col-form-label" htmlFor={`mod-ban-expires`}>
-                {i18n.t("expires")}
-              </label>
-              <input
-                type="number"
-                id={`mod-ban-expires`}
-                class="form-control mr-2"
-                placeholder={i18n.t("number_of_days")}
-                value={this.state.banExpireDays}
-                onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
-              />
-              <div class="form-group">
-                <div class="form-check">
+    return this.state.personRes
+      .map(r => r.person_view)
+      .match({
+        some: pv => (
+          <>
+            {this.state.showBanDialog && (
+              <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+                <div class="form-group row col-12">
+                  <label class="col-form-label" htmlFor="profile-ban-reason">
+                    {i18n.t("reason")}
+                  </label>
                   <input
-                    class="form-check-input"
-                    id="mod-ban-remove-data"
-                    type="checkbox"
-                    checked={this.state.removeData}
-                    onChange={linkEvent(this, this.handleModRemoveDataChange)}
+                    type="text"
+                    id="profile-ban-reason"
+                    class="form-control mr-2"
+                    placeholder={i18n.t("reason")}
+                    value={toUndefined(this.state.banReason)}
+                    onInput={linkEvent(this, this.handleModBanReasonChange)}
                   />
-                  <label
-                    class="form-check-label"
-                    htmlFor="mod-ban-remove-data"
-                    title={i18n.t("remove_content_more")}
-                  >
-                    {i18n.t("remove_content")}
+                  <label class="col-form-label" htmlFor={`mod-ban-expires`}>
+                    {i18n.t("expires")}
                   </label>
+                  <input
+                    type="number"
+                    id={`mod-ban-expires`}
+                    class="form-control mr-2"
+                    placeholder={i18n.t("number_of_days")}
+                    value={toUndefined(this.state.banExpireDays)}
+                    onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+                  />
+                  <div class="form-group">
+                    <div class="form-check">
+                      <input
+                        class="form-check-input"
+                        id="mod-ban-remove-data"
+                        type="checkbox"
+                        checked={this.state.removeData}
+                        onChange={linkEvent(
+                          this,
+                          this.handleModRemoveDataChange
+                        )}
+                      />
+                      <label
+                        class="form-check-label"
+                        htmlFor="mod-ban-remove-data"
+                        title={i18n.t("remove_content_more")}
+                      >
+                        {i18n.t("remove_content")}
+                      </label>
+                    </div>
+                  </div>
                 </div>
-              </div>
-            </div>
-            {/* TODO hold off on expires until later */}
-            {/* <div class="form-group row"> */}
-            {/*   <label class="col-form-label">Expires</label> */}
-            {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
-            {/* </div> */}
-            <div class="form-group row">
-              <button
-                type="cancel"
-                class="btn btn-secondary mr-2"
-                aria-label={i18n.t("cancel")}
-                onClick={linkEvent(this, this.handleModBanSubmitCancel)}
-              >
-                {i18n.t("cancel")}
-              </button>
-              <button
-                type="submit"
-                class="btn btn-secondary"
-                aria-label={i18n.t("ban")}
-              >
-                {i18n.t("ban")} {pv.person.name}
-              </button>
-            </div>
-          </form>
-        )}
-      </>
-    );
+                {/* TODO hold off on expires until later */}
+                {/* <div class="form-group row"> */}
+                {/*   <label class="col-form-label">Expires</label> */}
+                {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+                {/* </div> */}
+                <div class="form-group row">
+                  <button
+                    type="cancel"
+                    class="btn btn-secondary mr-2"
+                    aria-label={i18n.t("cancel")}
+                    onClick={linkEvent(this, this.handleModBanSubmitCancel)}
+                  >
+                    {i18n.t("cancel")}
+                  </button>
+                  <button
+                    type="submit"
+                    class="btn btn-secondary"
+                    aria-label={i18n.t("ban")}
+                  >
+                    {i18n.t("ban")} {pv.person.name}
+                  </button>
+                </div>
+              </form>
+            )}
+          </>
+        ),
+        none: <></>,
+      });
   }
 
+  // TODO test this, make sure its good
   moderates() {
-    return (
-      <div>
-        {this.state.personRes.moderates.length > 0 && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <h5>{i18n.t("moderates")}</h5>
-              <ul class="list-unstyled mb-0">
-                {this.state.personRes.moderates.map(cmv => (
-                  <li>
-                    <CommunityLink community={cmv.community} />
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        )}
-      </div>
-    );
+    return this.state.personRes
+      .map(r => r.moderates)
+      .match({
+        some: moderates => {
+          if (moderates.length > 0) {
+            <div class="card border-secondary mb-3">
+              <div class="card-body">
+                <h5>{i18n.t("moderates")}</h5>
+                <ul class="list-unstyled mb-0">
+                  {moderates.map(cmv => (
+                    <li>
+                      <CommunityLink community={cmv.community} />
+                    </li>
+                  ))}
+                </ul>
+              </div>
+            </div>;
+          }
+        },
+        none: void 0,
+      });
   }
 
   follows() {
-    let follows = UserService.Instance.myUserInfo.follows;
-    return (
-      <div>
-        {follows.length > 0 && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <h5>{i18n.t("subscribed")}</h5>
-              <ul class="list-unstyled mb-0">
-                {follows.map(cfv => (
-                  <li>
-                    <CommunityLink community={cfv.community} />
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        )}
-      </div>
-    );
+    return UserService.Instance.myUserInfo
+      .map(m => m.follows)
+      .match({
+        some: follows => {
+          if (follows.length > 0) {
+            <div class="card border-secondary mb-3">
+              <div class="card-body">
+                <h5>{i18n.t("subscribed")}</h5>
+                <ul class="list-unstyled mb-0">
+                  {follows.map(cfv => (
+                    <li>
+                      <CommunityLink community={cfv.community} />
+                    </li>
+                  ))}
+                </ul>
+              </div>
+            </div>;
+          }
+        },
+        none: void 0,
+      });
   }
 
   updateUrl(paramUpdates: UrlParams) {
@@ -649,29 +719,8 @@ export class Profile extends Component<any, ProfileState> {
     this.fetchUserData();
   }
 
-  get canAdmin(): boolean {
-    return (
-      this.state.siteRes?.admins &&
-      canMod(
-        UserService.Instance.myUserInfo,
-        this.state.siteRes.admins.map(a => a.person.id),
-        this.state.personRes?.person_view.person.id
-      )
-    );
-  }
-
-  get personIsAdmin(): boolean {
-    return (
-      this.state.siteRes?.admins &&
-      isMod(
-        this.state.siteRes.admins.map(a => a.person.id),
-        this.state.personRes?.person_view.person.id
-      )
-    );
-  }
-
   handlePageChange(page: number) {
-    this.updateUrl({ page });
+    this.updateUrl({ page: page });
   }
 
   handleSortChange(val: SortType) {
@@ -714,24 +763,30 @@ export class Profile extends Component<any, ProfileState> {
   handleModBanSubmit(i: Profile, event?: any) {
     if (event) event.preventDefault();
 
-    let pv = i.state.personRes.person_view;
-    // If its an unban, restore all their data
-    let ban = !pv.person.banned;
-    if (ban == false) {
-      i.state.removeData = false;
-    }
-    let form: BanPerson = {
-      person_id: pv.person.id,
-      ban,
-      remove_data: i.state.removeData,
-      reason: i.state.banReason,
-      expires: futureDaysToUnixTime(i.state.banExpireDays),
-      auth: authField(),
-    };
-    WebSocketService.Instance.send(wsClient.banPerson(form));
-
-    i.state.showBanDialog = false;
-    i.setState(i.state);
+    i.state.personRes
+      .map(r => r.person_view.person)
+      .match({
+        some: person => {
+          // If its an unban, restore all their data
+          let ban = !person.banned;
+          if (ban == false) {
+            i.state.removeData = false;
+          }
+          let form = new BanPerson({
+            person_id: person.id,
+            ban,
+            remove_data: Some(i.state.removeData),
+            reason: i.state.banReason,
+            expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+            auth: auth().unwrap(),
+          });
+          WebSocketService.Instance.send(wsClient.banPerson(form));
+
+          i.state.showBanDialog = false;
+          i.setState(i.state);
+        },
+        none: void 0,
+      });
   }
 
   parseMessage(msg: any) {
@@ -749,41 +804,53 @@ 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
       // and set the parent state if it is not set or differs
       // TODO this might need to get abstracted
-      let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
-      this.state.personRes = data;
-      console.log(data);
+      let data = wsJsonToRes<GetPersonDetailsResponse>(
+        msg,
+        GetPersonDetailsResponse
+      );
+      this.state.personRes = Some(data);
       this.state.loading = false;
       this.setPersonBlock();
       this.setState(this.state);
       restoreScrollPosition(this.context);
     } else if (op == UserOperation.AddAdmin) {
-      let data = wsJsonToRes<AddAdminResponse>(msg).data;
+      let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
       this.state.siteRes.admins = data.admins;
       this.setState(this.state);
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      createCommentLikeRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      createCommentLikeRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (
       op == UserOperation.EditComment ||
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      editCommentRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      editCommentRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      if (
-        UserService.Instance.myUserInfo &&
-        data.comment_view.creator.id ==
-          UserService.Instance.myUserInfo?.local_user_view.person.id
-      ) {
-        toast(i18n.t("reply_sent"));
-      }
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.comment_view.creator.id == mui.local_user_view.person.id) {
+            toast(i18n.t("reply_sent"));
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      saveCommentRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      saveCommentRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (
       op == UserOperation.EditPost ||
@@ -793,29 +860,40 @@ export class Profile extends Component<any, ProfileState> {
       op == UserOperation.StickyPost ||
       op == UserOperation.SavePost
     ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      editPostFindRes(data.post_view, this.state.personRes.posts);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      editPostFindRes(
+        data.post_view,
+        this.state.personRes.map(r => r.posts).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      createPostLikeFindRes(data.post_view, this.state.personRes.posts);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      createPostLikeFindRes(
+        data.post_view,
+        this.state.personRes.map(r => r.posts).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.BanPerson) {
-      let data = wsJsonToRes<BanPersonResponse>(msg).data;
-      this.state.personRes.comments
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator.banned = data.banned));
-      this.state.personRes.posts
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator.banned = data.banned));
-      let pv = this.state.personRes.person_view;
-
-      if (pv.person.id == data.person_view.person.id) {
-        pv.person.banned = data.banned;
-      }
-      this.setState(this.state);
+      let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
+      this.state.personRes.match({
+        some: res => {
+          res.comments
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator.banned = data.banned));
+          res.posts
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator.banned = data.banned));
+          let pv = res.person_view;
+
+          if (pv.person.id == data.person_view.person.id) {
+            pv.person.banned = data.banned;
+          }
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
       this.setPersonBlock();
       this.setState(this.state);
index 9009f746271098a87d6ec4ed98cb356649482388..eec90319ad8cfb1f7b3cf4e2bf47b1b7e4209678 100644 (file)
@@ -1,18 +1,20 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
+  GetSiteResponse,
   ListRegistrationApplications,
   ListRegistrationApplicationsResponse,
   RegistrationApplicationResponse,
-  RegistrationApplicationView,
-  SiteView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   fetchLimit,
   isBrowser,
   setIsoData,
@@ -20,9 +22,7 @@ import {
   toast,
   updateRegistrationApplicationRes,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
@@ -35,10 +35,10 @@ enum UnreadOrAll {
 }
 
 interface RegistrationApplicationsState {
-  applications: RegistrationApplicationView[];
-  page: number;
-  site_view: SiteView;
+  listRegistrationApplicationsResponse: Option<ListRegistrationApplicationsResponse>;
+  siteRes: GetSiteResponse;
   unreadOrAll: UnreadOrAll;
+  page: number;
   loading: boolean;
 }
 
@@ -46,13 +46,16 @@ export class RegistrationApplications extends Component<
   any,
   RegistrationApplicationsState
 > {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    ListRegistrationApplicationsResponse
+  );
   private subscription: Subscription;
   private emptyState: RegistrationApplicationsState = {
+    listRegistrationApplicationsResponse: None,
+    siteRes: this.isoData.site_res,
     unreadOrAll: UnreadOrAll.Unread,
-    applications: [],
     page: 1,
-    site_view: this.isoData.site_res.site_view,
     loading: true,
   };
 
@@ -62,7 +65,7 @@ export class RegistrationApplications extends Component<
     this.state = this.emptyState;
     this.handlePageChange = this.handlePageChange.bind(this);
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -72,8 +75,9 @@ export class RegistrationApplications extends Component<
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.applications =
-        this.isoData.routeData[0].registration_applications || []; // TODO test
+      this.state.listRegistrationApplicationsResponse = Some(
+        this.isoData.routeData[0] as ListRegistrationApplicationsResponse
+      );
       this.state.loading = false;
     } else {
       this.refetch();
@@ -91,11 +95,17 @@ export class RegistrationApplications extends Component<
   }
 
   get documentTitle(): string {
-    return `@${
-      UserService.Instance.myUserInfo.local_user_view.person.name
-    } ${i18n.t("registration_applications")} - ${
-      this.state.site_view.site.name
-    }`;
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        UserService.Instance.myUserInfo.match({
+          some: mui =>
+            `@${mui.local_user_view.person.name} ${i18n.t(
+              "registration_applications"
+            )} - ${siteView.site.name}`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
   render() {
@@ -111,6 +121,8 @@ export class RegistrationApplications extends Component<
               <HtmlTags
                 title={this.documentTitle}
                 path={this.context.router.route.match.url}
+                description={None}
+                image={None}
               />
               <h5 class="mb-2">{i18n.t("registration_applications")}</h5>
               {this.selects()}
@@ -168,19 +180,22 @@ export class RegistrationApplications extends Component<
   }
 
   applicationList() {
-    return (
-      <div>
-        {this.state.applications.map(ra => (
-          <>
-            <hr />
-            <RegistrationApplication
-              key={ra.registration_application.id}
-              application={ra}
-            />
-          </>
-        ))}
-      </div>
-    );
+    return this.state.listRegistrationApplicationsResponse.match({
+      some: res => (
+        <div>
+          {res.registration_applications.map(ra => (
+            <>
+              <hr />
+              <RegistrationApplication
+                key={ra.registration_application.id}
+                application={ra}
+              />
+            </>
+          ))}
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
@@ -198,12 +213,12 @@ export class RegistrationApplications extends Component<
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let promises: Promise<any>[] = [];
 
-    let form: ListRegistrationApplications = {
-      unread_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+    let form = new ListRegistrationApplications({
+      unread_only: Some(true),
+      page: Some(1),
+      limit: Some(fetchLimit),
+      auth: req.auth.unwrap(),
+    });
     promises.push(req.client.listRegistrationApplications(form));
 
     return promises;
@@ -211,12 +226,12 @@ export class RegistrationApplications extends Component<
 
   refetch() {
     let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
-    let form: ListRegistrationApplications = {
-      unread_only: unread_only,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+    let form = new ListRegistrationApplications({
+      unread_only: Some(unread_only),
+      page: Some(this.state.page),
+      limit: Some(fetchLimit),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.listRegistrationApplications(form));
   }
 
@@ -229,16 +244,24 @@ export class RegistrationApplications extends Component<
     } else if (msg.reconnect) {
       this.refetch();
     } else if (op == UserOperation.ListRegistrationApplications) {
-      let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg).data;
-      this.state.applications = data.registration_applications;
+      let data = wsJsonToRes<ListRegistrationApplicationsResponse>(
+        msg,
+        ListRegistrationApplicationsResponse
+      );
+      this.state.listRegistrationApplicationsResponse = Some(data);
       this.state.loading = false;
       window.scrollTo(0, 0);
       this.setState(this.state);
     } else if (op == UserOperation.ApproveRegistrationApplication) {
-      let data = wsJsonToRes<RegistrationApplicationResponse>(msg).data;
+      let data = wsJsonToRes<RegistrationApplicationResponse>(
+        msg,
+        RegistrationApplicationResponse
+      );
       updateRegistrationApplicationRes(
         data.registration_application,
-        this.state.applications
+        this.state.listRegistrationApplicationsResponse
+          .map(r => r.registration_applications)
+          .unwrapOr([])
       );
       let uacs = UserService.Instance.unreadApplicationCountSub;
       // Minor bug, where if the application switches from deny to approve, the count will still go down
index 99edf968130c41f42b54fa528e12d30af969c71c..f8a641b556032f24166efcda16f337448220b9bd 100644 (file)
@@ -1,22 +1,25 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   CommentReportResponse,
   CommentReportView,
+  GetSiteResponse,
   ListCommentReports,
   ListCommentReportsResponse,
   ListPostReports,
   ListPostReportsResponse,
   PostReportResponse,
   PostReportView,
-  SiteView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   fetchLimit,
   isBrowser,
   setIsoData,
@@ -25,9 +28,7 @@ import {
   updateCommentReportRes,
   updatePostReportRes,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentReport } from "../comment/comment-report";
 import { HtmlTags } from "../common/html-tags";
@@ -59,27 +60,31 @@ type ItemType = {
 };
 
 interface ReportsState {
+  listCommentReportsResponse: Option<ListCommentReportsResponse>;
+  listPostReportsResponse: Option<ListPostReportsResponse>;
   unreadOrAll: UnreadOrAll;
   messageType: MessageType;
-  commentReports: CommentReportView[];
-  postReports: PostReportView[];
   combined: ItemType[];
+  siteRes: GetSiteResponse;
   page: number;
-  site_view: SiteView;
   loading: boolean;
 }
 
 export class Reports extends Component<any, ReportsState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    ListCommentReportsResponse,
+    ListPostReportsResponse
+  );
   private subscription: Subscription;
   private emptyState: ReportsState = {
+    listCommentReportsResponse: None,
+    listPostReportsResponse: None,
     unreadOrAll: UnreadOrAll.Unread,
     messageType: MessageType.All,
-    commentReports: [],
-    postReports: [],
     combined: [],
     page: 1,
-    site_view: this.isoData.site_res.site_view,
+    siteRes: this.isoData.site_res,
     loading: true,
   };
 
@@ -89,7 +94,7 @@ export class Reports extends Component<any, ReportsState> {
     this.state = this.emptyState;
     this.handlePageChange = this.handlePageChange.bind(this);
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -99,9 +104,12 @@ export class Reports extends Component<any, ReportsState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.commentReports =
-        this.isoData.routeData[0].comment_reports || [];
-      this.state.postReports = this.isoData.routeData[1].post_reports || [];
+      this.state.listCommentReportsResponse = Some(
+        this.isoData.routeData[0] as ListCommentReportsResponse
+      );
+      this.state.listPostReportsResponse = Some(
+        this.isoData.routeData[1] as ListPostReportsResponse
+      );
       this.state.combined = this.buildCombined();
       this.state.loading = false;
     } else {
@@ -116,9 +124,17 @@ export class Reports extends Component<any, ReportsState> {
   }
 
   get documentTitle(): string {
-    return `@${
-      UserService.Instance.myUserInfo.local_user_view.person.name
-    } ${i18n.t("reports")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        UserService.Instance.myUserInfo.match({
+          some: mui =>
+            `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
+              siteView.site.name
+            }`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
   render() {
@@ -134,6 +150,8 @@ export class Reports extends Component<any, ReportsState> {
               <HtmlTags
                 title={this.documentTitle}
                 path={this.context.router.route.match.url}
+                description={None}
+                image={None}
               />
               <h5 class="mb-2">{i18n.t("reports")}</h5>
               {this.selects()}
@@ -260,12 +278,14 @@ export class Reports extends Component<any, ReportsState> {
   }
 
   buildCombined(): ItemType[] {
-    let comments: ItemType[] = this.state.commentReports.map(r =>
-      this.replyToReplyType(r)
-    );
-    let posts: ItemType[] = this.state.postReports.map(r =>
-      this.mentionToReplyType(r)
-    );
+    let comments: ItemType[] = this.state.listCommentReportsResponse
+      .map(r => r.comment_reports)
+      .unwrapOr([])
+      .map(r => this.replyToReplyType(r));
+    let posts: ItemType[] = this.state.listPostReportsResponse
+      .map(r => r.post_reports)
+      .unwrapOr([])
+      .map(r => this.mentionToReplyType(r));
 
     return [...comments, ...posts].sort((a, b) =>
       b.published.localeCompare(a.published)
@@ -299,29 +319,35 @@ export class Reports extends Component<any, ReportsState> {
   }
 
   commentReports() {
-    return (
-      <div>
-        {this.state.commentReports.map(cr => (
-          <>
-            <hr />
-            <CommentReport key={cr.comment_report.id} report={cr} />
-          </>
-        ))}
-      </div>
-    );
+    return this.state.listCommentReportsResponse.match({
+      some: res => (
+        <div>
+          {res.comment_reports.map(cr => (
+            <>
+              <hr />
+              <CommentReport key={cr.comment_report.id} report={cr} />
+            </>
+          ))}
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   postReports() {
-    return (
-      <div>
-        {this.state.postReports.map(pr => (
-          <>
-            <hr />
-            <PostReport key={pr.post_report.id} report={pr} />
-          </>
-        ))}
-      </div>
-    );
+    return this.state.listPostReportsResponse.match({
+      some: res => (
+        <div>
+          {res.post_reports.map(pr => (
+            <>
+              <hr />
+              <PostReport key={pr.post_report.id} report={pr} />
+            </>
+          ))}
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   handlePageChange(page: number) {
@@ -346,47 +372,61 @@ export class Reports extends Component<any, ReportsState> {
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let promises: Promise<any>[] = [];
 
-    let commentReportsForm: ListCommentReports = {
+    let unresolved_only = Some(true);
+    let page = Some(1);
+    let limit = Some(fetchLimit);
+    let community_id = None;
+    let auth = req.auth.unwrap();
+
+    let commentReportsForm = new ListCommentReports({
       // TODO community_id
-      unresolved_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+      unresolved_only,
+      community_id,
+      page,
+      limit,
+      auth,
+    });
     promises.push(req.client.listCommentReports(commentReportsForm));
 
-    let postReportsForm: ListPostReports = {
+    let postReportsForm = new ListPostReports({
       // TODO community_id
-      unresolved_only: true,
-      page: 1,
-      limit: fetchLimit,
-      auth: req.auth,
-    };
+      unresolved_only,
+      community_id,
+      page,
+      limit,
+      auth,
+    });
     promises.push(req.client.listPostReports(postReportsForm));
 
     return promises;
   }
 
   refetch() {
-    let unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
-    let commentReportsForm: ListCommentReports = {
-      // TODO community_id
+    let unresolved_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
+    let community_id = None;
+    let page = Some(this.state.page);
+    let limit = Some(fetchLimit);
+
+    let commentReportsForm = new ListCommentReports({
       unresolved_only,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+      // TODO community_id
+      community_id,
+      page,
+      limit,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(
       wsClient.listCommentReports(commentReportsForm)
     );
 
-    let postReportsForm: ListPostReports = {
-      // TODO community_id
+    let postReportsForm = new ListPostReports({
       unresolved_only,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(),
-    };
+      // TODO community_id
+      community_id,
+      page,
+      limit,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
   }
 
@@ -399,8 +439,11 @@ export class Reports extends Component<any, ReportsState> {
     } else if (msg.reconnect) {
       this.refetch();
     } else if (op == UserOperation.ListCommentReports) {
-      let data = wsJsonToRes<ListCommentReportsResponse>(msg).data;
-      this.state.commentReports = data.comment_reports;
+      let data = wsJsonToRes<ListCommentReportsResponse>(
+        msg,
+        ListCommentReportsResponse
+      );
+      this.state.listCommentReportsResponse = Some(data);
       this.state.combined = this.buildCombined();
       this.state.loading = false;
       // this.sendUnreadCount();
@@ -408,8 +451,11 @@ export class Reports extends Component<any, ReportsState> {
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.ListPostReports) {
-      let data = wsJsonToRes<ListPostReportsResponse>(msg).data;
-      this.state.postReports = data.post_reports;
+      let data = wsJsonToRes<ListPostReportsResponse>(
+        msg,
+        ListPostReportsResponse
+      );
+      this.state.listPostReportsResponse = Some(data);
       this.state.combined = this.buildCombined();
       this.state.loading = false;
       // this.sendUnreadCount();
@@ -417,8 +463,11 @@ export class Reports extends Component<any, ReportsState> {
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.ResolvePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
-      updatePostReportRes(data.post_report_view, this.state.postReports);
+      let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
+      updatePostReportRes(
+        data.post_report_view,
+        this.state.listPostReportsResponse.map(r => r.post_reports).unwrapOr([])
+      );
       let urcs = UserService.Instance.unreadReportCountSub;
       if (data.post_report_view.post_report.resolved) {
         urcs.next(urcs.getValue() - 1);
@@ -427,10 +476,12 @@ export class Reports extends Component<any, ReportsState> {
       }
       this.setState(this.state);
     } else if (op == UserOperation.ResolveCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
+      let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
       updateCommentReportRes(
         data.comment_report_view,
-        this.state.commentReports
+        this.state.listCommentReportsResponse
+          .map(r => r.comment_reports)
+          .unwrapOr([])
       );
       let urcs = UserService.Instance.unreadReportCountSub;
       if (data.comment_report_view.comment_report.resolved) {
index 0884085aa079935b7f3e450a8cfafedc6fda4cdc..532d92204ab9ae624a24e586b7d1a35a5cff1bc2 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   BlockCommunity,
@@ -15,19 +16,23 @@ import {
   PersonViewSafe,
   SaveUserSettings,
   SortType,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n, languages } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   choicesConfig,
   communitySelectName,
   communityToChoice,
   debounce,
   elementUrl,
+  enableNsfw,
   fetchCommunities,
   fetchThemeList,
   fetchUsers,
@@ -44,9 +49,7 @@ import {
   updateCommunityBlock,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
@@ -65,20 +68,19 @@ if (isBrowser()) {
 interface SettingsState {
   saveUserSettingsForm: SaveUserSettings;
   changePasswordForm: ChangePassword;
-  saveUserSettingsLoading: boolean;
-  changePasswordLoading: boolean;
-  deleteAccountLoading: boolean;
-  deleteAccountShowConfirm: boolean;
   deleteAccountForm: DeleteAccount;
   personBlocks: PersonBlockView[];
-  blockPersonId: number;
-  blockPerson?: PersonViewSafe;
+  blockPerson: Option<PersonViewSafe>;
   communityBlocks: CommunityBlockView[];
   blockCommunityId: number;
   blockCommunity?: CommunityView;
   currentTab: string;
-  siteRes: GetSiteResponse;
   themeList: string[];
+  saveUserSettingsLoading: boolean;
+  changePasswordLoading: boolean;
+  deleteAccountLoading: boolean;
+  deleteAccountShowConfirm: boolean;
+  siteRes: GetSiteResponse;
 }
 
 export class Settings extends Component<any, SettingsState> {
@@ -87,25 +89,43 @@ export class Settings extends Component<any, SettingsState> {
   private blockCommunityChoices: any;
   private subscription: Subscription;
   private emptyState: SettingsState = {
-    saveUserSettingsForm: {
-      auth: authField(false),
-    },
-    changePasswordForm: {
-      new_password: null,
-      new_password_verify: null,
-      old_password: null,
-      auth: authField(false),
-    },
-    saveUserSettingsLoading: null,
+    saveUserSettingsForm: new SaveUserSettings({
+      show_nsfw: None,
+      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,
+      lang: 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,
     changePasswordLoading: false,
-    deleteAccountLoading: null,
+    deleteAccountLoading: false,
     deleteAccountShowConfirm: false,
-    deleteAccountForm: {
-      password: null,
-      auth: authField(false),
-    },
+    deleteAccountForm: new DeleteAccount({
+      password: undefined,
+      auth: undefined,
+    }),
     personBlocks: [],
-    blockPersonId: 0,
+    blockPerson: None,
     communityBlocks: [],
     blockCommunityId: 0,
     currentTab: "settings",
@@ -154,7 +174,7 @@ export class Settings extends Component<any, SettingsState> {
           <HtmlTags
             title={this.documentTitle}
             path={this.context.router.route.match.url}
-            description={this.documentTitle}
+            description={Some(this.documentTitle)}
             image={this.state.saveUserSettingsForm.avatar}
           />
           <ul class="nav nav-tabs mb-2">
@@ -342,14 +362,17 @@ export class Settings extends Component<any, SettingsState> {
           <select
             class="form-control"
             id="block-person-filter"
-            value={this.state.blockPersonId}
+            value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)}
           >
             <option value="0">—</option>
-            {this.state.blockPerson && (
-              <option value={this.state.blockPerson.person.id}>
-                {personSelectName(this.state.blockPerson)}
-              </option>
-            )}
+            {this.state.blockPerson.match({
+              some: personView => (
+                <option value={personView.person.id}>
+                  {personSelectName(personView)}
+                </option>
+              ),
+              none: <></>,
+            })}
           </select>
         </div>
       </div>
@@ -431,7 +454,9 @@ export class Settings extends Component<any, SettingsState> {
                 type="text"
                 class="form-control"
                 placeholder={i18n.t("optional")}
-                value={this.state.saveUserSettingsForm.display_name}
+                value={toUndefined(
+                  this.state.saveUserSettingsForm.display_name
+                )}
                 onInput={linkEvent(this, this.handleDisplayNameChange)}
                 pattern="^(?!@)(.+)$"
                 minLength={3}
@@ -446,7 +471,9 @@ export class Settings extends Component<any, SettingsState> {
               <MarkdownTextArea
                 initialContent={this.state.saveUserSettingsForm.bio}
                 onContentChange={this.handleBioChange}
-                maxLength={300}
+                maxLength={Some(300)}
+                placeholder={None}
+                buttonTitle={None}
                 hideNavigationWarnings
               />
             </div>
@@ -461,7 +488,7 @@ export class Settings extends Component<any, SettingsState> {
                 id="user-email"
                 class="form-control"
                 placeholder={i18n.t("optional")}
-                value={this.state.saveUserSettingsForm.email}
+                value={toUndefined(this.state.saveUserSettingsForm.email)}
                 onInput={linkEvent(this, this.handleEmailChange)}
                 minLength={3}
               />
@@ -479,7 +506,9 @@ export class Settings extends Component<any, SettingsState> {
                 type="text"
                 class="form-control"
                 placeholder="@user:example.com"
-                value={this.state.saveUserSettingsForm.matrix_user_id}
+                value={toUndefined(
+                  this.state.saveUserSettingsForm.matrix_user_id
+                )}
                 onInput={linkEvent(this, this.handleMatrixUserIdChange)}
                 pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
               />
@@ -515,7 +544,7 @@ export class Settings extends Component<any, SettingsState> {
             <div class="col-sm-9">
               <select
                 id="user-language"
-                value={this.state.saveUserSettingsForm.lang}
+                value={toUndefined(this.state.saveUserSettingsForm.lang)}
                 onChange={linkEvent(this, this.handleLangChange)}
                 class="custom-select w-auto"
               >
@@ -541,7 +570,7 @@ export class Settings extends Component<any, SettingsState> {
             <div class="col-sm-9">
               <select
                 id="user-theme"
-                value={this.state.saveUserSettingsForm.theme}
+                value={toUndefined(this.state.saveUserSettingsForm.theme)}
                 onChange={linkEvent(this, this.handleThemeChange)}
                 class="custom-select w-auto"
               >
@@ -561,7 +590,9 @@ export class Settings extends Component<any, SettingsState> {
               <ListingTypeSelect
                 type_={
                   Object.values(ListingType)[
-                    this.state.saveUserSettingsForm.default_listing_type
+                    this.state.saveUserSettingsForm.default_listing_type.unwrapOr(
+                      1
+                    )
                   ]
                 }
                 showLocal={showLocal(this.isoData)}
@@ -576,21 +607,25 @@ export class Settings extends Component<any, SettingsState> {
               <SortSelect
                 sort={
                   Object.values(SortType)[
-                    this.state.saveUserSettingsForm.default_sort_type
+                    this.state.saveUserSettingsForm.default_sort_type.unwrapOr(
+                      0
+                    )
                   ]
                 }
                 onChange={this.handleSortTypeChange}
               />
             </div>
           </form>
-          {this.state.siteRes.site_view.site.enable_nsfw && (
+          {enableNsfw(this.state.siteRes) && (
             <div class="form-group">
               <div class="form-check">
                 <input
                   class="form-check-input"
                   id="user-show-nsfw"
                   type="checkbox"
-                  checked={this.state.saveUserSettingsForm.show_nsfw}
+                  checked={toUndefined(
+                    this.state.saveUserSettingsForm.show_nsfw
+                  )}
                   onChange={linkEvent(this, this.handleShowNsfwChange)}
                 />
                 <label class="form-check-label" htmlFor="user-show-nsfw">
@@ -605,7 +640,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-show-scores"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_scores}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.show_scores
+                )}
                 onChange={linkEvent(this, this.handleShowScoresChange)}
               />
               <label class="form-check-label" htmlFor="user-show-scores">
@@ -619,7 +656,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-show-avatars"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_avatars}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.show_avatars
+                )}
                 onChange={linkEvent(this, this.handleShowAvatarsChange)}
               />
               <label class="form-check-label" htmlFor="user-show-avatars">
@@ -633,7 +672,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-bot-account"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.bot_account}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.bot_account
+                )}
                 onChange={linkEvent(this, this.handleBotAccount)}
               />
               <label class="form-check-label" htmlFor="user-bot-account">
@@ -647,7 +688,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-show-bot-accounts"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_bot_accounts}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.show_bot_accounts
+                )}
                 onChange={linkEvent(this, this.handleShowBotAccounts)}
               />
               <label class="form-check-label" htmlFor="user-show-bot-accounts">
@@ -661,7 +704,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-show-read-posts"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_read_posts}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.show_read_posts
+                )}
                 onChange={linkEvent(this, this.handleReadPosts)}
               />
               <label class="form-check-label" htmlFor="user-show-read-posts">
@@ -675,7 +720,9 @@ export class Settings extends Component<any, SettingsState> {
                 class="form-check-input"
                 id="user-show-new-post-notifs"
                 type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_new_post_notifs}
+                checked={toUndefined(
+                  this.state.saveUserSettingsForm.show_new_post_notifs
+                )}
                 onChange={linkEvent(this, this.handleShowNewPostNotifs)}
               />
               <label
@@ -693,9 +740,9 @@ export class Settings extends Component<any, SettingsState> {
                 id="user-send-notifications-to-email"
                 type="checkbox"
                 disabled={!this.state.saveUserSettingsForm.email}
-                checked={
+                checked={toUndefined(
                   this.state.saveUserSettingsForm.send_notifications_to_email
-                }
+                )}
                 onChange={linkEvent(
                   this,
                   this.handleSendNotificationsToEmailChange
@@ -844,31 +891,31 @@ export class Settings extends Component<any, SettingsState> {
 
   handleBlockPerson(personId: number) {
     if (personId != 0) {
-      let blockUserForm: BlockPerson = {
+      let blockUserForm = new BlockPerson({
         person_id: personId,
         block: true,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
     }
   }
 
   handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
-    let blockUserForm: BlockPerson = {
+    let blockUserForm = new BlockPerson({
       person_id: i.recipientId,
       block: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
   }
 
   handleBlockCommunity(community_id: number) {
     if (community_id != 0) {
-      let blockCommunityForm: BlockCommunity = {
+      let blockCommunityForm = new BlockCommunity({
         community_id,
         block: true,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(
         wsClient.blockCommunity(blockCommunityForm)
       );
@@ -876,126 +923,133 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
-    let blockCommunityForm: BlockCommunity = {
+    let blockCommunityForm = new BlockCommunity({
       community_id: i.communityId,
       block: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
   }
 
   handleShowNsfwChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
+    i.state.saveUserSettingsForm.show_nsfw = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleShowAvatarsChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_avatars = event.target.checked;
-    UserService.Instance.myUserInfo.local_user_view.local_user.show_avatars =
-      event.target.checked; // Just for instant updates
+    i.state.saveUserSettingsForm.show_avatars = Some(event.target.checked);
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        (mui.local_user_view.local_user.show_avatars = event.target.checked),
+      none: void 0,
+    });
     i.setState(i.state);
   }
 
   handleBotAccount(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.bot_account = event.target.checked;
+    i.state.saveUserSettingsForm.bot_account = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleShowBotAccounts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
+    i.state.saveUserSettingsForm.show_bot_accounts = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleReadPosts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
+    i.state.saveUserSettingsForm.show_read_posts = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleShowNewPostNotifs(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
+    i.state.saveUserSettingsForm.show_new_post_notifs = Some(
+      event.target.checked
+    );
     i.setState(i.state);
   }
 
   handleShowScoresChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_scores = event.target.checked;
-    UserService.Instance.myUserInfo.local_user_view.local_user.show_scores =
-      event.target.checked; // Just for instant updates
+    i.state.saveUserSettingsForm.show_scores = Some(event.target.checked);
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        (mui.local_user_view.local_user.show_scores = event.target.checked),
+      none: void 0,
+    });
     i.setState(i.state);
   }
 
   handleSendNotificationsToEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.send_notifications_to_email =
-      event.target.checked;
+    i.state.saveUserSettingsForm.send_notifications_to_email = Some(
+      event.target.checked
+    );
     i.setState(i.state);
   }
 
   handleThemeChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.theme = event.target.value;
+    i.state.saveUserSettingsForm.theme = Some(event.target.value);
     setTheme(event.target.value, true);
     i.setState(i.state);
   }
 
   handleLangChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.lang = event.target.value;
-    i18n.changeLanguage(getLanguages(i.state.saveUserSettingsForm.lang)[0]);
+    i.state.saveUserSettingsForm.lang = Some(event.target.value);
+    i18n.changeLanguage(
+      getLanguages(i.state.saveUserSettingsForm.lang.unwrap())[0]
+    );
     i.setState(i.state);
   }
 
   handleSortTypeChange(val: SortType) {
-    this.state.saveUserSettingsForm.default_sort_type =
-      Object.keys(SortType).indexOf(val);
+    this.state.saveUserSettingsForm.default_sort_type = Some(
+      Object.keys(SortType).indexOf(val)
+    );
     this.setState(this.state);
   }
 
   handleListingTypeChange(val: ListingType) {
-    this.state.saveUserSettingsForm.default_listing_type =
-      Object.keys(ListingType).indexOf(val);
+    this.state.saveUserSettingsForm.default_listing_type = Some(
+      Object.keys(ListingType).indexOf(val)
+    );
     this.setState(this.state);
   }
 
   handleEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.email = event.target.value;
+    i.state.saveUserSettingsForm.email = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleBioChange(val: string) {
-    this.state.saveUserSettingsForm.bio = val;
+    this.state.saveUserSettingsForm.bio = Some(val);
     this.setState(this.state);
   }
 
   handleAvatarUpload(url: string) {
-    this.state.saveUserSettingsForm.avatar = url;
+    this.state.saveUserSettingsForm.avatar = Some(url);
     this.setState(this.state);
   }
 
   handleAvatarRemove() {
-    this.state.saveUserSettingsForm.avatar = "";
+    this.state.saveUserSettingsForm.avatar = Some("");
     this.setState(this.state);
   }
 
   handleBannerUpload(url: string) {
-    this.state.saveUserSettingsForm.banner = url;
+    this.state.saveUserSettingsForm.banner = Some(url);
     this.setState(this.state);
   }
 
   handleBannerRemove() {
-    this.state.saveUserSettingsForm.banner = "";
+    this.state.saveUserSettingsForm.banner = Some("");
     this.setState(this.state);
   }
 
   handleDisplayNameChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.display_name = event.target.value;
+    i.state.saveUserSettingsForm.display_name = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleMatrixUserIdChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
-    if (
-      i.state.saveUserSettingsForm.matrix_user_id == "" &&
-      !UserService.Instance.myUserInfo.local_user_view.person.matrix_user_id
-    ) {
-      i.state.saveUserSettingsForm.matrix_user_id = undefined;
-    }
+    i.state.saveUserSettingsForm.matrix_user_id = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -1026,6 +1080,7 @@ export class Settings extends Component<any, SettingsState> {
   handleSaveSettingsSubmit(i: Settings, event: any) {
     event.preventDefault();
     i.state.saveUserSettingsLoading = true;
+    i.state.saveUserSettingsForm.auth = auth().unwrap();
     i.setState(i.state);
 
     WebSocketService.Instance.send(
@@ -1036,6 +1091,7 @@ export class Settings extends Component<any, SettingsState> {
   handleChangePasswordSubmit(i: Settings, event: any) {
     event.preventDefault();
     i.state.changePasswordLoading = true;
+    i.state.changePasswordForm.auth = auth().unwrap();
     i.setState(i.state);
 
     WebSocketService.Instance.send(
@@ -1057,6 +1113,7 @@ export class Settings extends Component<any, SettingsState> {
   handleDeleteAccount(i: Settings, event: any) {
     event.preventDefault();
     i.state.deleteAccountLoading = true;
+    i.state.deleteAccountForm.auth = auth().unwrap();
     i.setState(i.state);
 
     WebSocketService.Instance.send(
@@ -1074,36 +1131,55 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   setUserInfo() {
-    let luv = UserService.Instance.myUserInfo.local_user_view;
-    this.state.saveUserSettingsForm.show_nsfw = luv.local_user.show_nsfw;
-    this.state.saveUserSettingsForm.theme = luv.local_user.theme
-      ? luv.local_user.theme
-      : "browser";
-    this.state.saveUserSettingsForm.default_sort_type =
-      luv.local_user.default_sort_type;
-    this.state.saveUserSettingsForm.default_listing_type =
-      luv.local_user.default_listing_type;
-    this.state.saveUserSettingsForm.lang = luv.local_user.lang;
-    this.state.saveUserSettingsForm.avatar = luv.person.avatar;
-    this.state.saveUserSettingsForm.banner = luv.person.banner;
-    this.state.saveUserSettingsForm.display_name = luv.person.display_name;
-    this.state.saveUserSettingsForm.show_avatars = luv.local_user.show_avatars;
-    this.state.saveUserSettingsForm.bot_account = luv.person.bot_account;
-    this.state.saveUserSettingsForm.show_bot_accounts =
-      luv.local_user.show_bot_accounts;
-    this.state.saveUserSettingsForm.show_scores = luv.local_user.show_scores;
-    this.state.saveUserSettingsForm.show_read_posts =
-      luv.local_user.show_read_posts;
-    this.state.saveUserSettingsForm.show_new_post_notifs =
-      luv.local_user.show_new_post_notifs;
-    this.state.saveUserSettingsForm.email = luv.local_user.email;
-    this.state.saveUserSettingsForm.bio = luv.person.bio;
-    this.state.saveUserSettingsForm.send_notifications_to_email =
-      luv.local_user.send_notifications_to_email;
-    this.state.saveUserSettingsForm.matrix_user_id = luv.person.matrix_user_id;
-    this.state.personBlocks = UserService.Instance.myUserInfo.person_blocks;
-    this.state.communityBlocks =
-      UserService.Instance.myUserInfo.community_blocks;
+    UserService.Instance.myUserInfo.match({
+      some: mui => {
+        let luv = mui.local_user_view;
+        this.state.saveUserSettingsForm.show_nsfw = Some(
+          luv.local_user.show_nsfw
+        );
+        this.state.saveUserSettingsForm.theme = Some(
+          luv.local_user.theme ? luv.local_user.theme : "browser"
+        );
+        this.state.saveUserSettingsForm.default_sort_type = Some(
+          luv.local_user.default_sort_type
+        );
+        this.state.saveUserSettingsForm.default_listing_type = Some(
+          luv.local_user.default_listing_type
+        );
+        this.state.saveUserSettingsForm.lang = Some(luv.local_user.lang);
+        this.state.saveUserSettingsForm.avatar = luv.person.avatar;
+        this.state.saveUserSettingsForm.banner = luv.person.banner;
+        this.state.saveUserSettingsForm.display_name = luv.person.display_name;
+        this.state.saveUserSettingsForm.show_avatars = Some(
+          luv.local_user.show_avatars
+        );
+        this.state.saveUserSettingsForm.bot_account = Some(
+          luv.person.bot_account
+        );
+        this.state.saveUserSettingsForm.show_bot_accounts = Some(
+          luv.local_user.show_bot_accounts
+        );
+        this.state.saveUserSettingsForm.show_scores = Some(
+          luv.local_user.show_scores
+        );
+        this.state.saveUserSettingsForm.show_read_posts = Some(
+          luv.local_user.show_read_posts
+        );
+        this.state.saveUserSettingsForm.show_new_post_notifs = Some(
+          luv.local_user.show_new_post_notifs
+        );
+        this.state.saveUserSettingsForm.email = luv.local_user.email;
+        this.state.saveUserSettingsForm.bio = luv.person.bio;
+        this.state.saveUserSettingsForm.send_notifications_to_email = Some(
+          luv.local_user.send_notifications_to_email
+        );
+        this.state.saveUserSettingsForm.matrix_user_id =
+          luv.person.matrix_user_id;
+        this.state.personBlocks = mui.person_blocks;
+        this.state.communityBlocks = mui.community_blocks;
+      },
+      none: void 0,
+    });
   }
 
   parseMessage(msg: any) {
@@ -1118,14 +1194,14 @@ export class Settings extends Component<any, SettingsState> {
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.SaveUserSettings) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
+      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
       UserService.Instance.login(data);
       this.state.saveUserSettingsLoading = false;
       this.setState(this.state);
-
+      toast(i18n.t("saved"));
       window.scrollTo(0, 0);
     } else if (op == UserOperation.ChangePassword) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
+      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
       UserService.Instance.login(data);
       this.state.changePasswordLoading = false;
       this.setState(this.state);
@@ -1138,13 +1214,21 @@ export class Settings extends Component<any, SettingsState> {
       });
       UserService.Instance.logout();
       window.location.href = "/";
-      location.reload();
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
-      this.setState({ personBlocks: updatePersonBlock(data) });
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
+      updatePersonBlock(data).match({
+        some: blocks => this.setState({ personBlocks: blocks }),
+        none: void 0,
+      });
     } else if (op == UserOperation.BlockCommunity) {
-      let data = wsJsonToRes<BlockCommunityResponse>(msg).data;
-      this.setState({ communityBlocks: updateCommunityBlock(data) });
+      let data = wsJsonToRes<BlockCommunityResponse>(
+        msg,
+        BlockCommunityResponse
+      );
+      updateCommunityBlock(data).match({
+        some: blocks => this.setState({ communityBlocks: blocks }),
+        none: void 0,
+      });
     }
   }
 }
index d27a8bbc38dda098155a95505392a4b85be01641..fed026fdff2419aa6829623012653bc6e249bb10 100644 (file)
@@ -1,9 +1,12 @@
+import { None } from "@sniptt/monads/build";
 import { Component } from "inferno";
 import {
-  SiteView,
+  GetSiteResponse,
   UserOperation,
   VerifyEmail as VerifyEmailForm,
   VerifyEmailResponse,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
@@ -13,15 +16,13 @@ import {
   setIsoData,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 
 interface State {
   verifyEmailForm: VerifyEmailForm;
-  site_view: SiteView;
+  siteRes: GetSiteResponse;
 }
 
 export class VerifyEmail extends Component<any, State> {
@@ -29,10 +30,10 @@ export class VerifyEmail extends Component<any, State> {
   private subscription: Subscription;
 
   emptyState: State = {
-    verifyEmailForm: {
+    verifyEmailForm: new VerifyEmailForm({
       token: this.props.match.params.token,
-    },
-    site_view: this.isoData.site_res.site_view,
+    }),
+    siteRes: this.isoData.site_res,
   };
 
   constructor(props: any, context: any) {
@@ -57,7 +58,10 @@ export class VerifyEmail extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("verify_email")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("verify_email")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -66,6 +70,8 @@ export class VerifyEmail extends Component<any, State> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
@@ -85,7 +91,7 @@ export class VerifyEmail extends Component<any, State> {
       this.props.history.push("/");
       return;
     } else if (op == UserOperation.VerifyEmail) {
-      let data = wsJsonToRes<VerifyEmailResponse>(msg).data;
+      let data = wsJsonToRes<VerifyEmailResponse>(msg, VerifyEmailResponse);
       if (data) {
         toast(i18n.t("email_verified"));
         this.state = this.emptyState;
index b5c95c4a85fb68f14609d0b0757fe8c2ab820a70..68d546e2e376f5009b81095d1bf0d33f82ef2787 100644 (file)
@@ -1,48 +1,50 @@
+import { Either, Left, None, Option, Right, Some } from "@sniptt/monads";
 import { Component } from "inferno";
 import {
-  CommunityView,
   GetCommunity,
   GetCommunityResponse,
+  GetSiteResponse,
   ListCommunities,
   ListCommunitiesResponse,
   ListingType,
   PostView,
-  SiteView,
   SortType,
+  toOption,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
+  enableDownvotes,
+  enableNsfw,
   fetchLimit,
   isBrowser,
   setIsoData,
-  setOptionalAuth,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
 import { PostForm } from "./post-form";
 
 interface CreatePostState {
-  site_view: SiteView;
-  communities: CommunityView[];
+  listCommunitiesResponse: Option<ListCommunitiesResponse>;
+  siteRes: GetSiteResponse;
   loading: boolean;
 }
 
 export class CreatePost extends Component<any, CreatePostState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, ListCommunitiesResponse);
   private subscription: Subscription;
   private emptyState: CreatePostState = {
-    site_view: this.isoData.site_res.site_view,
-    communities: [],
+    siteRes: this.isoData.site_res,
+    listCommunitiesResponse: None,
     loading: true,
   };
 
@@ -51,7 +53,7 @@ export class CreatePost extends Component<any, CreatePostState> {
     this.handlePostCreate = this.handlePostCreate.bind(this);
     this.state = this.emptyState;
 
-    if (!UserService.Instance.myUserInfo && isBrowser()) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -61,7 +63,9 @@ export class CreatePost extends Component<any, CreatePostState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.communities = this.isoData.routeData[0].communities;
+      this.state.listCommunitiesResponse = Some(
+        this.isoData.routeData[0] as ListCommunitiesResponse
+      );
       this.state.loading = false;
     } else {
       this.refetch();
@@ -69,27 +73,39 @@ export class CreatePost extends Component<any, CreatePostState> {
   }
 
   refetch() {
-    if (this.params.community_id) {
-      let form: GetCommunity = {
-        id: this.params.community_id,
-      };
-      WebSocketService.Instance.send(wsClient.getCommunity(form));
-    } else if (this.params.community_name) {
-      let form: GetCommunity = {
-        name: this.params.community_name,
-      };
-      WebSocketService.Instance.send(wsClient.getCommunity(form));
-    } else {
-      let listCommunitiesForm: ListCommunities = {
-        type_: ListingType.All,
-        sort: SortType.TopAll,
-        limit: fetchLimit,
-        auth: authField(false),
-      };
-      WebSocketService.Instance.send(
-        wsClient.listCommunities(listCommunitiesForm)
-      );
-    }
+    this.params.nameOrId.match({
+      some: opt =>
+        opt.match({
+          left: name => {
+            let form = new GetCommunity({
+              name: Some(name),
+              id: None,
+              auth: auth(false).ok(),
+            });
+            WebSocketService.Instance.send(wsClient.getCommunity(form));
+          },
+          right: id => {
+            let form = new GetCommunity({
+              id: Some(id),
+              name: None,
+              auth: auth(false).ok(),
+            });
+            WebSocketService.Instance.send(wsClient.getCommunity(form));
+          },
+        }),
+      none: () => {
+        let listCommunitiesForm = new ListCommunities({
+          type_: Some(ListingType.All),
+          sort: Some(SortType.TopAll),
+          limit: Some(fetchLimit),
+          page: None,
+          auth: auth(false).ok(),
+        });
+        WebSocketService.Instance.send(
+          wsClient.listCommunities(listCommunitiesForm)
+        );
+      },
+    });
   }
 
   componentWillUnmount() {
@@ -99,7 +115,10 @@ export class CreatePost extends Component<any, CreatePostState> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("create_post")} - ${this.state.site_view.site.name}`;
+    return this.state.siteRes.site_view.match({
+      some: siteView => `${i18n.t("create_post")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -108,24 +127,32 @@ export class CreatePost extends Component<any, CreatePostState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         {this.state.loading ? (
           <h5>
             <Spinner large />
           </h5>
         ) : (
-          <div class="row">
-            <div class="col-12 col-lg-6 offset-lg-3 mb-4">
-              <h5>{i18n.t("create_post")}</h5>
-              <PostForm
-                communities={this.state.communities}
-                onCreate={this.handlePostCreate}
-                params={this.params}
-                enableDownvotes={this.state.site_view.site.enable_downvotes}
-                enableNsfw={this.state.site_view.site.enable_nsfw}
-              />
-            </div>
-          </div>
+          this.state.listCommunitiesResponse.match({
+            some: res => (
+              <div class="row">
+                <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+                  <h5>{i18n.t("create_post")}</h5>
+                  <PostForm
+                    post_view={None}
+                    communities={Some(res.communities)}
+                    onCreate={this.handlePostCreate}
+                    params={Some(this.params)}
+                    enableDownvotes={enableDownvotes(this.state.siteRes)}
+                    enableNsfw={enableNsfw(this.state.siteRes)}
+                  />
+                </div>
+              </div>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
@@ -133,36 +160,48 @@ export class CreatePost extends Component<any, CreatePostState> {
 
   get params(): PostFormParams {
     let urlParams = new URLSearchParams(this.props.location.search);
+    let name = toOption(urlParams.get("community_name")).or(
+      this.prevCommunityName
+    );
+    let id = toOption(urlParams.get("community_id"))
+      .map(Number)
+      .or(this.prevCommunityId);
+    let nameOrId: Option<Either<string, number>>;
+    if (name.isSome()) {
+      nameOrId = Some(Left(name.unwrap()));
+    } else if (id.isSome()) {
+      nameOrId = Some(Right(id.unwrap()));
+    } else {
+      nameOrId = None;
+    }
+
     let params: PostFormParams = {
-      name: urlParams.get("title"),
-      community_name: urlParams.get("community_name") || this.prevCommunityName,
-      community_id: urlParams.get("community_id")
-        ? Number(urlParams.get("community_id")) || this.prevCommunityId
-        : null,
-      body: urlParams.get("body"),
-      url: urlParams.get("url"),
+      name: toOption(urlParams.get("title")),
+      nameOrId,
+      body: toOption(urlParams.get("body")),
+      url: toOption(urlParams.get("url")),
     };
 
     return params;
   }
 
-  get prevCommunityName(): string {
+  get prevCommunityName(): Option<string> {
     if (this.props.match.params.name) {
-      return this.props.match.params.name;
+      return toOption(this.props.match.params.name);
     } else if (this.props.location.state) {
       let lastLocation = this.props.location.state.prevPath;
       if (lastLocation.includes("/c/")) {
-        return lastLocation.split("/c/")[1];
+        return toOption(lastLocation.split("/c/")[1]);
       }
     }
-    return null;
+    return None;
   }
 
-  get prevCommunityId(): number {
+  get prevCommunityId(): Option<number> {
     if (this.props.match.params.id) {
-      return this.props.match.params.id;
+      return toOption(this.props.match.params.id);
     }
-    return null;
+    return None;
   }
 
   handlePostCreate(post_view: PostView) {
@@ -170,12 +209,13 @@ export class CreatePost extends Component<any, CreatePostState> {
   }
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
-    let listCommunitiesForm: ListCommunities = {
-      type_: ListingType.All,
-      sort: SortType.TopAll,
-      limit: fetchLimit,
-    };
-    setOptionalAuth(listCommunitiesForm, req.auth);
+    let listCommunitiesForm = new ListCommunities({
+      type_: Some(ListingType.All),
+      sort: Some(SortType.TopAll),
+      limit: Some(fetchLimit),
+      page: None,
+      auth: req.auth,
+    });
     return [req.client.listCommunities(listCommunitiesForm)];
   }
 
@@ -186,13 +226,18 @@ export class CreatePost extends Component<any, CreatePostState> {
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.ListCommunities) {
-      let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
-      this.state.communities = data.communities;
+      let data = wsJsonToRes<ListCommunitiesResponse>(
+        msg,
+        ListCommunitiesResponse
+      );
+      this.state.listCommunitiesResponse = Some(data);
       this.state.loading = false;
       this.setState(this.state);
     } else if (op == UserOperation.GetCommunity) {
-      let data = wsJsonToRes<GetCommunityResponse>(msg).data;
-      this.state.communities = [data.community_view];
+      let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
+      this.state.listCommunitiesResponse = Some({
+        communities: [data.community_view],
+      });
       this.state.loading = false;
       this.setState(this.state);
     }
index 8c27d2f259554b94d009877beb74da37ad7c6146..116cdc9a4ea3ba281f8a43afda09f2dbebeaec55 100644 (file)
@@ -29,56 +29,71 @@ export class MetadataCard extends Component<
     let post = this.props.post;
     return (
       <>
-        {post.embed_title && !this.state.expanded && (
-          <div class="card border-secondary mt-3 mb-2">
-            <div class="row">
-              <div class="col-12">
-                <div class="card-body">
-                  {post.name !== post.embed_title && [
-                    <h5 class="card-title d-inline">
-                      <a class="text-body" href={post.url} rel={relTags}>
-                        {post.embed_title}
-                      </a>
-                    </h5>,
-                    <span class="d-inline-block ml-2 mb-2 small text-muted">
-                      <a
-                        class="text-muted font-italic"
-                        href={post.url}
-                        rel={relTags}
-                      >
-                        {new URL(post.url).hostname}
-                        <Icon icon="external-link" classes="ml-1" />
-                      </a>
-                    </span>,
-                  ]}
-                  {post.embed_description && (
-                    <div
-                      className="card-text small text-muted md-div"
-                      dangerouslySetInnerHTML={{
-                        __html: post.embed_description,
-                      }}
-                    />
-                  )}
-                  {post.embed_html && (
-                    <button
-                      class="mt-2 btn btn-secondary text-monospace"
-                      onClick={linkEvent(this, this.handleIframeExpand)}
-                      data-tippy-content={i18n.t("expand_here")}
-                    >
-                      {this.state.expanded ? "-" : "+"}
-                    </button>
-                  )}
-                </div>
-              </div>
-            </div>
-          </div>
-        )}
-        {this.state.expanded && (
-          <div
-            class="mt-3 mb-2"
-            dangerouslySetInnerHTML={{ __html: post.embed_html }}
-          />
-        )}
+        {!this.state.expanded &&
+          post.embed_title.match({
+            some: embedTitle =>
+              post.url.match({
+                some: url => (
+                  <div class="card border-secondary mt-3 mb-2">
+                    <div class="row">
+                      <div class="col-12">
+                        <div class="card-body">
+                          {post.name !== embedTitle && [
+                            <h5 class="card-title d-inline">
+                              <a class="text-body" href={url} rel={relTags}>
+                                {embedTitle}
+                              </a>
+                            </h5>,
+                            <span class="d-inline-block ml-2 mb-2 small text-muted">
+                              <a
+                                class="text-muted font-italic"
+                                href={url}
+                                rel={relTags}
+                              >
+                                {new URL(url).hostname}
+                                <Icon icon="external-link" classes="ml-1" />
+                              </a>
+                            </span>,
+                          ]}
+                          {post.embed_description.match({
+                            some: desc => (
+                              <div
+                                className="card-text small text-muted md-div"
+                                dangerouslySetInnerHTML={{
+                                  __html: desc,
+                                }}
+                              />
+                            ),
+                            none: <></>,
+                          })}
+                          {post.embed_html.isSome() && (
+                            <button
+                              class="mt-2 btn btn-secondary text-monospace"
+                              onClick={linkEvent(this, this.handleIframeExpand)}
+                              data-tippy-content={i18n.t("expand_here")}
+                            >
+                              {this.state.expanded ? "-" : "+"}
+                            </button>
+                          )}
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                ),
+                none: <></>,
+              }),
+            none: <></>,
+          })}
+        {this.state.expanded &&
+          post.embed_html.match({
+            some: html => (
+              <div
+                class="mt-3 mb-2"
+                dangerouslySetInnerHTML={{ __html: html }}
+              />
+            ),
+            none: <></>,
+          })}
       </>
     );
   }
index de0b7657a56fd94a22cf8b84a9792d2841bbf48b..1bb78407fa50c18dedbfb00018902ec0a4be9887 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import autosize from "autosize";
 import { Component, linkEvent } from "inferno";
 import { Prompt } from "inferno-router";
@@ -12,7 +13,10 @@ import {
   SearchResponse,
   SearchType,
   SortType,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { pictrsUri } from "../../env";
@@ -21,7 +25,7 @@ import { PostFormParams } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
   archiveTodayUrl,
-  authField,
+  auth,
   capitalizeFirstLetter,
   choicesConfig,
   communitySelectName,
@@ -36,13 +40,12 @@ import {
   relTags,
   setupTippy,
   toast,
+  trendingFetchLimit,
   validTitle,
   validURL,
   webArchiveUrl,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { Icon, Spinner } from "../common/icon";
 import { MarkdownTextArea } from "../common/markdown-textarea";
@@ -56,42 +59,45 @@ if (isBrowser()) {
 const MAX_POST_TITLE_LENGTH = 200;
 
 interface PostFormProps {
-  post_view?: PostView; // If a post is given, that means this is an edit
-  communities?: CommunityView[];
-  params?: PostFormParams;
+  post_view: Option<PostView>; // If a post is given, that means this is an edit
+  communities: Option<CommunityView[]>;
+  params: Option<PostFormParams>;
   onCancel?(): any;
   onCreate?(post: PostView): any;
   onEdit?(post: PostView): any;
-  enableNsfw: boolean;
-  enableDownvotes: boolean;
+  enableNsfw?: boolean;
+  enableDownvotes?: boolean;
 }
 
 interface PostFormState {
   postForm: CreatePost;
+  suggestedTitle: Option<string>;
+  suggestedPosts: Option<PostView[]>;
+  crossPosts: Option<PostView[]>;
   loading: boolean;
   imageLoading: boolean;
   previewMode: boolean;
-  suggestedTitle: string;
-  suggestedPosts: PostView[];
-  crossPosts: PostView[];
 }
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
   private subscription: Subscription;
   private choices: any;
   private emptyState: PostFormState = {
-    postForm: {
-      community_id: null,
-      name: null,
-      nsfw: false,
-      auth: authField(false),
-    },
+    postForm: new CreatePost({
+      community_id: undefined,
+      name: undefined,
+      nsfw: Some(false),
+      url: None,
+      body: None,
+      honeypot: None,
+      auth: undefined,
+    }),
     loading: false,
     imageLoading: false,
     previewMode: false,
-    suggestedTitle: undefined,
-    suggestedPosts: [],
-    crossPosts: [],
+    suggestedTitle: None,
+    suggestedPosts: None,
+    crossPosts: None,
   };
 
   constructor(props: any, context: any) {
@@ -103,26 +109,28 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.state = this.emptyState;
 
     // Means its an edit
-    if (this.props.post_view) {
-      this.state.postForm = {
-        body: this.props.post_view.post.body,
-        name: this.props.post_view.post.name,
-        community_id: this.props.post_view.community.id,
-        url: this.props.post_view.post.url,
-        nsfw: this.props.post_view.post.nsfw,
-        auth: authField(),
-      };
-    }
-
-    if (this.props.params) {
-      this.state.postForm.name = this.props.params.name;
-      if (this.props.params.url) {
-        this.state.postForm.url = this.props.params.url;
-      }
-      if (this.props.params.body) {
-        this.state.postForm.body = this.props.params.body;
-      }
-    }
+    this.props.post_view.match({
+      some: pv =>
+        (this.state.postForm = new CreatePost({
+          body: pv.post.body,
+          name: pv.post.name,
+          community_id: pv.community.id,
+          url: pv.post.url,
+          nsfw: Some(pv.post.nsfw),
+          honeypot: None,
+          auth: auth().unwrap(),
+        })),
+      none: void 0,
+    });
+
+    this.props.params.match({
+      some: params => {
+        this.state.postForm.name = toUndefined(params.name);
+        this.state.postForm.url = params.url;
+        this.state.postForm.body = params.body;
+      },
+      none: void 0,
+    });
 
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
@@ -141,8 +149,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     if (
       !this.state.loading &&
       (this.state.postForm.name ||
-        this.state.postForm.url ||
-        this.state.postForm.body)
+        this.state.postForm.url.isSome() ||
+        this.state.postForm.body.isSome())
     ) {
       window.onbeforeunload = () => true;
     } else {
@@ -163,8 +171,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
           when={
             !this.state.loading &&
             (this.state.postForm.name ||
-              this.state.postForm.url ||
-              this.state.postForm.body)
+              this.state.postForm.url.isSome() ||
+              this.state.postForm.body.isSome())
           }
           message={i18n.t("block_leaving")}
         />
@@ -178,26 +186,29 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                 type="url"
                 id="post-url"
                 class="form-control"
-                value={this.state.postForm.url}
+                value={toUndefined(this.state.postForm.url)}
                 onInput={linkEvent(this, this.handlePostUrlChange)}
                 onPaste={linkEvent(this, this.handleImageUploadPaste)}
               />
-              {this.state.suggestedTitle && (
-                <div
-                  class="mt-1 text-muted small font-weight-bold pointer"
-                  role="button"
-                  onClick={linkEvent(this, this.copySuggestedTitle)}
-                >
-                  {i18n.t("copy_suggested_title", {
-                    title: this.state.suggestedTitle,
-                  })}
-                </div>
-              )}
+              {this.state.suggestedTitle.match({
+                some: title => (
+                  <div
+                    class="mt-1 text-muted small font-weight-bold pointer"
+                    role="button"
+                    onClick={linkEvent(this, this.copySuggestedTitle)}
+                  >
+                    {i18n.t("copy_suggested_title", {
+                      title,
+                    })}
+                  </div>
+                ),
+                none: <></>,
+              })}
               <form>
                 <label
                   htmlFor="file-upload"
                   className={`${
-                    UserService.Instance.myUserInfo && "pointer"
+                    UserService.Instance.myUserInfo.isSome() && "pointer"
                   } d-inline-block float-right text-muted font-weight-bold`}
                   data-tippy-content={i18n.t("upload_image")}
                 >
@@ -209,58 +220,68 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                   accept="image/*,video/*"
                   name="file"
                   class="d-none"
-                  disabled={!UserService.Instance.myUserInfo}
+                  disabled={UserService.Instance.myUserInfo.isNone()}
                   onChange={linkEvent(this, this.handleImageUpload)}
                 />
               </form>
-              {this.state.postForm.url && validURL(this.state.postForm.url) && (
-                <div>
-                  <a
-                    href={`${webArchiveUrl}/save/${encodeURIComponent(
-                      this.state.postForm.url
-                    )}`}
-                    class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    archive.org {i18n.t("archive_link")}
-                  </a>
-                  <a
-                    href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
-                      this.state.postForm.url
-                    )}`}
-                    class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    ghostarchive.org {i18n.t("archive_link")}
-                  </a>
-                  <a
-                    href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
-                      this.state.postForm.url
-                    )}`}
-                    class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    archive.today {i18n.t("archive_link")}
-                  </a>
-                </div>
-              )}
+              {this.state.postForm.url.match({
+                some: url =>
+                  validURL(url) && (
+                    <div>
+                      <a
+                        href={`${webArchiveUrl}/save/${encodeURIComponent(
+                          url
+                        )}`}
+                        class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                        rel={relTags}
+                      >
+                        archive.org {i18n.t("archive_link")}
+                      </a>
+                      <a
+                        href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
+                          url
+                        )}`}
+                        class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                        rel={relTags}
+                      >
+                        ghostarchive.org {i18n.t("archive_link")}
+                      </a>
+                      <a
+                        href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
+                          url
+                        )}`}
+                        class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                        rel={relTags}
+                      >
+                        archive.today {i18n.t("archive_link")}
+                      </a>
+                    </div>
+                  ),
+                none: <></>,
+              })}
               {this.state.imageLoading && <Spinner />}
-              {isImage(this.state.postForm.url) && (
-                <img src={this.state.postForm.url} class="img-fluid" alt="" />
-              )}
-              {this.state.crossPosts.length > 0 && (
-                <>
-                  <div class="my-1 text-muted small font-weight-bold">
-                    {i18n.t("cross_posts")}
-                  </div>
-                  <PostListings
-                    showCommunity
-                    posts={this.state.crossPosts}
-                    enableDownvotes={this.props.enableDownvotes}
-                    enableNsfw={this.props.enableNsfw}
-                  />
-                </>
-              )}
+              {this.state.postForm.url.match({
+                some: url =>
+                  isImage(url) && <img src={url} class="img-fluid" alt="" />,
+                none: <></>,
+              })}
+              {this.state.crossPosts.match({
+                some: xPosts =>
+                  xPosts.length > 0 && (
+                    <>
+                      <div class="my-1 text-muted small font-weight-bold">
+                        {i18n.t("cross_posts")}
+                      </div>
+                      <PostListings
+                        showCommunity
+                        posts={xPosts}
+                        enableDownvotes={this.props.enableDownvotes}
+                        enableNsfw={this.props.enableNsfw}
+                      />
+                    </>
+                  ),
+                none: <></>,
+              })}
             </div>
           </div>
           <div class="form-group row">
@@ -285,18 +306,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                   {i18n.t("invalid_post_title")}
                 </div>
               )}
-              {this.state.suggestedPosts.length > 0 && (
-                <>
-                  <div class="my-1 text-muted small font-weight-bold">
-                    {i18n.t("related_posts")}
-                  </div>
-                  <PostListings
-                    posts={this.state.suggestedPosts}
-                    enableDownvotes={this.props.enableDownvotes}
-                    enableNsfw={this.props.enableNsfw}
-                  />
-                </>
-              )}
+              {this.state.suggestedPosts.match({
+                some: sPosts =>
+                  sPosts.length > 0 && (
+                    <>
+                      <div class="my-1 text-muted small font-weight-bold">
+                        {i18n.t("related_posts")}
+                      </div>
+                      <PostListings
+                        posts={sPosts}
+                        enableDownvotes={this.props.enableDownvotes}
+                        enableNsfw={this.props.enableNsfw}
+                      />
+                    </>
+                  ),
+                none: <></>,
+              })}
             </div>
           </div>
 
@@ -306,10 +331,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
               <MarkdownTextArea
                 initialContent={this.state.postForm.body}
                 onContentChange={this.handlePostBodyChange}
+                placeholder={None}
+                buttonTitle={None}
+                maxLength={None}
               />
             </div>
           </div>
-          {!this.props.post_view && (
+          {this.props.post_view.isNone() && (
             <div class="form-group row">
               <label class="col-sm-2 col-form-label" htmlFor="post-community">
                 {i18n.t("community")}
@@ -322,7 +350,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                   onInput={linkEvent(this, this.handlePostCommunityChange)}
                 >
                   <option>{i18n.t("select_a_community")}</option>
-                  {this.props.communities.map(cv => (
+                  {this.props.communities.unwrapOr([]).map(cv => (
                     <option value={cv.community.id}>
                       {communitySelectName(cv)}
                     </option>
@@ -342,7 +370,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                     class="form-check-input position-static"
                     id="post-nsfw"
                     type="checkbox"
-                    checked={this.state.postForm.nsfw}
+                    checked={toUndefined(this.state.postForm.nsfw)}
                     onChange={linkEvent(this, this.handlePostNsfwChange)}
                   />
                 </div>
@@ -356,7 +384,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
             type="text"
             class="form-control honeypot"
             id="register-honey"
-            value={this.state.postForm.honeypot}
+            value={toUndefined(this.state.postForm.honeypot)}
             onInput={linkEvent(this, this.handleHoneyPotChange)}
           />
           <div class="form-group row">
@@ -370,13 +398,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
               >
                 {this.state.loading ? (
                   <Spinner />
-                ) : this.props.post_view ? (
+                ) : this.props.post_view.isSome() ? (
                   capitalizeFirstLetter(i18n.t("save"))
                 ) : (
                   capitalizeFirstLetter(i18n.t("create"))
                 )}
               </button>
-              {this.props.post_view && (
+              {this.props.post_view.isSome() && (
                 <button
                   type="button"
                   class="btn btn-secondary"
@@ -396,65 +424,87 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     event.preventDefault();
 
     // Coerce empty url string to undefined
-    if (i.state.postForm.url !== undefined && i.state.postForm.url === "") {
-      i.state.postForm.url = undefined;
+    if (
+      i.state.postForm.url.isSome() &&
+      i.state.postForm.url.unwrapOr("blank") === ""
+    ) {
+      i.state.postForm.url = None;
     }
 
-    if (i.props.post_view) {
-      let form: EditPost = {
-        ...i.state.postForm,
-        post_id: i.props.post_view.post.id,
-      };
-      WebSocketService.Instance.send(wsClient.editPost(form));
-    } else {
-      WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
-    }
+    let pForm = i.state.postForm;
+    i.props.post_view.match({
+      some: pv => {
+        let form = new EditPost({
+          name: Some(pForm.name),
+          url: pForm.url,
+          body: pForm.body,
+          nsfw: pForm.nsfw,
+          post_id: pv.post.id,
+          auth: auth().unwrap(),
+        });
+        WebSocketService.Instance.send(wsClient.editPost(form));
+      },
+      none: () => {
+        i.state.postForm.auth = auth().unwrap();
+        WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
+      },
+    });
     i.state.loading = true;
     i.setState(i.state);
   }
 
   copySuggestedTitle(i: PostForm) {
-    i.state.postForm.name = i.state.suggestedTitle.substring(
-      0,
-      MAX_POST_TITLE_LENGTH
-    );
-    i.state.suggestedTitle = undefined;
-    setTimeout(() => {
-      let textarea: any = document.getElementById("post-title");
-      autosize.update(textarea);
-    }, 10);
-    i.setState(i.state);
+    i.state.suggestedTitle.match({
+      some: sTitle => {
+        i.state.postForm.name = sTitle.substring(0, MAX_POST_TITLE_LENGTH);
+        i.state.suggestedTitle = None;
+        setTimeout(() => {
+          let textarea: any = document.getElementById("post-title");
+          autosize.update(textarea);
+        }, 10);
+        i.setState(i.state);
+      },
+      none: void 0,
+    });
   }
 
   handlePostUrlChange(i: PostForm, event: any) {
-    i.state.postForm.url = event.target.value;
+    i.state.postForm.url = Some(event.target.value);
     i.setState(i.state);
     i.fetchPageTitle();
   }
 
   fetchPageTitle() {
-    if (validURL(this.state.postForm.url)) {
-      let form: Search = {
-        q: this.state.postForm.url,
-        type_: SearchType.Url,
-        sort: SortType.TopAll,
-        listing_type: ListingType.All,
-        page: 1,
-        limit: 6,
-        auth: authField(false),
-      };
-
-      WebSocketService.Instance.send(wsClient.search(form));
-
-      // Fetch the page title
-      getSiteMetadata(this.state.postForm.url).then(d => {
-        this.state.suggestedTitle = d.metadata.title;
-        this.setState(this.state);
-      });
-    } else {
-      this.state.suggestedTitle = undefined;
-      this.state.crossPosts = [];
-    }
+    this.state.postForm.url.match({
+      some: url => {
+        if (validURL(url)) {
+          let form = new Search({
+            q: url,
+            community_id: None,
+            community_name: None,
+            creator_id: None,
+            type_: Some(SearchType.Url),
+            sort: Some(SortType.TopAll),
+            listing_type: Some(ListingType.All),
+            page: Some(1),
+            limit: Some(trendingFetchLimit),
+            auth: auth(false).ok(),
+          });
+
+          WebSocketService.Instance.send(wsClient.search(form));
+
+          // Fetch the page title
+          getSiteMetadata(url).then(d => {
+            this.state.suggestedTitle = d.metadata.title;
+            this.setState(this.state);
+          });
+        } else {
+          this.state.suggestedTitle = None;
+          this.state.crossPosts = None;
+        }
+      },
+      none: void 0,
+    });
   }
 
   handlePostNameChange(i: PostForm, event: any) {
@@ -464,28 +514,30 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   fetchSimilarPosts() {
-    let form: Search = {
+    let form = new Search({
       q: this.state.postForm.name,
-      type_: SearchType.Posts,
-      sort: SortType.TopAll,
-      listing_type: ListingType.All,
-      community_id: this.state.postForm.community_id,
-      page: 1,
-      limit: 6,
-      auth: authField(false),
-    };
+      type_: Some(SearchType.Posts),
+      sort: Some(SortType.TopAll),
+      listing_type: Some(ListingType.All),
+      community_id: Some(this.state.postForm.community_id),
+      community_name: None,
+      creator_id: None,
+      page: Some(1),
+      limit: Some(trendingFetchLimit),
+      auth: auth(false).ok(),
+    });
 
     if (this.state.postForm.name !== "") {
       WebSocketService.Instance.send(wsClient.search(form));
     } else {
-      this.state.suggestedPosts = [];
+      this.state.suggestedPosts = None;
     }
 
     this.setState(this.state);
   }
 
   handlePostBodyChange(val: string) {
-    this.state.postForm.body = val;
+    this.state.postForm.body = Some(val);
     this.setState(this.state);
   }
 
@@ -495,12 +547,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   handlePostNsfwChange(i: PostForm, event: any) {
-    i.state.postForm.nsfw = event.target.checked;
+    i.state.postForm.nsfw = Some(event.target.checked);
     i.setState(i.state);
   }
 
   handleHoneyPotChange(i: PostForm, event: any) {
-    i.state.postForm.honeypot = event.target.value;
+    i.state.postForm.honeypot = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -549,7 +601,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
           let url = `${pictrsUri}/${hash}`;
           let deleteToken = res.files[0].delete_token;
           let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
-          i.state.postForm.url = url;
+          i.state.postForm.url = Some(url);
           i.state.imageLoading = false;
           i.setState(i.state);
           pictrsDeleteToast(
@@ -606,30 +658,34 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       }
     }
 
-    if (this.props.post_view) {
-      this.state.postForm.community_id = this.props.post_view.community.id;
-    } else if (
-      this.props.params &&
-      (this.props.params.community_id || this.props.params.community_name)
-    ) {
-      if (this.props.params.community_name) {
-        let foundCommunityId = this.props.communities.find(
-          r => r.community.name == this.props.params.community_name
-        ).community.id;
-        this.state.postForm.community_id = foundCommunityId;
-      } else if (this.props.params.community_id) {
-        this.state.postForm.community_id = this.props.params.community_id;
-      }
-
-      if (isBrowser()) {
-        this.choices.setChoiceByValue(
-          this.state.postForm.community_id.toString()
-        );
-      }
-      this.setState(this.state);
-    } else {
-      // By default, the null valued 'Select a Community'
+    this.props.post_view.match({
+      some: pv => (this.state.postForm.community_id = pv.community.id),
+      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.state.postForm.community_id = foundCommunityId;
+              },
+              right: id => (this.state.postForm.community_id = id),
+            }),
+          none: void 0,
+        }),
+      none: void 0,
+    });
+
+    if (isBrowser() && this.state.postForm.community_id) {
+      this.choices.setChoiceByValue(
+        this.state.postForm.community_id.toString()
+      );
     }
+    this.setState(this.state);
   }
 
   parseMessage(msg: any) {
@@ -642,30 +698,34 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       this.setState(this.state);
       return;
     } else if (op == UserOperation.CreatePost) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      if (
-        data.post_view.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        this.state.loading = false;
-        this.props.onCreate(data.post_view);
-      }
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.post_view.creator.id == mui.local_user_view.person.id) {
+            this.state.loading = false;
+            this.props.onCreate(data.post_view);
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.EditPost) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      if (
-        data.post_view.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-      ) {
-        this.state.loading = false;
-        this.props.onEdit(data.post_view);
-      }
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.post_view.creator.id == mui.local_user_view.person.id) {
+            this.state.loading = false;
+            this.props.onEdit(data.post_view);
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.Search) {
-      let data = wsJsonToRes<SearchResponse>(msg).data;
+      let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
 
       if (data.type_ == SearchType[SearchType.Posts]) {
-        this.state.suggestedPosts = data.posts;
+        this.state.suggestedPosts = Some(data.posts);
       } else if (data.type_ == SearchType[SearchType.Url]) {
-        this.state.crossPosts = data.posts;
+        this.state.crossPosts = Some(data.posts);
       }
       this.setState(this.state);
     }
index 253eeb4e8adbbb2d321b609e436102168e1caeba..eadf48a030722495747b8977016f9bf480e324b5 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import classNames from "classnames";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
@@ -17,6 +18,7 @@ import {
   RemovePost,
   SavePost,
   StickyPost,
+  toUndefined,
   TransferCommunity,
 } from "lemmy-js-client";
 import { externalHost } from "../../env";
@@ -24,10 +26,13 @@ import { i18n } from "../../i18next";
 import { BanType } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  amCommunityCreator,
+  auth,
+  canAdmin,
   canMod,
   futureDaysToUnixTime,
   hostname,
+  isAdmin,
   isBanned,
   isImage,
   isMod,
@@ -51,10 +56,10 @@ import { PostForm } from "./post-form";
 interface PostListingState {
   showEdit: boolean;
   showRemoveDialog: boolean;
-  removeReason: string;
+  removeReason: Option<string>;
   showBanDialog: boolean;
-  banReason: string;
-  banExpireDays: number;
+  banReason: Option<string>;
+  banExpireDays: Option<number>;
   banType: BanType;
   removeData: boolean;
   showConfirmTransferSite: boolean;
@@ -65,8 +70,8 @@ interface PostListingState {
   showMoreMobile: boolean;
   showBody: boolean;
   showReportDialog: boolean;
-  reportReason: string;
-  my_vote: number;
+  reportReason: Option<string>;
+  my_vote: Option<number>;
   score: number;
   upvotes: number;
   downvotes: number;
@@ -74,13 +79,13 @@ interface PostListingState {
 
 interface PostListingProps {
   post_view: PostView;
-  duplicates?: PostView[];
+  duplicates: Option<PostView[]>;
+  moderators: Option<CommunityModeratorView[]>;
+  admins: Option<PersonViewSafe[]>;
   showCommunity?: boolean;
   showBody?: boolean;
-  moderators?: CommunityModeratorView[];
-  admins?: PersonViewSafe[];
-  enableDownvotes: boolean;
-  enableNsfw: boolean;
+  enableDownvotes?: boolean;
+  enableNsfw?: boolean;
   viewOnly?: boolean;
 }
 
@@ -88,10 +93,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   private emptyState: PostListingState = {
     showEdit: false,
     showRemoveDialog: false,
-    removeReason: null,
+    removeReason: None,
     showBanDialog: false,
-    banReason: null,
-    banExpireDays: null,
+    banReason: None,
+    banExpireDays: None,
     banType: BanType.Community,
     removeData: false,
     showConfirmTransferSite: false,
@@ -102,7 +107,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     showMoreMobile: false,
     showBody: false,
     showReportDialog: false,
-    reportReason: null,
+    reportReason: None,
     my_vote: this.props.post_view.my_vote,
     score: this.props.post_view.counts.score,
     upvotes: this.props.post_view.counts.upvotes,
@@ -138,15 +143,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           <>
             {this.listing()}
             {this.state.imageExpanded && this.img}
-            {post.url && this.showBody && post.embed_title && (
-              <MetadataCard post={post} />
-            )}
-            {this.showBody && post.body && this.body()}
+            {post.url.isSome() &&
+              this.showBody &&
+              post.embed_title.isSome() && <MetadataCard post={post} />}
+            {this.showBody && this.body()}
           </>
         ) : (
           <div class="col-12">
             <PostForm
-              post_view={this.props.post_view}
+              post_view={Some(this.props.post_view)}
+              communities={None}
+              params={None}
               onEdit={this.handleEditPost}
               onCancel={this.handleEditCancel}
               enableNsfw={this.props.enableNsfw}
@@ -159,39 +166,41 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   body() {
-    let post = this.props.post_view.post;
-    return (
-      <div class="col-12 card my-2 p-2">
-        {this.state.viewSource ? (
-          <pre>{post.body}</pre>
-        ) : (
-          <div
-            className="md-div"
-            dangerouslySetInnerHTML={mdToHtml(post.body)}
-          />
-        )}
-      </div>
-    );
+    return this.props.post_view.post.body.match({
+      some: body => (
+        <div class="col-12 card my-2 p-2">
+          {this.state.viewSource ? (
+            <pre>{body}</pre>
+          ) : (
+            <div className="md-div" dangerouslySetInnerHTML={mdToHtml(body)} />
+          )}
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   get img() {
-    return (
-      <>
-        <div class="offset-sm-3 my-2 d-none d-sm-block">
-          <a href={this.imageSrc} class="d-inline-block">
-            <PictrsImage src={this.imageSrc} />
-          </a>
-        </div>
-        <div className="my-2 d-block d-sm-none">
-          <a
-            class="d-inline-block"
-            onClick={linkEvent(this, this.handleImageExpandClick)}
-          >
-            <PictrsImage src={this.imageSrc} />
-          </a>
-        </div>
-      </>
-    );
+    return this.imageSrc.match({
+      some: src => (
+        <>
+          <div class="offset-sm-3 my-2 d-none d-sm-block">
+            <a href={src} class="d-inline-block">
+              <PictrsImage src={src} />
+            </a>
+          </div>
+          <div className="my-2 d-block d-sm-none">
+            <a
+              class="d-inline-block"
+              onClick={linkEvent(this, this.handleImageExpandClick)}
+            >
+              <PictrsImage src={src} />
+            </a>
+          </div>
+        </>
+      ),
+      none: <></>,
+    });
   }
 
   imgThumb(src: string) {
@@ -206,53 +215,58 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  get imageSrc(): string {
+  get imageSrc(): Option<string> {
     let post = this.props.post_view.post;
-    if (isImage(post.url)) {
-      if (post.url.includes("pictrs")) {
-        return post.url;
-      } else if (post.thumbnail_url) {
-        return post.thumbnail_url;
+    let url = post.url;
+    let thumbnail = post.thumbnail_url;
+
+    if (url.isSome() && isImage(url.unwrap())) {
+      if (url.unwrap().includes("pictrs")) {
+        return url;
+      } else if (thumbnail.isSome()) {
+        return thumbnail;
       } else {
-        return post.url;
+        return url;
       }
-    } else if (post.thumbnail_url) {
-      return post.thumbnail_url;
+    } else if (thumbnail.isSome()) {
+      return thumbnail;
     } else {
-      return null;
+      return None;
     }
   }
 
   thumbnail() {
     let post = this.props.post_view.post;
+    let url = post.url;
+    let thumbnail = post.thumbnail_url;
 
-    if (isImage(post.url)) {
+    if (url.isSome() && isImage(url.unwrap())) {
       return (
         <a
-          href={this.imageSrc}
+          href={this.imageSrc.unwrap()}
           class="text-body d-inline-block position-relative mb-2"
           data-tippy-content={i18n.t("expand_here")}
           onClick={linkEvent(this, this.handleImageExpandClick)}
           aria-label={i18n.t("expand_here")}
         >
-          {this.imgThumb(this.imageSrc)}
+          {this.imgThumb(this.imageSrc.unwrap())}
           <Icon icon="image" classes="mini-overlay" />
         </a>
       );
-    } else if (post.thumbnail_url) {
+    } else if (url.isSome() && thumbnail.isSome()) {
       return (
         <a
           class="text-body d-inline-block position-relative mb-2"
-          href={post.url}
+          href={url.unwrap()}
           rel={relTags}
-          title={post.url}
+          title={url.unwrap()}
         >
-          {this.imgThumb(this.imageSrc)}
+          {this.imgThumb(this.imageSrc.unwrap())}
           <Icon icon="external-link" classes="mini-overlay" />
         </a>
       );
-    } else if (post.url) {
-      if (isVideo(post.url)) {
+    } else if (url.isSome()) {
+      if (isVideo(url.unwrap())) {
         return (
           <div class="embed-responsive embed-responsive-16by9">
             <video
@@ -262,7 +276,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               controls
               class="embed-responsive-item"
             >
-              <source src={post.url} type="video/mp4" />
+              <source src={url.unwrap()} type="video/mp4" />
             </video>
           </div>
         );
@@ -270,8 +284,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         return (
           <a
             className="text-body"
-            href={post.url}
-            title={post.url}
+            href={url.unwrap()}
+            title={url.unwrap()}
             rel={relTags}
           >
             <div class="thumbnail rounded bg-light d-flex justify-content-center">
@@ -302,10 +316,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <li className="list-inline-item">
           <PersonListing person={post_view.creator} />
 
-          {this.creatorIsMod && (
+          {this.creatorIsMod_ && (
             <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
           )}
-          {this.creatorIsAdmin && (
+          {this.creatorIsAdmin_ && (
             <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
           )}
           {post_view.creator.bot_account && (
@@ -328,41 +342,51 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           )}
         </li>
         <li className="list-inline-item">•</li>
-        {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
-          <>
-            <li className="list-inline-item">
-              <a
-                className="text-muted font-italic"
-                href={post_view.post.url}
-                title={post_view.post.url}
-                rel={relTags}
-              >
-                {hostname(post_view.post.url)}
-              </a>
-            </li>
-            <li className="list-inline-item">•</li>
-          </>
-        )}
+        {post_view.post.url.match({
+          some: url =>
+            !(hostname(url) == externalHost) && (
+              <>
+                <li className="list-inline-item">
+                  <a
+                    className="text-muted font-italic"
+                    href={url}
+                    title={url}
+                    rel={relTags}
+                  >
+                    {hostname(url)}
+                  </a>
+                </li>
+                <li className="list-inline-item">•</li>
+              </>
+            ),
+          none: <></>,
+        })}
         <li className="list-inline-item">
           <span>
-            <MomentTime data={post_view.post} />
+            <MomentTime
+              published={post_view.post.published}
+              updated={post_view.post.updated}
+            />
           </span>
         </li>
-        {post_view.post.body && (
-          <>
-            <li className="list-inline-item">•</li>
-            <li className="list-inline-item">
-              <button
-                className="text-muted btn btn-sm btn-link p-0"
-                data-tippy-content={md.render(post_view.post.body)}
-                data-tippy-allowHtml={true}
-                onClick={linkEvent(this, this.handleShowBody)}
-              >
-                <Icon icon="book-open" classes="icon-inline mr-1" />
-              </button>
-            </li>
-          </>
-        )}
+        {post_view.post.body.match({
+          some: body => (
+            <>
+              <li className="list-inline-item">•</li>
+              <li className="list-inline-item">
+                <button
+                  className="text-muted btn btn-sm btn-link p-0"
+                  data-tippy-content={md.render(body)}
+                  data-tippy-allowHtml={true}
+                  onClick={linkEvent(this, this.handleShowBody)}
+                >
+                  <Icon icon="book-open" classes="icon-inline mr-1" />
+                </button>
+              </li>
+            </>
+          ),
+          none: <></>,
+        })}
       </ul>
     );
   }
@@ -372,7 +396,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       <div className={`vote-bar col-1 pr-0 small text-center`}>
         <button
           className={`btn-animate btn btn-link p-0 ${
-            this.state.my_vote == 1 ? "text-info" : "text-muted"
+            this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
           }`}
           onClick={linkEvent(this, this.handlePostLike)}
           data-tippy-content={i18n.t("upvote")}
@@ -393,7 +417,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         {this.props.enableDownvotes && (
           <button
             className={`btn-animate btn btn-link p-0 ${
-              this.state.my_vote == -1 ? "text-danger" : "text-muted"
+              this.state.my_vote.unwrapOr(0) == -1
+                ? "text-danger"
+                : "text-muted"
             }`}
             onClick={linkEvent(this, this.handlePostDisLike)}
             data-tippy-content={i18n.t("downvote")}
@@ -411,25 +437,28 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       <div className="post-title overflow-hidden">
         <h5>
-          {this.showBody && post.url ? (
-            <a
-              className={!post.stickied ? "text-body" : "text-primary"}
-              href={post.url}
-              title={post.url}
-              rel={relTags}
-            >
-              {post.name}
-            </a>
-          ) : (
-            <Link
-              className={!post.stickied ? "text-body" : "text-primary"}
-              to={`/post/${post.id}`}
-              title={i18n.t("comments")}
-            >
-              {post.name}
-            </Link>
-          )}
-          {(isImage(post.url) || post.thumbnail_url) && (
+          {post.url.match({
+            some: url => (
+              <a
+                className={!post.stickied ? "text-body" : "text-primary"}
+                href={url}
+                title={url}
+                rel={relTags}
+              >
+                {post.name}
+              </a>
+            ),
+            none: (
+              <Link
+                className={!post.stickied ? "text-body" : "text-primary"}
+                to={`/post/${post.id}`}
+                title={i18n.t("comments")}
+              >
+                {post.name}
+              </Link>
+            ),
+          })}
+          {post.url.map(isImage).or(post.thumbnail_url).unwrapOr(false) && (
             <button
               class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
               data-tippy-content={i18n.t("expand_here")}
@@ -483,28 +512,30 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   duplicatesLine() {
-    let dupes = this.props.duplicates;
-    return (
-      dupes &&
-      dupes.length > 0 && (
-        <ul class="list-inline mb-1 small text-muted">
-          <>
-            <li className="list-inline-item mr-2">
-              {i18n.t("cross_posted_to")}
-            </li>
-            {dupes.map(pv => (
+    return this.props.duplicates.match({
+      some: dupes =>
+        dupes.length > 0 && (
+          <ul class="list-inline mb-1 small text-muted">
+            <>
               <li className="list-inline-item mr-2">
-                <Link to={`/post/${pv.post.id}`}>
-                  {pv.community.local
-                    ? pv.community.name
-                    : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
-                </Link>
+                {i18n.t("cross_posted_to")}
               </li>
-            ))}
-          </>
-        </ul>
-      )
-    );
+              {dupes.map(pv => (
+                <li className="list-inline-item mr-2">
+                  <Link to={`/post/${pv.post.id}`}>
+                    {pv.community.local
+                      ? pv.community.name
+                      : `${pv.community.name}@${hostname(
+                          pv.community.actor_id
+                        )}`}
+                  </Link>
+                </li>
+              ))}
+            </>
+          </ul>
+        ),
+      none: <></>,
+    });
   }
 
   commentsLine(mobile = false) {
@@ -522,7 +553,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           </a>
         )}
         {mobile && !this.props.viewOnly && this.mobileVotes}
-        {UserService.Instance.myUserInfo &&
+        {UserService.Instance.myUserInfo.isSome() &&
           !this.props.viewOnly &&
           this.postActions(mobile)}
       </div>
@@ -556,16 +587,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         )}
         {this.state.showAdvanced && (
           <>
-            {this.showBody && post_view.post.body && this.viewSourceButton}
-            {this.canModOnSelf && (
+            {this.showBody &&
+              post_view.post.body.isSome() &&
+              this.viewSourceButton}
+            {this.canModOnSelf_ && (
               <>
                 {this.lockButton}
                 {this.stickyButton}
               </>
             )}
-            {(this.canMod || this.canAdmin || true) && (
-              <>{this.modRemoveButton}</>
-            )}
+            {(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
           </>
         )}
         {!mobile && this.showMoreButton}
@@ -603,7 +634,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <div>
           <button
             className={`btn-animate btn py-0 px-1 ${
-              this.state.my_vote == 1 ? "text-info" : "text-muted"
+              this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
             }`}
             {...tippy}
             onClick={linkEvent(this, this.handlePostLike)}
@@ -617,7 +648,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           {this.props.enableDownvotes && (
             <button
               className={`ml-2 btn-animate btn py-0 px-1 ${
-                this.state.my_vote == -1 ? "text-danger" : "text-muted"
+                this.state.my_vote.unwrapOr(0) == -1
+                  ? "text-danger"
+                  : "text-muted"
               }`}
               onClick={linkEvent(this, this.handlePostDisLike)}
               {...tippy}
@@ -822,9 +855,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       this.state.showAdvanced && (
         <>
-          {this.canMod && (
+          {this.canMod_ && (
             <>
-              {!this.creatorIsMod &&
+              {!this.creatorIsMod_ &&
                 (!post_view.creator_banned_from_community ? (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
@@ -853,12 +886,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   class="btn btn-link btn-animate text-muted py-0"
                   onClick={linkEvent(this, this.handleAddModToCommunity)}
                   aria-label={
-                    this.creatorIsMod
+                    this.creatorIsMod_
                       ? i18n.t("remove_as_mod")
                       : i18n.t("appoint_as_mod")
                   }
                 >
-                  {this.creatorIsMod
+                  {this.creatorIsMod_
                     ? i18n.t("remove_as_mod")
                     : i18n.t("appoint_as_mod")}
                 </button>
@@ -866,8 +899,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             </>
           )}
           {/* Community creators and admins can transfer community to another mod */}
-          {(this.amCommunityCreator || this.canAdmin) &&
-            this.creatorIsMod &&
+          {(amCommunityCreator(this.props.moderators, post_view.creator.id) ||
+            this.canAdmin_) &&
+            this.creatorIsMod_ &&
             (!this.state.showConfirmTransferCommunity ? (
               <button
                 class="btn btn-link btn-animate text-muted py-0"
@@ -907,9 +941,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               </>
             ))}
           {/* Admins can ban from all, and appoint other admins */}
-          {this.canAdmin && (
+          {this.canAdmin_ && (
             <>
-              {!this.creatorIsAdmin &&
+              {!this.creatorIsAdmin_ &&
                 (!isBanned(post_view.creator) ? (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
@@ -932,12 +966,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   class="btn btn-link btn-animate text-muted py-0"
                   onClick={linkEvent(this, this.handleAddAdmin)}
                   aria-label={
-                    this.creatorIsAdmin
+                    this.creatorIsAdmin_
                       ? i18n.t("remove_as_admin")
                       : i18n.t("appoint_as_admin")
                   }
                 >
-                  {this.creatorIsAdmin
+                  {this.creatorIsAdmin_
                     ? i18n.t("remove_as_admin")
                     : i18n.t("appoint_as_admin")}
                 </button>
@@ -966,7 +1000,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               id="post-listing-remove-reason"
               class="form-control mr-2"
               placeholder={i18n.t("reason")}
-              value={this.state.removeReason}
+              value={toUndefined(this.state.removeReason)}
               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
             />
             <button
@@ -989,7 +1023,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 id="post-listing-ban-reason"
                 class="form-control mr-2"
                 placeholder={i18n.t("reason")}
-                value={this.state.banReason}
+                value={toUndefined(this.state.banReason)}
                 onInput={linkEvent(this, this.handleModBanReasonChange)}
               />
               <label class="col-form-label" htmlFor={`mod-ban-expires`}>
@@ -1000,7 +1034,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 id={`mod-ban-expires`}
                 class="form-control mr-2"
                 placeholder={i18n.t("number_of_days")}
-                value={this.state.banExpireDays}
+                value={toUndefined(this.state.banExpireDays)}
                 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
               />
               <div class="form-group">
@@ -1052,7 +1086,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               class="form-control mr-2"
               placeholder={i18n.t("reason")}
               required
-              value={this.state.reportReason}
+              value={toUndefined(this.state.reportReason)}
               onInput={linkEvent(this, this.handleReportReasonChange)}
             />
             <button
@@ -1070,7 +1104,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   mobileThumbnail() {
     let post = this.props.post_view.post;
-    return post.thumbnail_url || isImage(post.url) ? (
+    return post.thumbnail_url.isSome() ||
+      post.url.map(isImage).unwrapOr(false) ? (
       <div class="row">
         <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
           {this.postTitleLine()}
@@ -1088,10 +1123,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   showMobilePreview() {
     let post = this.props.post_view.post;
     return (
-      post.body &&
-      !this.showBody && (
-        <div className="md-div mb-1 preview-lines">{post.body}</div>
-      )
+      !this.showBody &&
+      post.body.match({
+        some: body => <div className="md-div mb-1 preview-lines">{body}</div>,
+        none: <></>,
+      })
     );
   }
 
@@ -1144,119 +1180,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   private get myPost(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      this.props.post_view.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-    );
-  }
-
-  get creatorIsMod(): boolean {
-    return (
-      this.props.moderators &&
-      isMod(
-        this.props.moderators.map(m => m.moderator.id),
-        this.props.post_view.creator.id
-      )
-    );
-  }
-
-  get creatorIsAdmin(): boolean {
-    return (
-      this.props.admins &&
-      isMod(
-        this.props.admins.map(a => a.person.id),
-        this.props.post_view.creator.id
-      )
-    );
-  }
-
-  /**
-   * If the current user is allowed to mod this post.
-   * The creator of this post is not allowed even if they are a mod.
-   */
-  get canMod(): boolean {
-    if (this.props.admins && this.props.moderators) {
-      let adminsThenMods = this.props.admins
-        .map(a => a.person.id)
-        .concat(this.props.moderators.map(m => m.moderator.id));
-
-      return canMod(
-        UserService.Instance.myUserInfo,
-        adminsThenMods,
-        this.props.post_view.creator.id
-      );
-    } else {
-      return false;
-    }
-  }
-
-  /**
-   * If the current user is allowed to mod this post.
-   * The creator of this post is allowed if they are a mod.
-   */
-  get canModOnSelf(): boolean {
-    if (this.props.admins && this.props.moderators) {
-      let adminsThenMods = this.props.admins
-        .map(a => a.person.id)
-        .concat(this.props.moderators.map(m => m.moderator.id));
-
-      return canMod(
-        UserService.Instance.myUserInfo,
-        adminsThenMods,
-        this.props.post_view.creator.id,
-        true
-      );
-    } else {
-      return false;
-    }
-  }
-
-  get canAdmin(): boolean {
-    return (
-      this.props.admins &&
-      canMod(
-        UserService.Instance.myUserInfo,
-        this.props.admins.map(a => a.person.id),
-        this.props.post_view.creator.id
-      )
-    );
-  }
-
-  get amCommunityCreator(): boolean {
-    return (
-      this.props.moderators &&
-      UserService.Instance.myUserInfo &&
-      this.props.post_view.creator.id !=
-        UserService.Instance.myUserInfo.local_user_view.person.id &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        this.props.moderators[0].moderator.id
-    );
-  }
-
-  get amSiteCreator(): boolean {
-    return (
-      this.props.admins &&
-      UserService.Instance.myUserInfo &&
-      this.props.post_view.creator.id !=
-        UserService.Instance.myUserInfo.local_user_view.person.id &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        this.props.admins[0].person.id
-    );
+    return UserService.Instance.myUserInfo.match({
+      some: mui =>
+        this.props.post_view.creator.id == mui.local_user_view.person.id,
+      none: false,
+    });
   }
 
   handlePostLike(i: PostListing, event: any) {
     event.preventDefault();
-    if (!UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isNone()) {
       this.context.router.history.push(`/login`);
     }
 
-    let new_vote = i.state.my_vote == 1 ? 0 : 1;
+    let myVote = this.state.my_vote.unwrapOr(0);
+    let newVote = myVote == 1 ? 0 : 1;
 
-    if (i.state.my_vote == 1) {
+    if (myVote == 1) {
       i.state.score--;
       i.state.upvotes--;
-    } else if (i.state.my_vote == -1) {
+    } else if (myVote == -1) {
       i.state.downvotes--;
       i.state.upvotes++;
       i.state.score += 2;
@@ -1265,13 +1208,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       i.state.score++;
     }
 
-    i.state.my_vote = new_vote;
+    i.state.my_vote = Some(newVote);
 
-    let form: CreatePostLike = {
+    let form = new CreatePostLike({
       post_id: i.props.post_view.post.id,
-      score: i.state.my_vote,
-      auth: authField(),
-    };
+      score: newVote,
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.likePost(form));
     i.setState(i.state);
@@ -1280,17 +1223,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handlePostDisLike(i: PostListing, event: any) {
     event.preventDefault();
-    if (!UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isNone()) {
       this.context.router.history.push(`/login`);
     }
 
-    let new_vote = i.state.my_vote == -1 ? 0 : -1;
+    let myVote = this.state.my_vote.unwrapOr(0);
+    let newVote = myVote == -1 ? 0 : -1;
 
-    if (i.state.my_vote == 1) {
+    if (myVote == 1) {
       i.state.score -= 2;
       i.state.upvotes--;
       i.state.downvotes++;
-    } else if (i.state.my_vote == -1) {
+    } else if (myVote == -1) {
       i.state.downvotes--;
       i.state.score++;
     } else {
@@ -1298,13 +1242,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       i.state.score--;
     }
 
-    i.state.my_vote = new_vote;
+    i.state.my_vote = Some(newVote);
 
-    let form: CreatePostLike = {
+    let form = new CreatePostLike({
       post_id: i.props.post_view.post.id,
-      score: i.state.my_vote,
-      auth: authField(),
-    };
+      score: newVote,
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.likePost(form));
     i.setState(i.state);
@@ -1333,17 +1277,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleReportReasonChange(i: PostListing, event: any) {
-    i.state.reportReason = event.target.value;
+    i.state.reportReason = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleReportSubmit(i: PostListing, event: any) {
     event.preventDefault();
-    let form: CreatePostReport = {
+    let form = new CreatePostReport({
       post_id: i.props.post_view.post.id,
-      reason: i.state.reportReason,
-      auth: authField(),
-    };
+      reason: toUndefined(i.state.reportReason),
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.createPostReport(form));
 
     i.state.showReportDialog = false;
@@ -1351,31 +1295,31 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleBlockUserClick(i: PostListing) {
-    let blockUserForm: BlockPerson = {
+    let blockUserForm = new BlockPerson({
       person_id: i.props.post_view.creator.id,
       block: true,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
   }
 
   handleDeleteClick(i: PostListing) {
-    let deleteForm: DeletePost = {
+    let deleteForm = new DeletePost({
       post_id: i.props.post_view.post.id,
       deleted: !i.props.post_view.post.deleted,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
   }
 
   handleSavePostClick(i: PostListing) {
     let saved =
       i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
-    let form: SavePost = {
+    let form = new SavePost({
       post_id: i.props.post_view.post.id,
       save: saved,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
 
     WebSocketService.Instance.send(wsClient.savePost(form));
   }
@@ -1384,10 +1328,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     let post = this.props.post_view.post;
     let params = `?title=${encodeURIComponent(post.name)}`;
 
-    if (post.url) {
-      params += `&url=${encodeURIComponent(post.url)}`;
+    if (post.url.isSome()) {
+      params += `&url=${encodeURIComponent(post.url.unwrap())}`;
     }
-    if (post.body) {
+    if (post.body.isSome()) {
       params += `&body=${encodeURIComponent(this.crossPostBody())}`;
     }
     return params;
@@ -1395,9 +1339,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   crossPostBody(): string {
     let post = this.props.post_view.post;
-    let body = `${i18n.t("cross_posted_from")} ${
-      post.ap_id
-    }\n\n${post.body.replace(/^/gm, "> ")}`;
+    let body = `${i18n.t("cross_posted_from")} ${post.ap_id}\n\n${post.body
+      .unwrap()
+      .replace(/^/gm, "> ")}`;
     return body;
   }
 
@@ -1412,7 +1356,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleModRemoveReasonChange(i: PostListing, event: any) {
-    i.state.removeReason = event.target.value;
+    i.state.removeReason = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -1423,12 +1367,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handleModRemoveSubmit(i: PostListing, event: any) {
     event.preventDefault();
-    let form: RemovePost = {
+    let form = new RemovePost({
       post_id: i.props.post_view.post.id,
       removed: !i.props.post_view.post.removed,
       reason: i.state.removeReason,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.removePost(form));
 
     i.state.showRemoveDialog = false;
@@ -1436,20 +1380,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleModLock(i: PostListing) {
-    let form: LockPost = {
+    let form = new LockPost({
       post_id: i.props.post_view.post.id,
       locked: !i.props.post_view.post.locked,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.lockPost(form));
   }
 
   handleModSticky(i: PostListing) {
-    let form: StickyPost = {
+    let form = new StickyPost({
       post_id: i.props.post_view.post.id,
       stickied: !i.props.post_view.post.stickied,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.stickyPost(form));
   }
 
@@ -1468,12 +1412,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleModBanReasonChange(i: PostListing, event: any) {
-    i.state.banReason = event.target.value;
+    i.state.banReason = Some(event.target.value);
     i.setState(i.state);
   }
 
   handleModBanExpireDaysChange(i: PostListing, event: any) {
-    i.state.banExpireDays = event.target.value;
+    i.state.banExpireDays = Some(event.target.value);
     i.setState(i.state);
   }
 
@@ -1498,15 +1442,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       if (ban == false) {
         i.state.removeData = false;
       }
-      let form: BanFromCommunity = {
+      let form = new BanFromCommunity({
         person_id: i.props.post_view.creator.id,
         community_id: i.props.post_view.community.id,
         ban,
-        remove_data: i.state.removeData,
+        remove_data: Some(i.state.removeData),
         reason: i.state.banReason,
-        expires: futureDaysToUnixTime(i.state.banExpireDays),
-        auth: authField(),
-      };
+        expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.banFromCommunity(form));
     } else {
       // If its an unban, restore all their data
@@ -1514,14 +1458,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       if (ban == false) {
         i.state.removeData = false;
       }
-      let form: BanPerson = {
+      let form = new BanPerson({
         person_id: i.props.post_view.creator.id,
         ban,
-        remove_data: i.state.removeData,
+        remove_data: Some(i.state.removeData),
         reason: i.state.banReason,
-        expires: futureDaysToUnixTime(i.state.banExpireDays),
-        auth: authField(),
-      };
+        expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.banPerson(form));
     }
 
@@ -1530,22 +1474,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleAddModToCommunity(i: PostListing) {
-    let form: AddModToCommunity = {
+    let form = new AddModToCommunity({
       person_id: i.props.post_view.creator.id,
       community_id: i.props.post_view.community.id,
-      added: !i.creatorIsMod,
-      auth: authField(),
-    };
+      added: !i.creatorIsMod_,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
     i.setState(i.state);
   }
 
   handleAddAdmin(i: PostListing) {
-    let form: AddAdmin = {
+    let form = new AddAdmin({
       person_id: i.props.post_view.creator.id,
-      added: !i.creatorIsAdmin,
-      auth: authField(),
-    };
+      added: !i.creatorIsAdmin_,
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.addAdmin(form));
     i.setState(i.state);
   }
@@ -1561,11 +1505,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleTransferCommunity(i: PostListing) {
-    let form: TransferCommunity = {
+    let form = new TransferCommunity({
       community_id: i.props.post_view.community.id,
       person_id: i.props.post_view.creator.id,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.transferCommunity(form));
     i.state.showConfirmTransferCommunity = false;
     i.setState(i.state);
@@ -1630,4 +1574,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
     return `${points} â€¢ ${upvotes} â€¢ ${downvotes}`;
   }
+
+  get canModOnSelf_(): boolean {
+    return canMod(
+      this.props.moderators,
+      this.props.admins,
+      this.props.post_view.creator.id,
+      undefined,
+      true
+    );
+  }
+
+  get canMod_(): boolean {
+    return canMod(
+      this.props.moderators,
+      this.props.admins,
+      this.props.post_view.creator.id
+    );
+  }
+
+  get canAdmin_(): boolean {
+    return canAdmin(this.props.admins, this.props.post_view.creator.id);
+  }
+
+  get creatorIsMod_(): boolean {
+    return isMod(this.props.moderators, this.props.post_view.creator.id);
+  }
+
+  get creatorIsAdmin_(): boolean {
+    return isAdmin(this.props.admins, this.props.post_view.creator.id);
+  }
 }
index 8bdb5698a334dcce70c79524bfefb718a84dd97e..a26d9d6d4483b3731753d16dcb11e1bfe2f81478 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Some } from "@sniptt/monads";
 import { Component } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { Link } from "inferno-router";
@@ -13,39 +14,30 @@ interface PostListingsProps {
   enableNsfw: boolean;
 }
 
-interface PostListingsState {
-  posts: PostView[];
-}
-
-export class PostListings extends Component<
-  PostListingsProps,
-  PostListingsState
-> {
+export class PostListings extends Component<PostListingsProps, any> {
   duplicatesMap = new Map<number, PostView[]>();
 
-  private emptyState: PostListingsState = {
-    posts: [],
-  };
-
   constructor(props: any, context: any) {
     super(props, context);
-    this.state = this.emptyState;
-    if (this.props.removeDuplicates) {
-      this.state.posts = this.removeDuplicates();
-    } else {
-      this.state.posts = this.props.posts;
-    }
+  }
+
+  get posts() {
+    return this.props.removeDuplicates
+      ? this.removeDuplicates()
+      : this.props.posts;
   }
 
   render() {
     return (
       <div>
-        {this.state.posts.length > 0 ? (
-          this.state.posts.map(post_view => (
+        {this.posts.length > 0 ? (
+          this.posts.map(post_view => (
             <>
               <PostListing
                 post_view={post_view}
-                duplicates={this.duplicatesMap.get(post_view.post.id)}
+                duplicates={Some(this.duplicatesMap.get(post_view.post.id))}
+                moderators={None}
+                admins={None}
                 showCommunity={this.props.showCommunity}
                 enableDownvotes={this.props.enableDownvotes}
                 enableNsfw={this.props.enableNsfw}
@@ -56,7 +48,7 @@ export class PostListings extends Component<
         ) : (
           <>
             <div>{i18n.t("no_posts")}</div>
-            {this.props.showCommunity !== undefined && (
+            {this.props.showCommunity && (
               <T i18nKey="subscribe_to_communities">
                 #<Link to="/communities">#</Link>
               </T>
@@ -76,19 +68,20 @@ export class PostListings extends Component<
 
     // Loop over the posts, find ones with same urls
     for (let pv of posts) {
-      if (
-        pv.post.url &&
-        !pv.post.deleted &&
+      !pv.post.deleted &&
         !pv.post.removed &&
         !pv.community.deleted &&
-        !pv.community.removed
-      ) {
-        if (!urlMap.get(pv.post.url)) {
-          urlMap.set(pv.post.url, [pv]);
-        } else {
-          urlMap.get(pv.post.url).push(pv);
-        }
-      }
+        !pv.community.removed &&
+        pv.post.url.match({
+          some: url => {
+            if (!urlMap.get(url)) {
+              urlMap.set(url, [pv]);
+            } else {
+              urlMap.get(url).push(pv);
+            }
+          },
+          none: void 0,
+        });
     }
 
     // Sort by oldest
@@ -103,19 +96,22 @@ export class PostListings extends Component<
 
     for (let i = 0; i < posts.length; i++) {
       let pv = posts[i];
-      if (pv.post.url) {
-        let found = urlMap.get(pv.post.url);
-        if (found) {
-          // If its the oldest, add
-          if (pv.post.id == found[0].post.id) {
-            this.duplicatesMap.set(pv.post.id, found.slice(1));
+      pv.post.url.match({
+        some: url => {
+          let found = urlMap.get(url);
+          if (found) {
+            // If its the oldest, add
+            if (pv.post.id == found[0].post.id) {
+              this.duplicatesMap.set(pv.post.id, found.slice(1));
+            }
+            // Otherwise, delete it
+            else {
+              posts.splice(i--, 1);
+            }
           }
-          // Otherwise, delete it
-          else {
-            posts.splice(i--, 1);
-          }
-        }
-      }
+        },
+        none: void 0,
+      });
     }
 
     return posts;
index 9a8005599bfd1dd2686ccf363e21e37da26dbafa..18487613cc1138df56717ce44597ab7344e873ed 100644 (file)
@@ -1,9 +1,10 @@
+import { None } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
-import { authField, wsClient } from "../../utils";
+import { auth, wsClient } from "../../utils";
 import { Icon } from "../common/icon";
 import { PersonListing } from "../person/person-listing";
 import { PostListing } from "./post-listing";
@@ -45,6 +46,9 @@ export class PostReport extends Component<PostReportProps, any> {
       <div>
         <PostListing
           post_view={pv}
+          duplicates={None}
+          moderators={None}
+          admins={None}
           showCommunity={true}
           enableDownvotes={true}
           enableNsfw={true}
@@ -56,21 +60,24 @@ export class PostReport extends Component<PostReportProps, any> {
         <div>
           {i18n.t("reason")}: {r.post_report.reason}
         </div>
-        {r.resolver && (
-          <div>
-            {r.post_report.resolved ? (
-              <T i18nKey="resolved_by">
-                #
-                <PersonListing person={r.resolver} />
-              </T>
-            ) : (
-              <T i18nKey="unresolved_by">
-                #
-                <PersonListing person={r.resolver} />
-              </T>
-            )}
-          </div>
-        )}
+        {r.resolver.match({
+          some: resolver => (
+            <div>
+              {r.post_report.resolved ? (
+                <T i18nKey="resolved_by">
+                  #
+                  <PersonListing person={resolver} />
+                </T>
+              ) : (
+                <T i18nKey="unresolved_by">
+                  #
+                  <PersonListing person={resolver} />
+                </T>
+              )}
+            </div>
+          ),
+          none: <></>,
+        })}
         <button
           className="btn btn-link btn-animate text-muted py-0"
           onClick={linkEvent(this, this.handleResolveReport)}
@@ -89,11 +96,11 @@ export class PostReport extends Component<PostReportProps, any> {
   }
 
   handleResolveReport(i: PostReport) {
-    let form: ResolvePostReport = {
+    let form = new ResolvePostReport({
       report_id: i.props.report.post_report.id,
       resolved: !i.props.report.post_report.resolved,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.resolvePostReport(form));
   }
 }
index 5d15e832fab318c355c574ee76aa9a930c1871c2..00d2789973f755355f42daa4f7b7ffef7ff5da6c 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Right, Some } from "@sniptt/monads";
 import autosize from "autosize";
 import { Component, createRef, linkEvent, RefObject } from "inferno";
 import {
@@ -23,6 +24,8 @@ import {
   SearchType,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
@@ -34,13 +37,15 @@ import {
 } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   buildCommentsTree,
   commentsToFlatNodes,
   createCommentLikeRes,
   createPostLikeRes,
   debounce,
   editCommentRes,
+  enableDownvotes,
+  enableNsfw,
   getCommentIdFromProps,
   getIdFromProps,
   insertCommentIntoTree,
@@ -50,14 +55,12 @@ import {
   saveCommentRes,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   setupTippy,
   toast,
+  trendingFetchLimit,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { CommentForm } from "../comment/comment-form";
 import { CommentNodes } from "../comment/comment-nodes";
@@ -69,7 +72,7 @@ import { PostListing } from "./post-listing";
 const commentsShownInterval = 15;
 
 interface PostState {
-  postRes: GetPostResponse;
+  postRes: Option<GetPostResponse>;
   postId: number;
   commentTree: CommentNodeI[];
   commentId?: number;
@@ -77,7 +80,7 @@ interface PostState {
   commentViewType: CommentViewType;
   scrolled?: boolean;
   loading: boolean;
-  crossPosts: PostView[];
+  crossPosts: Option<PostView[]>;
   siteRes: GetSiteResponse;
   commentSectionRef?: RefObject<HTMLDivElement>;
   showSidebarMobile: boolean;
@@ -86,10 +89,10 @@ interface PostState {
 
 export class Post extends Component<any, PostState> {
   private subscription: Subscription;
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, GetPostResponse);
   private commentScrollDebounced: () => void;
   private emptyState: PostState = {
-    postRes: null,
+    postRes: None,
     postId: getIdFromProps(this.props),
     commentTree: [],
     commentId: getCommentIdFromProps(this.props),
@@ -97,7 +100,7 @@ export class Post extends Component<any, PostState> {
     commentViewType: CommentViewType.Tree,
     scrolled: false,
     loading: true,
-    crossPosts: [],
+    crossPosts: None,
     siteRes: this.isoData.site_res,
     commentSectionRef: null,
     showSidebarMobile: false,
@@ -115,14 +118,24 @@ export class Post extends Component<any, PostState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.postRes = this.isoData.routeData[0];
+      this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse);
       this.state.commentTree = buildCommentsTree(
-        this.state.postRes.comments,
+        this.state.postRes.unwrap().comments,
         this.state.commentSort
       );
       this.state.loading = false;
 
       if (isBrowser()) {
+        WebSocketService.Instance.send(
+          wsClient.communityJoin({
+            community_id:
+              this.state.postRes.unwrap().community_view.community.id,
+          })
+        );
+        WebSocketService.Instance.send(
+          wsClient.postJoin({ post_id: this.state.postId })
+        );
+
         this.fetchCrossPosts();
         if (this.state.commentId) {
           this.scrollCommentIntoView();
@@ -138,42 +151,47 @@ export class Post extends Component<any, PostState> {
   }
 
   fetchPost() {
-    let form: GetPost = {
+    let form = new GetPost({
       id: this.state.postId,
-      auth: authField(false),
-    };
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getPost(form));
   }
 
   fetchCrossPosts() {
-    if (this.state.postRes.post_view.post.url) {
-      let form: Search = {
-        q: this.state.postRes.post_view.post.url,
-        type_: SearchType.Url,
-        sort: SortType.TopAll,
-        listing_type: ListingType.All,
-        page: 1,
-        limit: 6,
-        auth: authField(false),
-      };
-      WebSocketService.Instance.send(wsClient.search(form));
-    }
+    this.state.postRes
+      .andThen(r => r.post_view.post.url)
+      .match({
+        some: url => {
+          let form = new Search({
+            q: url,
+            type_: Some(SearchType.Url),
+            sort: Some(SortType.TopAll),
+            listing_type: Some(ListingType.All),
+            page: Some(1),
+            limit: Some(trendingFetchLimit),
+            community_id: None,
+            community_name: None,
+            creator_id: None,
+            auth: auth(false).ok(),
+          });
+          WebSocketService.Instance.send(wsClient.search(form));
+        },
+        none: void 0,
+      });
   }
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let pathSplit = req.path.split("/");
-    let promises: Promise<any>[] = [];
 
     let id = Number(pathSplit[2]);
 
-    let postForm: GetPost = {
+    let postForm = new GetPost({
       id,
-    };
-    setOptionalAuth(postForm, req.auth);
-
-    promises.push(req.client.getPost(postForm));
+      auth: req.auth,
+    });
 
-    return promises;
+    return [req.client.getPost(postForm)];
   }
 
   componentWillUnmount() {
@@ -185,9 +203,6 @@ export class Post extends Component<any, PostState> {
   }
 
   componentDidMount() {
-    WebSocketService.Instance.send(
-      wsClient.postJoin({ post_id: this.state.postId })
-    );
     autosize(document.querySelectorAll("textarea"));
 
     this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
@@ -231,34 +246,38 @@ export class Post extends Component<any, PostState> {
 
   // TODO this needs some re-work
   markScrolledAsRead(commentId: number) {
-    let found = this.state.postRes.comments.find(
-      c => c.comment.id == commentId
-    );
-    let parent = this.state.postRes.comments.find(
-      c => found.comment.parent_id == c.comment.id
-    );
-    let parent_person_id = parent
-      ? parent.creator.id
-      : this.state.postRes.post_view.creator.id;
-
-    if (
-      UserService.Instance.myUserInfo &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        parent_person_id
-    ) {
-      let form: MarkCommentAsRead = {
-        comment_id: found.comment.id,
-        read: true,
-        auth: authField(),
-      };
-      WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
-      UserService.Instance.unreadInboxCountSub.next(
-        UserService.Instance.unreadInboxCountSub.value - 1
-      );
-    }
+    this.state.postRes.match({
+      some: res => {
+        let found = res.comments.find(c => c.comment.id == commentId);
+        let parent = res.comments.find(
+          c => found.comment.parent_id.unwrapOr(0) == c.comment.id
+        );
+        let parent_person_id = parent
+          ? parent.creator.id
+          : res.post_view.creator.id;
+
+        UserService.Instance.myUserInfo.match({
+          some: mui => {
+            if (mui.local_user_view.person.id == parent_person_id) {
+              let form = new MarkCommentAsRead({
+                comment_id: found.comment.id,
+                read: true,
+                auth: auth().unwrap(),
+              });
+              WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
+              UserService.Instance.unreadInboxCountSub.next(
+                UserService.Instance.unreadInboxCountSub.value - 1
+              );
+            }
+          },
+          none: void 0,
+        });
+      },
+      none: void 0,
+    });
   }
 
-  isBottom(el: Element) {
+  isBottom(el: Element): boolean {
     return el?.getBoundingClientRect().bottom <= window.innerHeight;
   }
 
@@ -274,23 +293,35 @@ export class Post extends Component<any, PostState> {
   };
 
   get documentTitle(): string {
-    return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`;
+    return this.state.postRes.match({
+      some: res =>
+        this.state.siteRes.site_view.match({
+          some: siteView =>
+            `${res.post_view.post.name} - ${siteView.site.name}`,
+          none: "",
+        }),
+      none: "",
+    });
   }
 
-  get imageTag(): string {
-    let post = this.state.postRes.post_view.post;
-    return (
-      post.thumbnail_url ||
-      (post.url ? (isImage(post.url) ? post.url : undefined) : undefined)
-    );
+  get imageTag(): Option<string> {
+    return this.state.postRes.match({
+      some: res =>
+        res.post_view.post.thumbnail_url.or(
+          res.post_view.post.url.match({
+            some: url => (isImage(url) ? Some(url) : None),
+            none: None,
+          })
+        ),
+      none: None,
+    });
   }
 
-  get descriptionTag(): string {
-    return this.state.postRes.post_view.post.body;
+  get descriptionTag(): Option<string> {
+    return this.state.postRes.andThen(r => r.post_view.post.body);
   }
 
   render() {
-    let pv = this.state.postRes?.post_view;
     return (
       <div class="container">
         {this.state.loading ? (
@@ -298,56 +329,59 @@ export class Post extends Component<any, PostState> {
             <Spinner large />
           </h5>
         ) : (
-          <div class="row">
-            <div class="col-12 col-md-8 mb-3">
-              <HtmlTags
-                title={this.documentTitle}
-                path={this.context.router.route.match.url}
-                image={this.imageTag}
-                description={this.descriptionTag}
-              />
-              <PostListing
-                post_view={pv}
-                duplicates={this.state.crossPosts}
-                showBody
-                showCommunity
-                moderators={this.state.postRes.moderators}
-                admins={this.state.siteRes.admins}
-                enableDownvotes={
-                  this.state.siteRes.site_view.site.enable_downvotes
-                }
-                enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
-              />
-              <div ref={this.state.commentSectionRef} className="mb-2" />
-              <CommentForm
-                postId={this.state.postId}
-                disabled={pv.post.locked}
-              />
-              <div class="d-block d-md-none">
-                <button
-                  class="btn btn-secondary d-inline-block mb-2 mr-3"
-                  onClick={linkEvent(this, this.handleShowSidebarMobile)}
-                >
-                  {i18n.t("sidebar")}{" "}
-                  <Icon
-                    icon={
-                      this.state.showSidebarMobile
-                        ? `minus-square`
-                        : `plus-square`
-                    }
-                    classes="icon-inline"
+          this.state.postRes.match({
+            some: res => (
+              <div class="row">
+                <div class="col-12 col-md-8 mb-3">
+                  <HtmlTags
+                    title={this.documentTitle}
+                    path={this.context.router.route.match.url}
+                    image={this.imageTag}
+                    description={this.descriptionTag}
+                  />
+                  <PostListing
+                    post_view={res.post_view}
+                    duplicates={this.state.crossPosts}
+                    showBody
+                    showCommunity
+                    moderators={Some(res.moderators)}
+                    admins={Some(this.state.siteRes.admins)}
+                    enableDownvotes={enableDownvotes(this.state.siteRes)}
+                    enableNsfw={enableNsfw(this.state.siteRes)}
                   />
-                </button>
-                {this.state.showSidebarMobile && this.sidebar()}
+                  <div ref={this.state.commentSectionRef} className="mb-2" />
+                  <CommentForm
+                    node={Right(this.state.postId)}
+                    disabled={res.post_view.post.locked}
+                  />
+                  <div class="d-block d-md-none">
+                    <button
+                      class="btn btn-secondary d-inline-block mb-2 mr-3"
+                      onClick={linkEvent(this, this.handleShowSidebarMobile)}
+                    >
+                      {i18n.t("sidebar")}{" "}
+                      <Icon
+                        icon={
+                          this.state.showSidebarMobile
+                            ? `minus-square`
+                            : `plus-square`
+                        }
+                        classes="icon-inline"
+                      />
+                    </button>
+                    {this.state.showSidebarMobile && this.sidebar()}
+                  </div>
+                  {res.comments.length > 0 && this.sortRadios()}
+                  {this.state.commentViewType == CommentViewType.Tree &&
+                    this.commentsTree()}
+                  {this.state.commentViewType == CommentViewType.Chat &&
+                    this.commentsFlat()}
+                </div>
+                <div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
               </div>
-              {this.state.postRes.comments.length > 0 && this.sortRadios()}
-              {this.state.commentViewType == CommentViewType.Tree &&
-                this.commentsTree()}
-              {this.state.commentViewType == CommentViewType.Chat &&
-                this.commentsFlat()}
-            </div>
-            <div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
-          </div>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
@@ -431,43 +465,48 @@ export class Post extends Component<any, PostState> {
 
   commentsFlat() {
     // These are already sorted by new
-    return (
-      <div>
-        <CommentNodes
-          nodes={commentsToFlatNodes(this.state.postRes.comments)}
-          maxCommentsShown={this.state.maxCommentsShown}
-          noIndent
-          locked={this.state.postRes.post_view.post.locked}
-          moderators={this.state.postRes.moderators}
-          admins={this.state.siteRes.admins}
-          postCreatorId={this.state.postRes.post_view.creator.id}
-          showContext
-          enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
-        />
-      </div>
-    );
+    return this.state.postRes.match({
+      some: res => (
+        <div>
+          <CommentNodes
+            nodes={commentsToFlatNodes(res.comments)}
+            maxCommentsShown={Some(this.state.maxCommentsShown)}
+            noIndent
+            locked={res.post_view.post.locked}
+            moderators={Some(res.moderators)}
+            admins={Some(this.state.siteRes.admins)}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
+            showContext
+          />
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   sidebar() {
-    return (
-      <div class="mb-3">
-        <Sidebar
-          community_view={this.state.postRes.community_view}
-          moderators={this.state.postRes.moderators}
-          admins={this.state.siteRes.admins}
-          online={this.state.postRes.online}
-          enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
-          showIcon
-        />
-      </div>
-    );
+    return this.state.postRes.match({
+      some: res => (
+        <div class="mb-3">
+          <Sidebar
+            community_view={res.community_view}
+            moderators={res.moderators}
+            admins={this.state.siteRes.admins}
+            online={res.online}
+            enableNsfw={enableNsfw(this.state.siteRes)}
+            showIcon
+          />
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   handleCommentSortChange(i: Post, event: any) {
     i.state.commentSort = Number(event.target.value);
     i.state.commentViewType = CommentViewType.Tree;
     i.state.commentTree = buildCommentsTree(
-      i.state.postRes.comments,
+      i.state.postRes.map(r => r.comments).unwrapOr([]),
       i.state.commentSort
     );
     i.setState(i.state);
@@ -477,7 +516,7 @@ export class Post extends Component<any, PostState> {
     i.state.commentViewType = Number(event.target.value);
     i.state.commentSort = CommentSortType.New;
     i.state.commentTree = buildCommentsTree(
-      i.state.postRes.comments,
+      i.state.postRes.map(r => r.comments).unwrapOr([]),
       i.state.commentSort
     );
     i.setState(i.state);
@@ -489,19 +528,21 @@ export class Post extends Component<any, PostState> {
   }
 
   commentsTree() {
-    return (
-      <div>
-        <CommentNodes
-          nodes={this.state.commentTree}
-          maxCommentsShown={this.state.maxCommentsShown}
-          locked={this.state.postRes.post_view.post.locked}
-          moderators={this.state.postRes.moderators}
-          admins={this.state.siteRes.admins}
-          postCreatorId={this.state.postRes.post_view.creator.id}
-          enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
-        />
-      </div>
-    );
+    return this.state.postRes.match({
+      some: res => (
+        <div>
+          <CommentNodes
+            nodes={this.state.commentTree}
+            maxCommentsShown={Some(this.state.maxCommentsShown)}
+            locked={res.post_view.post.locked}
+            moderators={Some(res.moderators)}
+            admins={Some(this.state.siteRes.admins)}
+            enableDownvotes={enableDownvotes(this.state.siteRes)}
+          />
+        </div>
+      ),
+      none: <></>,
+    });
   }
 
   parseMessage(msg: any) {
@@ -516,18 +557,29 @@ export class Post extends Component<any, PostState> {
       WebSocketService.Instance.send(
         wsClient.getPost({
           id: postId,
-          auth: authField(false),
+          auth: auth(false).ok(),
         })
       );
     } else if (op == UserOperation.GetPost) {
-      let data = wsJsonToRes<GetPostResponse>(msg).data;
-      this.state.postRes = data;
+      let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
+      this.state.postRes = Some(data);
+
       this.state.commentTree = buildCommentsTree(
-        this.state.postRes.comments,
+        this.state.postRes.map(r => r.comments).unwrapOr([]),
         this.state.commentSort
       );
       this.state.loading = false;
 
+      // join the rooms
+      WebSocketService.Instance.send(
+        wsClient.postJoin({ post_id: this.state.postId })
+      );
+      WebSocketService.Instance.send(
+        wsClient.communityJoin({
+          community_id: data.community_view.community.id,
+        })
+      );
+
       // Get cross-posts
       this.fetchCrossPosts();
       this.setState(this.state);
@@ -542,18 +594,25 @@ export class Post extends Component<any, PostState> {
         this.scrollCommentIntoView();
       }
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
 
       // Don't get comments from the post room, if the creator is blocked
-      let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
+      let creatorBlocked = UserService.Instance.myUserInfo
+        .map(m => m.person_blocks)
+        .unwrapOr([])
         .map(pb => pb.target.id)
         .includes(data.comment_view.creator.id);
 
       // Necessary since it might be a user reply, which has the recipients, to avoid double
       if (data.recipient_ids.length == 0 && !creatorBlocked) {
-        this.state.postRes.comments.unshift(data.comment_view);
-        insertCommentIntoTree(this.state.commentTree, data.comment_view);
-        this.state.postRes.post_view.counts.comments++;
+        this.state.postRes.match({
+          some: res => {
+            res.comments.unshift(data.comment_view);
+            insertCommentIntoTree(this.state.commentTree, data.comment_view);
+            res.post_view.counts.comments++;
+          },
+          none: void 0,
+        });
         this.setState(this.state);
         setupTippy();
       }
@@ -562,21 +621,33 @@ export class Post extends Component<any, PostState> {
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      editCommentRes(data.comment_view, this.state.postRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      editCommentRes(
+        data.comment_view,
+        this.state.postRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      saveCommentRes(data.comment_view, this.state.postRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      saveCommentRes(
+        data.comment_view,
+        this.state.postRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
       setupTippy();
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      createCommentLikeRes(data.comment_view, this.state.postRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      createCommentLikeRes(
+        data.comment_view,
+        this.state.postRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      createPostLikeRes(data.post_view, this.state.postRes.post_view);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      this.state.postRes.match({
+        some: res => createPostLikeRes(data.post_view, res.post_view),
+        none: void 0,
+      });
       this.setState(this.state);
     } else if (
       op == UserOperation.EditPost ||
@@ -586,8 +657,11 @@ export class Post extends Component<any, PostState> {
       op == UserOperation.StickyPost ||
       op == UserOperation.SavePost
     ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      this.state.postRes.post_view = data.post_view;
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      this.state.postRes.match({
+        some: res => (res.post_view = data.post_view),
+        none: void 0,
+      });
       this.setState(this.state);
       setupTippy();
     } else if (
@@ -596,68 +670,94 @@ export class Post extends Component<any, PostState> {
       op == UserOperation.RemoveCommunity ||
       op == UserOperation.FollowCommunity
     ) {
-      let data = wsJsonToRes<CommunityResponse>(msg).data;
-      this.state.postRes.community_view = data.community_view;
-      this.state.postRes.post_view.community = data.community_view.community;
-      this.setState(this.state);
-      this.setState(this.state);
+      let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
+      this.state.postRes.match({
+        some: res => {
+          res.community_view = data.community_view;
+          res.post_view.community = data.community_view.community;
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.BanFromCommunity) {
-      let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
-      this.state.postRes.comments
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator_banned_from_community = data.banned));
-      if (
-        this.state.postRes.post_view.creator.id == data.person_view.person.id
-      ) {
-        this.state.postRes.post_view.creator_banned_from_community =
-          data.banned;
-      }
-      this.setState(this.state);
+      let data = wsJsonToRes<BanFromCommunityResponse>(
+        msg,
+        BanFromCommunityResponse
+      );
+      this.state.postRes.match({
+        some: res => {
+          res.comments
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator_banned_from_community = data.banned));
+          if (res.post_view.creator.id == data.person_view.person.id) {
+            res.post_view.creator_banned_from_community = data.banned;
+          }
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.AddModToCommunity) {
-      let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
-      this.state.postRes.moderators = data.moderators;
-      this.setState(this.state);
+      let data = wsJsonToRes<AddModToCommunityResponse>(
+        msg,
+        AddModToCommunityResponse
+      );
+      this.state.postRes.match({
+        some: res => {
+          res.moderators = data.moderators;
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.BanPerson) {
-      let data = wsJsonToRes<BanPersonResponse>(msg).data;
-      this.state.postRes.comments
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator.banned = data.banned));
-      if (
-        this.state.postRes.post_view.creator.id == data.person_view.person.id
-      ) {
-        this.state.postRes.post_view.creator.banned = data.banned;
-      }
-      this.setState(this.state);
+      let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
+      this.state.postRes.match({
+        some: res => {
+          res.comments
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator.banned = data.banned));
+          if (res.post_view.creator.id == data.person_view.person.id) {
+            res.post_view.creator.banned = data.banned;
+          }
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.AddAdmin) {
-      let data = wsJsonToRes<AddAdminResponse>(msg).data;
+      let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
       this.state.siteRes.admins = data.admins;
       this.setState(this.state);
     } else if (op == UserOperation.Search) {
-      let data = wsJsonToRes<SearchResponse>(msg).data;
-      this.state.crossPosts = data.posts.filter(
+      let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
+      let xPosts = data.posts.filter(
         p => p.post.id != Number(this.props.match.params.id)
       );
+      this.state.crossPosts = xPosts.length > 0 ? Some(xPosts) : None;
       this.setState(this.state);
     } else if (op == UserOperation.LeaveAdmin) {
-      let data = wsJsonToRes<GetSiteResponse>(msg).data;
+      let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
       this.state.siteRes = data;
       this.setState(this.state);
     } else if (op == UserOperation.TransferCommunity) {
-      let data = wsJsonToRes<GetCommunityResponse>(msg).data;
-      this.state.postRes.community_view = data.community_view;
-      this.state.postRes.post_view.community = data.community_view.community;
-      this.state.postRes.moderators = data.moderators;
-      this.setState(this.state);
+      let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
+      this.state.postRes.match({
+        some: res => {
+          res.community_view = data.community_view;
+          res.post_view.community = data.community_view.community;
+          res.moderators = data.moderators;
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
     } else if (op == UserOperation.CreatePostReport) {
-      let data = wsJsonToRes<PostReportResponse>(msg).data;
+      let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
     } else if (op == UserOperation.CreateCommentReport) {
-      let data = wsJsonToRes<CommentReportResponse>(msg).data;
+      let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
       if (data) {
         toast(i18n.t("report_created"));
       }
index b3129f60cbe1c4f5c9cb87278003f641d6644db3..a93ba99ccf42a330a83dce42e90ab5166cb99e03 100644 (file)
@@ -1,34 +1,34 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component } from "inferno";
 import {
   GetPersonDetails,
   GetPersonDetailsResponse,
-  PersonViewSafe,
-  SiteView,
+  GetSiteResponse,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { InitialFetchRequest } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   getRecipientIdFromProps,
   isBrowser,
   setIsoData,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
 import { PrivateMessageForm } from "./private-message-form";
 
 interface CreatePrivateMessageState {
-  site_view: SiteView;
-  recipient: PersonViewSafe;
+  siteRes: GetSiteResponse;
+  recipientDetailsRes: Option<GetPersonDetailsResponse>;
   recipient_id: number;
   loading: boolean;
 }
@@ -37,11 +37,11 @@ export class CreatePrivateMessage extends Component<
   any,
   CreatePrivateMessageState
 > {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, GetPersonDetailsResponse);
   private subscription: Subscription;
   private emptyState: CreatePrivateMessageState = {
-    site_view: this.isoData.site_res.site_view,
-    recipient: undefined,
+    siteRes: this.isoData.site_res,
+    recipientDetailsRes: None,
     recipient_id: getRecipientIdFromProps(this.props),
     loading: true,
   };
@@ -54,14 +54,16 @@ export class CreatePrivateMessage extends Component<
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
 
-    if (!UserService.Instance.myUserInfo) {
+    if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.recipient = this.isoData.routeData[0].user;
+      this.state.recipientDetailsRes = Some(
+        this.isoData.routeData[0] as GetPersonDetailsResponse
+      );
       this.state.loading = false;
     } else {
       this.fetchPersonDetails();
@@ -69,30 +71,40 @@ export class CreatePrivateMessage extends Component<
   }
 
   fetchPersonDetails() {
-    let form: GetPersonDetails = {
-      person_id: this.state.recipient_id,
-      sort: SortType.New,
-      saved_only: false,
-      auth: authField(false),
-    };
+    let form = new GetPersonDetails({
+      person_id: Some(this.state.recipient_id),
+      sort: Some(SortType.New),
+      saved_only: Some(false),
+      username: None,
+      page: None,
+      limit: None,
+      community_id: None,
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
   }
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
-    let person_id = Number(req.path.split("/").pop());
-    let form: GetPersonDetails = {
+    let person_id = Some(Number(req.path.split("/").pop()));
+    let form = new GetPersonDetails({
       person_id,
-      sort: SortType.New,
-      saved_only: false,
+      sort: Some(SortType.New),
+      saved_only: Some(false),
+      username: None,
+      page: None,
+      limit: None,
+      community_id: None,
       auth: req.auth,
-    };
+    });
     return [req.client.getPersonDetails(form)];
   }
 
   get documentTitle(): string {
-    return `${i18n.t("create_private_message")} - ${
-      this.state.site_view.site.name
-    }`;
+    return this.state.recipientDetailsRes.match({
+      some: res =>
+        `${i18n.t("create_private_message")} - ${res.person_view.person.name}`,
+      none: "",
+    });
   }
 
   componentWillUnmount() {
@@ -107,21 +119,29 @@ export class CreatePrivateMessage extends Component<
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         {this.state.loading ? (
           <h5>
             <Spinner large />
           </h5>
         ) : (
-          <div class="row">
-            <div class="col-12 col-lg-6 offset-lg-3 mb-4">
-              <h5>{i18n.t("create_private_message")}</h5>
-              <PrivateMessageForm
-                onCreate={this.handlePrivateMessageCreate}
-                recipient={this.state.recipient.person}
-              />
-            </div>
-          </div>
+          this.state.recipientDetailsRes.match({
+            some: res => (
+              <div class="row">
+                <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+                  <h5>{i18n.t("create_private_message")}</h5>
+                  <PrivateMessageForm
+                    privateMessageView={None}
+                    onCreate={this.handlePrivateMessageCreate}
+                    recipient={res.person_view.person}
+                  />
+                </div>
+              </div>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
@@ -143,8 +163,11 @@ export class CreatePrivateMessage extends Component<
       this.setState(this.state);
       return;
     } else if (op == UserOperation.GetPersonDetails) {
-      let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
-      this.state.recipient = data.person_view;
+      let data = wsJsonToRes<GetPersonDetailsResponse>(
+        msg,
+        GetPersonDetailsResponse
+      );
+      this.state.recipientDetailsRes = Some(data);
       this.state.loading = false;
       this.setState(this.state);
     }
index 4f0e9d1290fb847aaa2b3f4c3b16939c6d6cd6a2..62649816d0ea37b72efd84deadb97a2f96532582 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { T } from "inferno-i18next-dess";
 import { Prompt } from "inferno-router";
@@ -8,21 +9,21 @@ import {
   PrivateMessageResponse,
   PrivateMessageView,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   isBrowser,
   relTags,
   setupTippy,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { Icon, Spinner } from "../common/icon";
 import { MarkdownTextArea } from "../common/markdown-textarea";
@@ -30,7 +31,7 @@ import { PersonListing } from "../person/person-listing";
 
 interface PrivateMessageFormProps {
   recipient: PersonSafe;
-  privateMessage?: PrivateMessageView; // If a pm is given, that means this is an edit
+  privateMessageView: Option<PrivateMessageView>; // If a pm is given, that means this is an edit
   onCancel?(): any;
   onCreate?(message: PrivateMessageView): any;
   onEdit?(message: PrivateMessageView): any;
@@ -49,11 +50,11 @@ export class PrivateMessageForm extends Component<
 > {
   private subscription: Subscription;
   private emptyState: PrivateMessageFormState = {
-    privateMessageForm: {
+    privateMessageForm: new CreatePrivateMessage({
       content: null,
       recipient_id: this.props.recipient.id,
-      auth: authField(),
-    },
+      auth: auth().unwrap(),
+    }),
     loading: false,
     previewMode: false,
     showDisclaimer: false,
@@ -70,10 +71,11 @@ export class PrivateMessageForm extends Component<
     this.subscription = wsSubscribe(this.parseMessage);
 
     // Its an edit
-    if (this.props.privateMessage) {
-      this.state.privateMessageForm.content =
-        this.props.privateMessage.private_message.content;
-    }
+    this.props.privateMessageView.match({
+      some: pm =>
+        (this.state.privateMessageForm.content = pm.private_message.content),
+      none: void 0,
+    });
   }
 
   componentDidMount() {
@@ -103,7 +105,7 @@ export class PrivateMessageForm extends Component<
           message={i18n.t("block_leaving")}
         />
         <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
-          {!this.props.privateMessage && (
+          {this.props.privateMessageView.isNone() && (
             <div class="form-group row">
               <label class="col-sm-2 col-form-label">
                 {capitalizeFirstLetter(i18n.t("to"))}
@@ -128,7 +130,10 @@ export class PrivateMessageForm extends Component<
             </label>
             <div class="col-sm-10">
               <MarkdownTextArea
-                initialContent={this.state.privateMessageForm.content}
+                initialContent={Some(this.state.privateMessageForm.content)}
+                placeholder={None}
+                buttonTitle={None}
+                maxLength={None}
                 onContentChange={this.handleContentChange}
               />
             </div>
@@ -161,13 +166,13 @@ export class PrivateMessageForm extends Component<
               >
                 {this.state.loading ? (
                   <Spinner />
-                ) : this.props.privateMessage ? (
+                ) : this.props.privateMessageView.isSome() ? (
                   capitalizeFirstLetter(i18n.t("save"))
                 ) : (
                   capitalizeFirstLetter(i18n.t("send_message"))
                 )}
               </button>
-              {this.props.privateMessage && (
+              {this.props.privateMessageView.isSome() && (
                 <button
                   type="button"
                   class="btn btn-secondary"
@@ -188,18 +193,19 @@ export class PrivateMessageForm extends Component<
 
   handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
     event.preventDefault();
-    if (i.props.privateMessage) {
-      let form: EditPrivateMessage = {
-        private_message_id: i.props.privateMessage.private_message.id,
-        content: i.state.privateMessageForm.content,
-        auth: authField(),
-      };
-      WebSocketService.Instance.send(wsClient.editPrivateMessage(form));
-    } else {
-      WebSocketService.Instance.send(
+    i.props.privateMessageView.match({
+      some: pm => {
+        let form = new EditPrivateMessage({
+          private_message_id: pm.private_message.id,
+          content: i.state.privateMessageForm.content,
+          auth: auth().unwrap(),
+        });
+        WebSocketService.Instance.send(wsClient.editPrivateMessage(form));
+      },
+      none: WebSocketService.Instance.send(
         wsClient.createPrivateMessage(i.state.privateMessageForm)
-      );
-    }
+      ),
+    });
     i.state.loading = true;
     i.setState(i.state);
   }
@@ -237,11 +243,17 @@ export class PrivateMessageForm extends Component<
       op == UserOperation.DeletePrivateMessage ||
       op == UserOperation.MarkPrivateMessageAsRead
     ) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
       this.state.loading = false;
       this.props.onEdit(data.private_message_view);
     } else if (op == UserOperation.CreatePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
+      let data = wsJsonToRes<PrivateMessageResponse>(
+        msg,
+        PrivateMessageResponse
+      );
       this.state.loading = false;
       this.props.onCreate(data.private_message_view);
       this.setState(this.state);
index 1a7c3b3376aa0552b64aaf80efd66059e398bb8b..57662606518f421cd08e384c3368b28269e28464 100644 (file)
@@ -1,3 +1,4 @@
+import { None, Some } from "@sniptt/monads/build";
 import { Component, linkEvent } from "inferno";
 import {
   DeletePrivateMessage,
@@ -7,7 +8,7 @@ import {
 } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
-import { authField, mdToHtml, toast, wsClient } from "../../utils";
+import { auth, mdToHtml, toast, wsClient } from "../../utils";
 import { Icon } from "../common/icon";
 import { MomentTime } from "../common/moment-time";
 import { PersonListing } from "../person/person-listing";
@@ -46,16 +47,17 @@ export class PrivateMessage extends Component<
   }
 
   get mine(): boolean {
-    return (
-      UserService.Instance.myUserInfo &&
-      UserService.Instance.myUserInfo.local_user_view.person.id ==
-        this.props.private_message_view.creator.id
-    );
+    return UserService.Instance.myUserInfo
+      .map(
+        m =>
+          m.local_user_view.person.id ==
+          this.props.private_message_view.creator.id
+      )
+      .unwrapOr(false);
   }
 
   render() {
     let message_view = this.props.private_message_view;
-    // TODO check this again
     let otherPerson: PersonSafe = this.mine
       ? message_view.recipient
       : message_view.creator;
@@ -73,7 +75,10 @@ export class PrivateMessage extends Component<
             </li>
             <li className="list-inline-item">
               <span>
-                <MomentTime data={message_view.private_message} />
+                <MomentTime
+                  published={message_view.private_message.published}
+                  updated={message_view.private_message.updated}
+                />
               </span>
             </li>
             <li className="list-inline-item">
@@ -93,7 +98,7 @@ export class PrivateMessage extends Component<
           {this.state.showEdit && (
             <PrivateMessageForm
               recipient={otherPerson}
-              privateMessage={message_view}
+              privateMessageView={Some(message_view)}
               onEdit={this.handlePrivateMessageEdit}
               onCreate={this.handlePrivateMessageCreate}
               onCancel={this.handleReplyCancel}
@@ -207,6 +212,7 @@ export class PrivateMessage extends Component<
         {this.state.showReply && (
           <PrivateMessageForm
             recipient={otherPerson}
+            privateMessageView={None}
             onCreate={this.handlePrivateMessageCreate}
           />
         )}
@@ -232,11 +238,11 @@ export class PrivateMessage extends Component<
   }
 
   handleDeleteClick(i: PrivateMessage) {
-    let form: DeletePrivateMessage = {
+    let form = new DeletePrivateMessage({
       private_message_id: i.props.private_message_view.private_message.id,
       deleted: !i.props.private_message_view.private_message.deleted,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.deletePrivateMessage(form));
   }
 
@@ -247,11 +253,11 @@ export class PrivateMessage extends Component<
   }
 
   handleMarkRead(i: PrivateMessage) {
-    let form: MarkPrivateMessageAsRead = {
+    let form = new MarkPrivateMessageAsRead({
       private_message_id: i.props.private_message_view.private_message.id,
       read: !i.props.private_message_view.private_message.read,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form));
   }
 
@@ -271,14 +277,15 @@ export class PrivateMessage extends Component<
   }
 
   handlePrivateMessageCreate(message: PrivateMessageView) {
-    if (
-      UserService.Instance.myUserInfo &&
-      message.creator.id ==
-        UserService.Instance.myUserInfo.local_user_view.person.id
-    ) {
-      this.state.showReply = false;
-      this.setState(this.state);
-      toast(i18n.t("message_sent"));
-    }
+    UserService.Instance.myUserInfo.match({
+      some: mui => {
+        if (message.creator.id == mui.local_user_view.person.id) {
+          this.state.showReply = false;
+          this.setState(this.state);
+          toast(i18n.t("message_sent"));
+        }
+      },
+      none: void 0,
+    });
   }
 }
index 373b5a0574a1d2b6d2fd198f0cc5093bdc7c94fb..b6b0567b7c2778d8e973f2871da6f36c7a1ca2fe 100644 (file)
@@ -1,10 +1,14 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import {
   CommentResponse,
   CommentView,
   CommunityView,
   GetCommunity,
+  GetCommunityResponse,
   GetPersonDetails,
+  GetPersonDetailsResponse,
+  GetSiteResponse,
   ListCommunities,
   ListCommunitiesResponse,
   ListingType,
@@ -16,16 +20,17 @@ import {
   Search as SearchForm,
   SearchResponse,
   SearchType,
-  Site,
   SortType,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { InitialFetchRequest } from "shared/interfaces";
 import { i18n } from "../i18next";
 import { WebSocketService } from "../services";
 import {
-  authField,
+  auth,
   capitalizeFirstLetter,
   choicesConfig,
   commentsToFlatNodes,
@@ -34,6 +39,8 @@ import {
   createCommentLikeRes,
   createPostLikeFindRes,
   debounce,
+  enableDownvotes,
+  enableNsfw,
   fetchCommunities,
   fetchLimit,
   fetchUsers,
@@ -48,13 +55,10 @@ import {
   routeSortTypeToEnum,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   showLocal,
   toast,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../utils";
 import { CommentNodes } from "./comment/comment-nodes";
 import { HtmlTags } from "./common/html-tags";
@@ -89,13 +93,13 @@ interface SearchState {
   communityId: number;
   creatorId: number;
   page: number;
-  searchResponse?: SearchResponse;
+  searchResponse: Option<SearchResponse>;
   communities: CommunityView[];
-  creator?: PersonViewSafe;
+  creatorDetails: Option<GetPersonDetailsResponse>;
   loading: boolean;
-  site: Site;
+  siteRes: GetSiteResponse;
   searchText: string;
-  resolveObjectResponse?: ResolveObjectResponse;
+  resolveObjectResponse: Option<ResolveObjectResponse>;
 }
 
 interface UrlParams {
@@ -115,7 +119,14 @@ interface Combined {
 }
 
 export class Search extends Component<any, SearchState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(
+    this.context,
+    GetCommunityResponse,
+    ListCommunitiesResponse,
+    GetPersonDetailsResponse,
+    SearchResponse,
+    ResolveObjectResponse
+  );
   private communityChoices: any;
   private creatorChoices: any;
   private subscription: Subscription;
@@ -132,10 +143,11 @@ export class Search extends Component<any, SearchState> {
       this.props.match.params.community_id
     ),
     creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
-    searchResponse: null,
-    resolveObjectResponse: null,
+    searchResponse: None,
+    resolveObjectResponse: None,
+    creatorDetails: None,
     loading: true,
-    site: this.isoData.site_res.site_view.site,
+    siteRes: this.isoData.site_res,
     communities: [],
   };
 
@@ -180,20 +192,29 @@ export class Search extends Component<any, SearchState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      let singleOrMultipleCommunities = this.isoData.routeData[0];
-      if (singleOrMultipleCommunities.communities) {
-        this.state.communities = this.isoData.routeData[0].communities;
-      } else {
-        this.state.communities = [this.isoData.routeData[0].community_view];
-      }
+      let communityRes = Some(
+        this.isoData.routeData[0] as GetCommunityResponse
+      );
+      let communitiesRes = Some(
+        this.isoData.routeData[1] as ListCommunitiesResponse
+      );
+
+      // This can be single or multiple communities given
+      this.state.communities = communitiesRes
+        .map(c => c.communities)
+        .unwrapOr([communityRes.map(c => c.community_view).unwrap()]);
+
+      this.state.creatorDetails = Some(
+        this.isoData.routeData[2] as GetPersonDetailsResponse
+      );
 
-      let creator = this.isoData.routeData[1];
-      if (creator?.person_view) {
-        this.state.creator = this.isoData.routeData[1].person_view;
-      }
       if (this.state.q != "") {
-        this.state.searchResponse = this.isoData.routeData[2];
-        this.state.resolveObjectResponse = this.isoData.routeData[3];
+        this.state.searchResponse = Some(
+          this.isoData.routeData[3] as SearchResponse
+        );
+        this.state.resolveObjectResponse = Some(
+          this.isoData.routeData[4] as ResolveObjectResponse
+        );
         this.state.loading = false;
       } else {
         this.search();
@@ -231,12 +252,13 @@ export class Search extends Component<any, SearchState> {
   }
 
   fetchCommunities() {
-    let listCommunitiesForm: ListCommunities = {
-      type_: ListingType.All,
-      sort: SortType.TopAll,
-      limit: fetchLimit,
-      auth: authField(false),
-    };
+    let listCommunitiesForm = new ListCommunities({
+      type_: Some(ListingType.All),
+      sort: Some(SortType.TopAll),
+      limit: Some(fetchLimit),
+      page: None,
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(
       wsClient.listCommunities(listCommunitiesForm)
     );
@@ -247,59 +269,76 @@ export class Search extends Component<any, SearchState> {
     let promises: Promise<any>[] = [];
 
     let communityId = this.getCommunityIdFromProps(pathSplit[11]);
-    if (communityId !== 0) {
-      let getCommunityForm: GetCommunity = {
-        id: communityId,
-      };
-      setOptionalAuth(getCommunityForm, req.auth);
-      promises.push(req.client.getCommunity(getCommunityForm));
-    } else {
-      let listCommunitiesForm: ListCommunities = {
-        type_: ListingType.All,
-        sort: SortType.TopAll,
-        limit: fetchLimit,
-      };
-      setOptionalAuth(listCommunitiesForm, req.auth);
-      promises.push(req.client.listCommunities(listCommunitiesForm));
-    }
+    let community_id: Option<number> =
+      communityId == 0 ? None : Some(communityId);
+    community_id.match({
+      some: id => {
+        let getCommunityForm = new GetCommunity({
+          id: Some(id),
+          name: None,
+          auth: req.auth,
+        });
+        promises.push(req.client.getCommunity(getCommunityForm));
+        promises.push(Promise.resolve());
+      },
+      none: () => {
+        let listCommunitiesForm = new ListCommunities({
+          type_: Some(ListingType.All),
+          sort: Some(SortType.TopAll),
+          limit: Some(fetchLimit),
+          page: None,
+          auth: req.auth,
+        });
+        promises.push(Promise.resolve());
+        promises.push(req.client.listCommunities(listCommunitiesForm));
+      },
+    });
 
     let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
-    if (creatorId !== 0) {
-      let getCreatorForm: GetPersonDetails = {
-        person_id: creatorId,
-      };
-      setOptionalAuth(getCreatorForm, req.auth);
-      promises.push(req.client.getPersonDetails(getCreatorForm));
-    } else {
-      promises.push(Promise.resolve());
-    }
+    let creator_id: Option<number> = creatorId == 0 ? None : Some(creatorId);
+    creator_id.match({
+      some: id => {
+        let getCreatorForm = new GetPersonDetails({
+          person_id: Some(id),
+          username: None,
+          sort: None,
+          page: None,
+          limit: None,
+          community_id: None,
+          saved_only: None,
+          auth: req.auth,
+        });
+        promises.push(req.client.getPersonDetails(getCreatorForm));
+      },
+      none: () => {
+        promises.push(Promise.resolve());
+      },
+    });
 
-    let form: SearchForm = {
+    let form = new SearchForm({
       q: this.getSearchQueryFromProps(pathSplit[3]),
-      type_: this.getSearchTypeFromProps(pathSplit[5]),
-      sort: this.getSortTypeFromProps(pathSplit[7]),
-      listing_type: this.getListingTypeFromProps(pathSplit[9]),
-      page: this.getPageFromProps(pathSplit[15]),
-      limit: fetchLimit,
-    };
-    if (communityId !== 0) {
-      form.community_id = communityId;
-    }
-    if (creatorId !== 0) {
-      form.creator_id = creatorId;
-    }
-    setOptionalAuth(form, req.auth);
+      community_id,
+      community_name: None,
+      creator_id,
+      type_: Some(this.getSearchTypeFromProps(pathSplit[5])),
+      sort: Some(this.getSortTypeFromProps(pathSplit[7])),
+      listing_type: Some(this.getListingTypeFromProps(pathSplit[9])),
+      page: Some(this.getPageFromProps(pathSplit[15])),
+      limit: Some(fetchLimit),
+      auth: req.auth,
+    });
 
-    let resolveObjectForm: ResolveObject = {
+    let resolveObjectForm = new ResolveObject({
       q: this.getSearchQueryFromProps(pathSplit[3]),
-    };
-    setOptionalAuth(resolveObjectForm, req.auth);
+      auth: req.auth,
+    });
 
     if (form.q != "") {
-      //this.state.loading = false;
-      //this.setState(this.state);
       promises.push(req.client.search(form));
       promises.push(req.client.resolveObject(resolveObjectForm));
+    } else {
+      promises.push(Promise.resolve());
+      promises.push(Promise.resolve());
     }
 
     return promises;
@@ -318,19 +357,21 @@ export class Search extends Component<any, SearchState> {
       this.setState({
         loading: true,
         searchText: this.state.q,
-        searchResponse: null,
-        resolveObjectResponse: null,
+        searchResponse: None,
+        resolveObjectResponse: None,
       });
       this.search();
     }
   }
 
   get documentTitle(): string {
-    if (this.state.q) {
-      return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`;
-    } else {
-      return `${i18n.t("search")} - ${this.state.site.name}`;
-    }
+    return this.state.siteRes.site_view.match({
+      some: siteView =>
+        this.state.q
+          ? `${i18n.t("search")} - ${this.state.q} - ${siteView.site.name}`
+          : `${i18n.t("search")} - ${siteView.site.name}`,
+      none: "",
+    });
   }
 
   render() {
@@ -339,6 +380,8 @@ export class Search extends Component<any, SearchState> {
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
+          description={None}
+          image={None}
         />
         <h5>{i18n.t("search")}</h5>
         {this.selects()}
@@ -459,46 +502,52 @@ export class Search extends Component<any, SearchState> {
     let combined: Combined[] = [];
 
     // Push the possible resolve / federated objects first
-    let resolveComment = this.state.resolveObjectResponse?.comment;
-    if (resolveComment) {
-      combined.push(this.commentViewToCombined(resolveComment));
-    }
-    let resolvePost = this.state.resolveObjectResponse?.post;
-    if (resolvePost) {
-      combined.push(this.postViewToCombined(resolvePost));
-    }
-    let resolveCommunity = this.state.resolveObjectResponse?.community;
-    if (resolveCommunity) {
-      combined.push(this.communityViewToCombined(resolveCommunity));
-    }
-    let resolveUser = this.state.resolveObjectResponse?.person;
-    if (resolveUser) {
-      combined.push(this.personViewSafeToCombined(resolveUser));
-    }
+    this.state.resolveObjectResponse.match({
+      some: res => {
+        let resolveComment = res.comment;
+        if (resolveComment.isSome()) {
+          combined.push(this.commentViewToCombined(resolveComment.unwrap()));
+        }
+        let resolvePost = res.post;
+        if (resolvePost.isSome()) {
+          combined.push(this.postViewToCombined(resolvePost.unwrap()));
+        }
+        let resolveCommunity = res.community;
+        if (resolveCommunity.isSome()) {
+          combined.push(
+            this.communityViewToCombined(resolveCommunity.unwrap())
+          );
+        }
+        let resolveUser = res.person;
+        if (resolveUser.isSome()) {
+          combined.push(this.personViewSafeToCombined(resolveUser.unwrap()));
+        }
+      },
+      none: void 0,
+    });
 
     // Push the search results
-    pushNotNull(
-      combined,
-      this.state.searchResponse?.comments?.map(e =>
-        this.commentViewToCombined(e)
-      )
-    );
-    pushNotNull(
-      combined,
-      this.state.searchResponse?.posts?.map(e => this.postViewToCombined(e))
-    );
-    pushNotNull(
-      combined,
-      this.state.searchResponse?.communities?.map(e =>
-        this.communityViewToCombined(e)
-      )
-    );
-    pushNotNull(
-      combined,
-      this.state.searchResponse?.users?.map(e =>
-        this.personViewSafeToCombined(e)
-      )
-    );
+    this.state.searchResponse.match({
+      some: res => {
+        pushNotNull(
+          combined,
+          res.comments?.map(e => this.commentViewToCombined(e))
+        );
+        pushNotNull(
+          combined,
+          res.posts?.map(e => this.postViewToCombined(e))
+        );
+        pushNotNull(
+          combined,
+          res.communities?.map(e => this.communityViewToCombined(e))
+        );
+        pushNotNull(
+          combined,
+          res.users?.map(e => this.personViewSafeToCombined(e))
+        );
+      },
+      none: void 0,
+    });
 
     // Sort it
     if (this.state.sort == SortType.New) {
@@ -528,18 +577,24 @@ export class Search extends Component<any, SearchState> {
                 <PostListing
                   key={(i.data as PostView).post.id}
                   post_view={i.data as PostView}
+                  duplicates={None}
+                  moderators={None}
+                  admins={None}
                   showCommunity
-                  enableDownvotes={this.state.site.enable_downvotes}
-                  enableNsfw={this.state.site.enable_nsfw}
+                  enableDownvotes={enableDownvotes(this.state.siteRes)}
+                  enableNsfw={enableNsfw(this.state.siteRes)}
                 />
               )}
               {i.type_ == "comments" && (
                 <CommentNodes
                   key={(i.data as CommentView).comment.id}
                   nodes={[{ comment_view: i.data as CommentView }]}
+                  moderators={None}
+                  admins={None}
+                  maxCommentsShown={None}
                   locked
                   noIndent
-                  enableDownvotes={this.state.site.enable_downvotes}
+                  enableDownvotes={enableDownvotes(this.state.siteRes)}
                 />
               )}
               {i.type_ == "communities" && (
@@ -558,15 +613,24 @@ export class Search extends Component<any, SearchState> {
   comments() {
     let comments: CommentView[] = [];
 
-    pushNotNull(comments, this.state.resolveObjectResponse?.comment);
-    pushNotNull(comments, this.state.searchResponse?.comments);
+    this.state.resolveObjectResponse.match({
+      some: res => pushNotNull(comments, res.comment),
+      none: void 0,
+    });
+    this.state.searchResponse.match({
+      some: res => pushNotNull(comments, res.comments),
+      none: void 0,
+    });
 
     return (
       <CommentNodes
         nodes={commentsToFlatNodes(comments)}
         locked
         noIndent
-        enableDownvotes={this.state.site.enable_downvotes}
+        moderators={None}
+        admins={None}
+        maxCommentsShown={None}
+        enableDownvotes={enableDownvotes(this.state.siteRes)}
       />
     );
   }
@@ -574,8 +638,14 @@ export class Search extends Component<any, SearchState> {
   posts() {
     let posts: PostView[] = [];
 
-    pushNotNull(posts, this.state.resolveObjectResponse?.post);
-    pushNotNull(posts, this.state.searchResponse?.posts);
+    this.state.resolveObjectResponse.match({
+      some: res => pushNotNull(posts, res.post),
+      none: void 0,
+    });
+    this.state.searchResponse.match({
+      some: res => pushNotNull(posts, res.posts),
+      none: void 0,
+    });
 
     return (
       <>
@@ -585,8 +655,11 @@ export class Search extends Component<any, SearchState> {
               <PostListing
                 post_view={post}
                 showCommunity
-                enableDownvotes={this.state.site.enable_downvotes}
-                enableNsfw={this.state.site.enable_nsfw}
+                duplicates={None}
+                moderators={None}
+                admins={None}
+                enableDownvotes={enableDownvotes(this.state.siteRes)}
+                enableNsfw={enableNsfw(this.state.siteRes)}
               />
             </div>
           </div>
@@ -598,8 +671,14 @@ export class Search extends Component<any, SearchState> {
   communities() {
     let communities: CommunityView[] = [];
 
-    pushNotNull(communities, this.state.resolveObjectResponse?.community);
-    pushNotNull(communities, this.state.searchResponse?.communities);
+    this.state.resolveObjectResponse.match({
+      some: res => pushNotNull(communities, res.community),
+      none: void 0,
+    });
+    this.state.searchResponse.match({
+      some: res => pushNotNull(communities, res.communities),
+      none: void 0,
+    });
 
     return (
       <>
@@ -615,8 +694,14 @@ export class Search extends Component<any, SearchState> {
   users() {
     let users: PersonViewSafe[] = [];
 
-    pushNotNull(users, this.state.resolveObjectResponse?.person);
-    pushNotNull(users, this.state.searchResponse?.users);
+    this.state.resolveObjectResponse.match({
+      some: res => pushNotNull(users, res.person),
+      none: void 0,
+    });
+    this.state.searchResponse.match({
+      some: res => pushNotNull(users, res.users),
+      none: void 0,
+    });
 
     return (
       <>
@@ -692,11 +777,14 @@ export class Search extends Component<any, SearchState> {
             value={this.state.creatorId}
           >
             <option value="0">{i18n.t("all")}</option>
-            {this.state.creator && (
-              <option value={this.state.creator.person.id}>
-                {personSelectName(this.state.creator)}
-              </option>
-            )}
+            {this.state.creatorDetails.match({
+              some: creator => (
+                <option value={creator.person_view.person.id}>
+                  {personSelectName(creator.person_view)}
+                </option>
+              ),
+              none: <></>,
+            })}
           </select>
         </div>
       </div>
@@ -704,19 +792,21 @@ export class Search extends Component<any, SearchState> {
   }
 
   resultsCount(): number {
-    let res = this.state.searchResponse;
-    let resObj = this.state.resolveObjectResponse;
-    let resObjCount =
-      resObj?.post || resObj?.person || resObj?.community || resObj?.comment
-        ? 1
-        : 0;
-    return (
-      res?.posts?.length +
-      res?.comments?.length +
-      res?.communities?.length +
-      res?.users?.length +
-      resObjCount
-    );
+    let searchCount = this.state.searchResponse
+      .map(
+        r =>
+          r.posts?.length +
+          r.comments?.length +
+          r.communities?.length +
+          r.users?.length
+      )
+      .unwrapOr(0);
+
+    let resObjCount = this.state.resolveObjectResponse
+      .map(r => (r.post || r.person || r.community || r.comment ? 1 : 0))
+      .unwrapOr(0);
+
+    return resObjCount + searchCount;
   }
 
   handlePageChange(page: number) {
@@ -724,30 +814,34 @@ export class Search extends Component<any, SearchState> {
   }
 
   search() {
-    let form: SearchForm = {
+    let community_id: Option<number> =
+      this.state.communityId == 0 ? None : Some(this.state.communityId);
+    let creator_id: Option<number> =
+      this.state.creatorId == 0 ? None : Some(this.state.creatorId);
+
+    console.log(community_id.unwrapOr(-22));
+
+    let form = new SearchForm({
       q: this.state.q,
-      type_: this.state.type_,
-      sort: this.state.sort,
-      listing_type: this.state.listingType,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(false),
-    };
-    if (this.state.communityId !== 0) {
-      form.community_id = this.state.communityId;
-    }
-    if (this.state.creatorId !== 0) {
-      form.creator_id = this.state.creatorId;
-    }
+      community_id,
+      community_name: None,
+      creator_id,
+      type_: Some(this.state.type_),
+      sort: Some(this.state.sort),
+      listing_type: Some(this.state.listingType),
+      page: Some(this.state.page),
+      limit: Some(fetchLimit),
+      auth: auth(false).ok(),
+    });
 
-    let resolveObjectForm: ResolveObject = {
+    let resolveObjectForm = new ResolveObject({
       q: this.state.q,
-      auth: authField(false),
-    };
+      auth: auth(false).ok(),
+    });
 
     if (this.state.q != "") {
-      this.state.searchResponse = null;
-      this.state.resolveObjectResponse = null;
+      this.state.searchResponse = None;
+      this.state.resolveObjectResponse = None;
       this.state.loading = true;
       this.setState(this.state);
       WebSocketService.Instance.send(wsClient.search(form));
@@ -890,50 +984,56 @@ export class Search extends Component<any, SearchState> {
     let op = wsUserOp(msg);
     if (msg.error) {
       if (msg.error == "couldnt_find_object") {
-        this.state.resolveObjectResponse = {
-          comment: null,
-          post: null,
-          community: null,
-          person: null,
-        };
+        this.state.resolveObjectResponse = Some({
+          comment: None,
+          post: None,
+          community: None,
+          person: None,
+        });
         this.checkFinishedLoading();
       } else {
         toast(i18n.t(msg.error), "danger");
         return;
       }
     } else if (op == UserOperation.Search) {
-      let data = wsJsonToRes<SearchResponse>(msg).data;
-      this.state.searchResponse = data;
+      let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
+      this.state.searchResponse = Some(data);
       window.scrollTo(0, 0);
       this.checkFinishedLoading();
       restoreScrollPosition(this.context);
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
       createCommentLikeRes(
         data.comment_view,
-        this.state.searchResponse?.comments
+        this.state.searchResponse.map(r => r.comments).unwrapOr([])
       );
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      createPostLikeFindRes(
+        data.post_view,
+        this.state.searchResponse.map(r => r.posts).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.ListCommunities) {
-      let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
+      let data = wsJsonToRes<ListCommunitiesResponse>(
+        msg,
+        ListCommunitiesResponse
+      );
       this.state.communities = data.communities;
       this.setState(this.state);
       this.setupCommunityFilter();
     } else if (op == UserOperation.ResolveObject) {
-      let data = wsJsonToRes<ResolveObjectResponse>(msg).data;
-      this.state.resolveObjectResponse = data;
+      let data = wsJsonToRes<ResolveObjectResponse>(msg, ResolveObjectResponse);
+      this.state.resolveObjectResponse = Some(data);
       this.checkFinishedLoading();
     }
   }
 
   checkFinishedLoading() {
     if (
-      this.state.searchResponse != null &&
-      this.state.resolveObjectResponse != null
+      this.state.searchResponse.isSome() &&
+      this.state.resolveObjectResponse.isSome()
     ) {
       this.state.loading = false;
       this.setState(this.state);
index e09f3bd598c65ebff0208cc7ff03cbe6b41139e4..92fabb8eb2a0470eff8a9fc383c9d513e94f08e0 100644 (file)
@@ -1,3 +1,4 @@
+import { Either, Option } from "@sniptt/monads";
 import {
   CommentView,
   GetSiteResponse,
@@ -5,6 +6,9 @@ import {
   PersonMentionView,
 } from "lemmy-js-client";
 
+/**
+ * This contains serialized data, it needs to be deserialized before use.
+ */
 export interface IsoData {
   path: string;
   routeData: any[];
@@ -23,9 +27,9 @@ declare global {
 }
 
 export interface InitialFetchRequest {
-  auth: string;
-  path: string;
+  auth: Option<string>;
   client: LemmyHttp;
+  path: string;
 }
 
 export interface CommentNode {
@@ -35,11 +39,10 @@ export interface CommentNode {
 }
 
 export interface PostFormParams {
-  name: string;
-  url?: string;
-  body?: string;
-  community_name?: string;
-  community_id?: number;
+  name: Option<string>;
+  url: Option<string>;
+  body: Option<string>;
+  nameOrId: Option<Either<string, number>>;
 }
 
 export enum CommentSortType {
index 031cf7d8d0872093166108244c78f1f70853cb97..678d1129026b23310dfa5a18f8cefca8be6407ed 100644 (file)
@@ -1,9 +1,12 @@
 // import Cookies from 'js-cookie';
+import { Err, None, Ok, Option, Result, Some } from "@sniptt/monads";
 import IsomorphicCookie from "isomorphic-cookie";
 import jwt_decode from "jwt-decode";
 import { LoginResponse, MyUserInfo } from "lemmy-js-client";
 import { BehaviorSubject, Subject } from "rxjs";
 import { isHttps } from "../env";
+import { i18n } from "../i18next";
+import { isBrowser, toast } from "../utils";
 
 interface Claims {
   sub: number;
@@ -11,11 +14,16 @@ interface Claims {
   iat: number;
 }
 
+interface JwtInfo {
+  claims: Claims;
+  jwt: string;
+}
+
 export class UserService {
   private static _instance: UserService;
-  public myUserInfo: MyUserInfo;
-  public claims: Claims;
-  public jwtSub: Subject<string> = new Subject<string>();
+  public myUserInfo: Option<MyUserInfo> = None;
+  public jwtInfo: Option<JwtInfo> = None;
+  public jwtSub: Subject<Option<JwtInfo>> = new Subject<Option<JwtInfo>>();
   public unreadInboxCountSub: BehaviorSubject<number> =
     new BehaviorSubject<number>(0);
   public unreadReportCountSub: BehaviorSubject<number> =
@@ -24,12 +32,7 @@ export class UserService {
     new BehaviorSubject<number>(0);
 
   private constructor() {
-    if (this.auth) {
-      this.setClaims(this.auth);
-    } else {
-      // setTheme();
-      console.log("No JWT cookie found.");
-    }
+    this.setJwtInfo();
   }
 
   public login(res: LoginResponse) {
@@ -37,26 +40,42 @@ export class UserService {
     expires.setDate(expires.getDate() + 365);
     IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps });
     console.log("jwt cookie set");
-    this.setClaims(res.jwt);
+    this.setJwtInfo();
   }
 
   public logout() {
-    this.claims = undefined;
-    this.myUserInfo = undefined;
-    // setTheme();
-    this.jwtSub.next("");
+    this.jwtInfo = None;
+    this.myUserInfo = None;
+    this.jwtSub.next(this.jwtInfo);
     IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
     document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.host;
+    location.reload(); // TODO may not be necessary anymore
     console.log("Logged out.");
   }
 
-  public get auth(): string {
-    return IsomorphicCookie.load("jwt");
+  public auth(throwErr = true): Result<string, string> {
+    // Can't use match to convert to result for some reason
+    let jwt = this.jwtInfo.map(j => j.jwt);
+    if (jwt.isSome()) {
+      return Ok(jwt.unwrap());
+    } else {
+      let msg = "No JWT cookie found";
+      if (throwErr && isBrowser()) {
+        console.log(msg);
+        toast(i18n.t("not_logged_in"), "danger");
+      }
+      return Err(msg);
+    }
   }
 
-  private setClaims(jwt: string) {
-    this.claims = jwt_decode(jwt);
-    this.jwtSub.next(jwt);
+  private setJwtInfo() {
+    let jwt = IsomorphicCookie.load("jwt");
+
+    if (jwt) {
+      let jwtInfo: JwtInfo = { jwt, claims: jwt_decode(jwt) };
+      this.jwtInfo = Some(jwtInfo);
+      this.jwtSub.next(this.jwtInfo);
+    }
   }
 
   public static get Instance() {
index 87d8f9798494ca9e1d2e4408e963c7b53a69f8db..7d7a46d597a5d5d9c295c04c534bb454a31638f0 100644 (file)
@@ -1,4 +1,3 @@
-import { PersonViewSafe, WebSocketJsonResponse } from "lemmy-js-client";
 import { Observable } from "rxjs";
 import { share } from "rxjs/operators";
 import {
@@ -15,9 +14,6 @@ export class WebSocketService {
   private ws: WS;
   public subject: Observable<any>;
 
-  public admins: PersonViewSafe[];
-  public banned: PersonViewSafe[];
-
   private constructor() {
     let firstConnect = true;
 
@@ -34,7 +30,7 @@ export class WebSocketService {
           console.log(`Connected to ${wsUri}`);
 
           if (!firstConnect) {
-            let res: WebSocketJsonResponse<any> = {
+            let res = {
               reconnect: true,
             };
             obs.next(res);
index 0199f47fe772cdc50d126df70bb83275b506712e..222190abdfdb212e579e232f4a29895770fef03f 100644 (file)
@@ -1,3 +1,5 @@
+import { None, Option, Result, Some } from "@sniptt/monads";
+import { ClassConstructor, deserialize, serialize } from "class-transformer";
 import emojiShortName from "emoji-short-name";
 import {
   BlockCommunityResponse,
@@ -5,6 +7,7 @@ import {
   CommentReportView,
   CommentView,
   CommunityBlockView,
+  CommunityModeratorView,
   CommunityView,
   GetSiteMetadata,
   GetSiteResponse,
@@ -22,9 +25,6 @@ import {
   Search,
   SearchType,
   SortType,
-  UserOperation,
-  WebSocketJsonResponse,
-  WebSocketResponse,
 } from "lemmy-js-client";
 import markdown_it from "markdown-it";
 import markdown_it_container from "markdown-it-container";
@@ -72,6 +72,7 @@ export const elementUrl = "https://element.io";
 
 export const postRefetchSeconds: number = 60 * 1000;
 export const fetchLimit = 20;
+export const trendingFetchLimit = 6;
 export const mentionDropdownFetchLimit = 10;
 
 export const relTags = "noopener nofollow";
@@ -98,20 +99,6 @@ export function randomStr(
     .join("");
 }
 
-export function wsJsonToRes<ResponseType>(
-  msg: WebSocketJsonResponse<ResponseType>
-): WebSocketResponse<ResponseType> {
-  return {
-    op: wsUserOp(msg),
-    data: msg.data,
-  };
-}
-
-export function wsUserOp(msg: any): UserOperation {
-  let opStr: string = msg.op;
-  return UserOperation[opStr];
-}
-
 export const md = new markdown_it({
   html: false,
   linkify: true,
@@ -192,30 +179,135 @@ export function futureDaysToUnixTime(days: number): number {
 }
 
 export function canMod(
-  myUserInfo: MyUserInfo,
-  modIds: number[],
+  mods: Option<CommunityModeratorView[]>,
+  admins: Option<PersonViewSafe[]>,
   creator_id: number,
+  myUserInfo = UserService.Instance.myUserInfo,
   onSelf = false
 ): boolean {
   // You can do moderator actions only on the mods added after you.
-  if (myUserInfo) {
-    let yourIndex = modIds.findIndex(
-      id => id == myUserInfo.local_user_view.person.id
-    );
-    if (yourIndex == -1) {
-      return false;
-    } else {
-      // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
-      modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
-      return !modIds.includes(creator_id);
-    }
-  } else {
-    return false;
-  }
+  let adminsThenMods = admins
+    .unwrapOr([])
+    .map(a => a.person.id)
+    .concat(mods.unwrapOr([]).map(m => m.moderator.id));
+
+  return myUserInfo.match({
+    some: me => {
+      let myIndex = adminsThenMods.findIndex(
+        id => id == me.local_user_view.person.id
+      );
+      if (myIndex == -1) {
+        return false;
+      } else {
+        // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
+        adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1));
+        return !adminsThenMods.includes(creator_id);
+      }
+    },
+    none: false,
+  });
+}
+
+export function canAdmin(
+  admins: Option<PersonViewSafe[]>,
+  creator_id: number,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return canMod(None, admins, creator_id, myUserInfo);
+}
+
+export function isMod(
+  mods: Option<CommunityModeratorView[]>,
+  creator_id: number
+): boolean {
+  return mods.match({
+    some: mods => mods.map(m => m.moderator.id).includes(creator_id),
+    none: false,
+  });
+}
+
+export function amMod(
+  mods: Option<CommunityModeratorView[]>,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return myUserInfo.match({
+    some: mui => isMod(mods, mui.local_user_view.person.id),
+    none: false,
+  });
 }
 
-export function isMod(modIds: number[], creator_id: number): boolean {
-  return modIds.includes(creator_id);
+export function isAdmin(
+  admins: Option<PersonViewSafe[]>,
+  creator_id: number
+): boolean {
+  return admins.match({
+    some: admins => admins.map(a => a.person.id).includes(creator_id),
+    none: false,
+  });
+}
+
+export function amAdmin(
+  admins: Option<PersonViewSafe[]>,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return myUserInfo.match({
+    some: mui => isAdmin(admins, mui.local_user_view.person.id),
+    none: false,
+  });
+}
+
+export function amCommunityCreator(
+  mods: Option<CommunityModeratorView[]>,
+  creator_id: number,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return mods.match({
+    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
+            myId != creator_id,
+          none: false,
+        }),
+    none: false,
+  });
+}
+
+export function amSiteCreator(
+  admins: Option<PersonViewSafe[]>,
+  creator_id: number,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return admins.match({
+    some: admins =>
+      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(
+  mods: Option<CommunityModeratorView[]>,
+  myUserInfo = UserService.Instance.myUserInfo
+): boolean {
+  return mods.match({
+    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))/;
@@ -321,13 +413,14 @@ export function debounce(func: any, wait = 1000, immediate = false) {
   };
 }
 
-export function getLanguages(override?: string): string[] {
-  let myUserInfo = UserService.Instance.myUserInfo;
-  let lang =
-    override ||
-    (myUserInfo?.local_user_view.local_user.lang
-      ? myUserInfo.local_user_view.local_user.lang
-      : "browser");
+export function getLanguages(
+  override?: string,
+  myUserInfo = UserService.Instance.myUserInfo
+): string[] {
+  let myLang = myUserInfo
+    .map(m => m.local_user_view.local_user.lang)
+    .unwrapOr("browser");
+  let lang = override || myLang;
 
   if (lang == "browser" && isBrowser()) {
     return getBrowserLanguages();
@@ -406,24 +499,26 @@ export function objectFlip(obj: any) {
   return ret;
 }
 
-export function showAvatars(): boolean {
-  return (
-    UserService.Instance.myUserInfo?.local_user_view.local_user.show_avatars ||
-    !UserService.Instance.myUserInfo
-  );
+export function showAvatars(
+  myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo
+): boolean {
+  return myUserInfo
+    .map(m => m.local_user_view.local_user.show_avatars)
+    .unwrapOr(true);
 }
 
-export function showScores(): boolean {
-  return (
-    UserService.Instance.myUserInfo?.local_user_view.local_user.show_scores ||
-    !UserService.Instance.myUserInfo
-  );
+export function showScores(
+  myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo
+): boolean {
+  return myUserInfo
+    .map(m => m.local_user_view.local_user.show_scores)
+    .unwrapOr(true);
 }
 
 export function isCakeDay(published: string): boolean {
   // moment(undefined) or moment.utc(undefined) returns the current date/time
   // moment(null) or moment.utc(null) returns null
-  const createDate = moment.utc(published || null).local();
+  const createDate = moment.utc(published).local();
   const currentDate = moment(new Date());
 
   return (
@@ -472,7 +567,7 @@ export function pictrsDeleteToast(
 
 interface NotifyInfo {
   name: string;
-  icon?: string;
+  icon: Option<string>;
   link: string;
   body: string;
 }
@@ -484,7 +579,7 @@ export function messageToastify(info: NotifyInfo, router: any) {
 
     let toast = Toastify({
       text: `${htmlBody}<br />${info.name}`,
-      avatar: info.icon ? info.icon : null,
+      avatar: info.icon,
       backgroundColor: backgroundColor,
       className: "text-dark",
       close: true,
@@ -666,16 +761,18 @@ async function communitySearch(text: string): Promise<CommunityTribute[]> {
 
 export function getListingTypeFromProps(
   props: any,
-  defaultListingType: ListingType
+  defaultListingType: ListingType,
+  myUserInfo = UserService.Instance.myUserInfo
 ): ListingType {
   return props.match.params.listing_type
     ? routeListingTypeToEnum(props.match.params.listing_type)
-    : UserService.Instance.myUserInfo
-    ? Object.values(ListingType)[
-        UserService.Instance.myUserInfo.local_user_view.local_user
-          .default_listing_type
-      ]
-    : defaultListingType;
+    : myUserInfo.match({
+        some: me =>
+          Object.values(ListingType)[
+            me.local_user_view.local_user.default_listing_type
+          ],
+        none: defaultListingType,
+      });
 }
 
 export function getListingTypeFromPropsNoDefault(props: any): ListingType {
@@ -691,15 +788,19 @@ export function getDataTypeFromProps(props: any): DataType {
     : DataType.Post;
 }
 
-export function getSortTypeFromProps(props: any): SortType {
+export function getSortTypeFromProps(
+  props: any,
+  myUserInfo = UserService.Instance.myUserInfo
+): SortType {
   return props.match.params.sort
     ? routeSortTypeToEnum(props.match.params.sort)
-    : UserService.Instance.myUserInfo
-    ? Object.values(SortType)[
-        UserService.Instance.myUserInfo.local_user_view.local_user
-          .default_sort_type
-      ]
-    : SortType.Active;
+    : myUserInfo.match({
+        some: mui =>
+          Object.values(SortType)[
+            mui.local_user_view.local_user.default_sort_type
+          ],
+        none: SortType.Active,
+      });
 }
 
 export function getPageFromProps(props: any): number {
@@ -744,42 +845,53 @@ export function saveCommentRes(data: CommentView, comments: CommentView[]) {
   }
 }
 
+// TODO Should only use the return now, no state?
 export function updatePersonBlock(
-  data: BlockPersonResponse
-): PersonBlockView[] {
-  if (data.blocked) {
-    UserService.Instance.myUserInfo.person_blocks.push({
-      person: UserService.Instance.myUserInfo.local_user_view.person,
-      target: data.person_view.person,
-    });
-    toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
-  } else {
-    UserService.Instance.myUserInfo.person_blocks =
-      UserService.Instance.myUserInfo.person_blocks.filter(
-        i => i.target.id != data.person_view.person.id
-      );
-    toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
-  }
-  return UserService.Instance.myUserInfo.person_blocks;
+  data: BlockPersonResponse,
+  myUserInfo = UserService.Instance.myUserInfo
+): Option<PersonBlockView[]> {
+  return myUserInfo.match({
+    some: (mui: MyUserInfo) => {
+      if (data.blocked) {
+        mui.person_blocks.push({
+          person: mui.local_user_view.person,
+          target: data.person_view.person,
+        });
+        toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
+      } else {
+        mui.person_blocks = mui.person_blocks.filter(
+          i => i.target.id != data.person_view.person.id
+        );
+        toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
+      }
+      return Some(mui.person_blocks);
+    },
+    none: None,
+  });
 }
 
 export function updateCommunityBlock(
-  data: BlockCommunityResponse
-): CommunityBlockView[] {
-  if (data.blocked) {
-    UserService.Instance.myUserInfo.community_blocks.push({
-      person: UserService.Instance.myUserInfo.local_user_view.person,
-      community: data.community_view.community,
-    });
-    toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
-  } else {
-    UserService.Instance.myUserInfo.community_blocks =
-      UserService.Instance.myUserInfo.community_blocks.filter(
-        i => i.community.id != data.community_view.community.id
-      );
-    toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
-  }
-  return UserService.Instance.myUserInfo.community_blocks;
+  data: BlockCommunityResponse,
+  myUserInfo = UserService.Instance.myUserInfo
+): Option<CommunityBlockView[]> {
+  return myUserInfo.match({
+    some: (mui: MyUserInfo) => {
+      if (data.blocked) {
+        mui.community_blocks.push({
+          person: mui.local_user_view.person,
+          community: data.community_view.community,
+        });
+        toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
+      } else {
+        mui.community_blocks = mui.community_blocks.filter(
+          i => i.community.id != data.community_view.community.id
+        );
+        toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
+      }
+      return Some(mui.community_blocks);
+    },
+    none: None,
+  });
 }
 
 export function createCommentLikeRes(
@@ -910,7 +1022,8 @@ function commentSort(tree: CommentNodeI[], sort: CommentSortType) {
       (a, b) =>
         +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
         +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
-        hotRankComment(b.comment_view) - hotRankComment(a.comment_view)
+        hotRankComment(b.comment_view as CommentView) -
+          hotRankComment(a.comment_view as CommentView)
     );
   }
 
@@ -953,6 +1066,7 @@ export function buildCommentsTree(
     let node: CommentNodeI = {
       comment_view: comment_view,
       children: [],
+      depth: 0,
     };
     map.set(comment_view.comment.id, { ...node });
   }
@@ -960,15 +1074,18 @@ export function buildCommentsTree(
   for (let comment_view of comments) {
     let child = map.get(comment_view.comment.id);
     let parent_id = comment_view.comment.parent_id;
-    if (parent_id) {
-      let parent = map.get(parent_id);
-      // Necessary because blocked comment might not exist
-      if (parent) {
-        parent.children.push(child);
-      }
-    } else {
-      tree.push(child);
-    }
+    parent_id.match({
+      some: parentId => {
+        let parent = map.get(parentId);
+        // Necessary because blocked comment might not exist
+        if (parent) {
+          parent.children.push(child);
+        }
+      },
+      none: () => {
+        tree.push(child);
+      },
+    });
 
     setDepth(child);
   }
@@ -993,35 +1110,41 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
     depth: 0,
   };
 
-  if (cv.comment.parent_id) {
-    let parentComment = searchCommentTree(tree, cv.comment.parent_id);
-    if (parentComment) {
-      node.depth = parentComment.depth + 1;
-      parentComment.children.unshift(node);
-    }
-  } else {
-    tree.unshift(node);
-  }
+  cv.comment.parent_id.match({
+    some: parentId => {
+      let parentComment = searchCommentTree(tree, parentId);
+      parentComment.match({
+        some: pComment => {
+          node.depth = pComment.depth + 1;
+          pComment.children.unshift(node);
+        },
+        none: void 0,
+      });
+    },
+    none: () => {
+      tree.unshift(node);
+    },
+  });
 }
 
 export function searchCommentTree(
   tree: CommentNodeI[],
   id: number
-): CommentNodeI {
+): Option<CommentNodeI> {
   for (let node of tree) {
     if (node.comment_view.comment.id === id) {
-      return node;
+      return Some(node);
     }
 
     for (const child of node.children) {
-      const res = searchCommentTree([child], id);
+      let res = searchCommentTree([child], id);
 
-      if (res) {
+      if (res.isSome()) {
         return res;
       }
     }
   }
-  return null;
+  return None;
 }
 
 export const colorList: string[] = [
@@ -1044,7 +1167,7 @@ export function hostname(url: string): string {
 
 export function validTitle(title?: string): boolean {
   // Initial title is null, minimum length is taken care of by textarea's minLength={3}
-  if (title === null || title.length < 3) return true;
+  if (!title || title.length < 3) return true;
 
   const regex = new RegExp(/.*\S.*/, "g");
 
@@ -1068,11 +1191,51 @@ export function isBrowser() {
   return typeof window !== "undefined";
 }
 
-export function setIsoData(context: any): IsoData {
-  let isoData: IsoData = isBrowser()
-    ? window.isoData
-    : context.router.staticContext;
-  return isoData;
+export function setIsoData<Type1, Type2, Type3, Type4, Type5>(
+  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 (isBrowser()) {
+    let json = window.isoData;
+    let routeData = json.routeData;
+    let routeDataOut: any[] = [];
+
+    // 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 isoData: IsoData = {
+      path: json.path,
+      site_res: convertWindowJson(GetSiteResponse, json.site_res),
+      routeData: routeDataOut,
+    };
+    return isoData;
+  } else return context.router.staticContext;
+}
+
+/**
+ * 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 {
@@ -1089,24 +1252,6 @@ export function wsSubscribe(parseMessage: any): Subscription {
   }
 }
 
-export function setOptionalAuth(obj: any, auth = UserService.Instance.auth) {
-  if (auth) {
-    obj.auth = auth;
-  }
-}
-
-export function authField(
-  throwErr = true,
-  auth = UserService.Instance.auth
-): string {
-  if (auth == null && throwErr) {
-    toast(i18n.t("not_logged_in"), "danger");
-    throw "Not logged in";
-  } else {
-    return auth;
-  }
-}
-
 moment.updateLocale("en", {
   relativeTime: {
     future: "in %s",
@@ -1141,7 +1286,9 @@ export function restoreScrollPosition(context: any) {
 }
 
 export function showLocal(isoData: IsoData): boolean {
-  return isoData.site_res.federated_instances?.linked.length > 0;
+  return isoData.site_res.federated_instances
+    .map(f => f.linked.length > 0)
+    .unwrapOr(false);
 }
 
 interface ChoicesValue {
@@ -1168,12 +1315,15 @@ export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
 export async function fetchCommunities(q: string) {
   let form: Search = {
     q,
-    type_: SearchType.Communities,
-    sort: SortType.TopAll,
-    listing_type: ListingType.All,
-    page: 1,
-    limit: fetchLimit,
-    auth: authField(false),
+    type_: Some(SearchType.Communities),
+    sort: Some(SortType.TopAll),
+    listing_type: Some(ListingType.All),
+    page: Some(1),
+    limit: Some(fetchLimit),
+    community_id: None,
+    community_name: None,
+    creator_id: None,
+    auth: auth(false).ok(),
   };
   let client = new LemmyHttp(httpBase);
   return client.search(form);
@@ -1182,12 +1332,15 @@ export async function fetchCommunities(q: string) {
 export async function fetchUsers(q: string) {
   let form: Search = {
     q,
-    type_: SearchType.Users,
-    sort: SortType.TopAll,
-    listing_type: ListingType.All,
-    page: 1,
-    limit: fetchLimit,
-    auth: authField(false),
+    type_: Some(SearchType.Users),
+    sort: Some(SortType.TopAll),
+    listing_type: Some(ListingType.All),
+    page: Some(1),
+    limit: Some(fetchLimit),
+    community_id: None,
+    community_name: None,
+    creator_id: None,
+    auth: auth(false).ok(),
   };
   let client = new LemmyHttp(httpBase);
   return client.search(form);
@@ -1233,7 +1386,7 @@ export function communitySelectName(cv: CommunityView): string {
 }
 
 export function personSelectName(pvs: PersonViewSafe): string {
-  let pName = pvs.person.display_name || pvs.person.name;
+  let pName = pvs.person.display_name.unwrapOr(pvs.person.name);
   return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
 }
 
@@ -1254,9 +1407,11 @@ export function numToSI(value: number): string {
 }
 
 export function isBanned(ps: PersonSafe): boolean {
+  let expires = ps.ban_expires;
   // Add Z to convert from UTC date
-  if (ps.ban_expires) {
-    if (ps.banned && new Date(ps.ban_expires + "Z") > new Date()) {
+  // TODO this check probably isn't necessary anymore
+  if (expires.isSome()) {
+    if (ps.banned && new Date(expires.unwrap() + "Z") > new Date()) {
       return true;
     } else {
       return false;
@@ -1271,3 +1426,15 @@ export function pushNotNull(array: any[], new_item?: any) {
     array.push(...new_item);
   }
 }
+
+export function auth(throwErr = true): Result<string, string> {
+  return UserService.Instance.auth(throwErr);
+}
+
+export function enableDownvotes(siteRes: GetSiteResponse): boolean {
+  return siteRes.site_view.map(s => s.site.enable_downvotes).unwrapOr(true);
+}
+
+export function enableNsfw(siteRes: GetSiteResponse): boolean {
+  return siteRes.site_view.map(s => s.site.enable_nsfw).unwrapOr(false);
+}
index cd9bc8d0b35161873a380449592f00244a6bedbc..c708247264f5d763eda62ce550c7198c1e0d0bec 100644 (file)
@@ -18,7 +18,8 @@
                "skipLibCheck": true,\r
                "noUnusedParameters": true,\r
                "noImplicitReturns": true,\r
-               "noFallthroughCasesInSwitch": true\r
+    "experimentalDecorators": true,\r
+    "noFallthroughCasesInSwitch": true\r
        },\r
        "include": [\r
                "src/**/*",\r
index 551a4528c95ab830baa70e3a15140f9d390446ff..2b2fba457256a86225394fd37a042ddcd819f26f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.18.2":
+  version "7.18.2"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d"
+  integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==
+  dependencies:
+    "@babel/types" "^7.18.2"
+    "@jridgewell/gen-mapping" "^0.3.0"
+    jsesc "^2.5.1"
+
 "@babel/helper-annotate-as-pure@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862"
     "@babel/helper-replace-supers" "^7.16.7"
     "@babel/helper-split-export-declaration" "^7.16.7"
 
+"@babel/helper-create-class-features-plugin@^7.18.0":
+  version "7.18.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.0.tgz#fac430912606331cb075ea8d82f9a4c145a4da19"
+  integrity sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.16.7"
+    "@babel/helper-environment-visitor" "^7.16.7"
+    "@babel/helper-function-name" "^7.17.9"
+    "@babel/helper-member-expression-to-functions" "^7.17.7"
+    "@babel/helper-optimise-call-expression" "^7.16.7"
+    "@babel/helper-replace-supers" "^7.16.7"
+    "@babel/helper-split-export-declaration" "^7.16.7"
+
 "@babel/helper-create-regexp-features-plugin@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz#0cb82b9bac358eb73bfbd73985a776bfa6b14d48"
   dependencies:
     "@babel/types" "^7.16.7"
 
+"@babel/helper-environment-visitor@^7.18.2":
+  version "7.18.2"
+  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd"
+  integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==
+
 "@babel/helper-explode-assignable-expression@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a"
   dependencies:
     "@babel/types" "^7.16.7"
 
+"@babel/helper-member-expression-to-functions@^7.17.7":
+  version "7.17.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4"
+  integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==
+  dependencies:
+    "@babel/types" "^7.17.0"
+
 "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
   integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
 
+"@babel/helper-plugin-utils@^7.17.12":
+  version "7.17.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96"
+  integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==
+
 "@babel/helper-remap-async-to-generator@^7.16.8":
   version "7.16.8"
   resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3"
     "@babel/traverse" "^7.16.7"
     "@babel/types" "^7.16.7"
 
+"@babel/helper-replace-supers@^7.18.2":
+  version "7.18.2"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.2.tgz#41fdfcc9abaf900e18ba6e5931816d9062a7b2e0"
+  integrity sha512-XzAIyxx+vFnrOxiQrToSUOzUOn0e1J2Li40ntddek1Y69AXUTXoDJ40/D5RdjFu7s7qHiaeoTiempZcbuVXh2Q==
+  dependencies:
+    "@babel/helper-environment-visitor" "^7.18.2"
+    "@babel/helper-member-expression-to-functions" "^7.17.7"
+    "@babel/helper-optimise-call-expression" "^7.16.7"
+    "@babel/traverse" "^7.18.2"
+    "@babel/types" "^7.18.2"
+
 "@babel/helper-simple-access@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef"
   integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==
 
+"@babel/parser@^7.18.5":
+  version "7.18.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c"
+  integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==
+
 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050"
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/plugin-syntax-class-static-block" "^7.14.5"
 
+"@babel/plugin-proposal-decorators@^7.18.2":
+  version "7.18.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.2.tgz#dbe4086d2d42db489399783c3aa9272e9700afd4"
+  integrity sha512-kbDISufFOxeczi0v4NQP3p5kIeW6izn/6klfWBrIIdGZZe4UpHR+QU03FAoWjGGd9SUXAwbw2pup1kaL4OQsJQ==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.18.0"
+    "@babel/helper-plugin-utils" "^7.17.12"
+    "@babel/helper-replace-supers" "^7.18.2"
+    "@babel/helper-split-export-declaration" "^7.16.7"
+    "@babel/plugin-syntax-decorators" "^7.17.12"
+    charcodes "^0.2.0"
+
 "@babel/plugin-proposal-dynamic-import@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2"
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
+"@babel/plugin-syntax-decorators@^7.17.12":
+  version "7.17.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.12.tgz#02e8f678602f0af8222235271efea945cfdb018a"
+  integrity sha512-D1Hz0qtGTza8K2xGyEdVNCYLdVHukAcbQr4K3/s6r/esadyEriZovpJimQOpu8ju4/jV8dW/1xdaE0UpDroidw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.17.12"
+
 "@babel/plugin-syntax-dynamic-import@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
     debug "^4.1.0"
     globals "^11.1.0"
 
+"@babel/traverse@^7.18.2":
+  version "7.18.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd"
+  integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==
+  dependencies:
+    "@babel/code-frame" "^7.16.7"
+    "@babel/generator" "^7.18.2"
+    "@babel/helper-environment-visitor" "^7.18.2"
+    "@babel/helper-function-name" "^7.17.9"
+    "@babel/helper-hoist-variables" "^7.16.7"
+    "@babel/helper-split-export-declaration" "^7.16.7"
+    "@babel/parser" "^7.18.5"
+    "@babel/types" "^7.18.4"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
 "@babel/types@^7", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.4.4":
   version "7.16.8"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1"
     "@babel/helper-validator-identifier" "^7.16.7"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.18.2", "@babel/types@^7.18.4":
+  version "7.18.4"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354"
+  integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.16.7"
+    to-fast-properties "^2.0.0"
+
 "@discoveryjs/json-ext@^0.5.0":
   version "0.5.6"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
     update-notifier "^2.2.0"
     yargs "^8.0.2"
 
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9"
+  integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.0"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@jridgewell/resolve-uri@^3.0.3":
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c"
   integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==
 
+"@jridgewell/set-array@^1.0.0":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea"
+  integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==
+
 "@jridgewell/sourcemap-codec@^1.4.10":
   version "1.4.11"
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.13"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea"
+  integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@leichtgewicht/ip-codec@^2.0.1":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0"
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
   integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
 
+"@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":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-4.0.1.tgz#999a7c305b96766248044ebaac1a0299961f3b61"
@@ -2254,6 +2379,11 @@ chalk@^4.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+charcodes@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
+  integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==
+
 check-password-strength@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.5.tgz#bb10da01d24bd69e5e629c5cea2a6b729e5061af"
@@ -2323,6 +2453,11 @@ cidr-regex@1.0.6:
   resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
   integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME=
 
+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:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
@@ -4813,10 +4948,10 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-lemmy-js-client@0.16.4:
-  version "0.16.4"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.16.4.tgz#d24bae2b0d93c4d13eb4a5e5ddceaa2999f94740"
-  integrity sha512-EFHl6tbFZ0jk8VE68bgZOXoWuNHVzfcsyoAEZeHP6f8PkQ1g9zjxB/e3b5cIG2fFzOLsYIDh2w/SJy21WkFiiA==
+lemmy-js-client@0.17.0-rc.30:
+  version "0.17.0-rc.30"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.30.tgz#91cc926e662a5cd27f87cd2e6cdfcd210176745a"
+  integrity sha512-AcG8IZNNTa54BAXEqsL/QNlyPPwLntRLWpIOw9S3u84824d5inL7UCKnyx0UMbQklUuH/D3E2K9WNmZiUdvr3A==
 
 levn@^0.4.1:
   version "0.4.1"
@@ -6673,6 +6808,11 @@ redux@^4.1.2:
   dependencies:
     "@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@^9.0.0:
   version "9.0.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"