From b96e16b4e9a8baeddc3c97f449ef6c1cad1fbe29 Mon Sep 17 00:00:00 2001
From: Dessalines <dessalines@users.noreply.github.com>
Date: Thu, 30 Dec 2021 10:26:45 -0500
Subject: [PATCH] Private instances (#523)

* Updating translations.

* Adding registration applications.

* Updating translations.

* Adding verify email route.

* Fix missing signup question bug.

* Updating translations.

* A few fixes from comments on lemmy PR.

* v0.15.0-rc.4

* Some suggestions from PR.

* v0.15.0-rc.5

* Adding optional auth to modlog fetches.

* v0.15.0-rc.6

* Hide deny / approve buttons
---
 lemmy-translations                            |   2 +-
 package.json                                  |   4 +-
 src/server/index.tsx                          |   6 +-
 src/shared/components/app/navbar.tsx          |  86 +++++-
 ...{comment_report.tsx => comment-report.tsx} |  11 +-
 .../components/common/markdown-textarea.tsx   |   2 +-
 .../common/registration-application.tsx       | 150 +++++++++++
 src/shared/components/common/symbols.tsx      |   3 +
 src/shared/components/home/home.tsx           |   1 +
 src/shared/components/home/login.tsx          |   2 -
 src/shared/components/home/signup.tsx         | 100 +++++--
 src/shared/components/home/site-form.tsx      | 138 ++++++++--
 src/shared/components/modlog.tsx              |   5 +
 .../password-change.tsx}                      |   0
 .../person/registration-applications.tsx      | 249 ++++++++++++++++++
 src/shared/components/person/reports.tsx      |   4 +-
 src/shared/components/person/settings.tsx     |   5 +
 src/shared/components/person/verify-email.tsx |  97 +++++++
 .../post/{post_report.tsx => post-report.tsx} |  11 +-
 src/shared/routes.ts                          |  13 +-
 src/shared/services/UserService.ts            |   2 +
 src/shared/utils.ts                           |  15 ++
 yarn.lock                                     |   8 +-
 23 files changed, 849 insertions(+), 65 deletions(-)
 rename src/shared/components/comment/{comment_report.tsx => comment-report.tsx} (92%)
 create mode 100644 src/shared/components/common/registration-application.tsx
 rename src/shared/components/{home/password_change.tsx => person/password-change.tsx} (100%)
 create mode 100644 src/shared/components/person/registration-applications.tsx
 create mode 100644 src/shared/components/person/verify-email.tsx
 rename src/shared/components/post/{post_report.tsx => post-report.tsx} (92%)

diff --git a/lemmy-translations b/lemmy-translations
index 0412b6b..1e0bb99 160000
--- a/lemmy-translations
+++ b/lemmy-translations
@@ -1 +1 @@
-Subproject commit 0412b6b349e5e8d6ac3ed88801187833e95c72c9
+Subproject commit 1e0bb9920cda13bb128c87e85125b98ab8f319b6
diff --git a/package.json b/package.json
index 7621abb..ba0665a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "lemmy-ui",
   "description": "An isomorphic UI for lemmy",
-  "version": "0.14.5",
+  "version": "0.15.0-rc.6",
   "author": "Dessalines <tyhou13@gmx.com>",
   "license": "AGPL-3.0",
   "scripts": {
@@ -72,7 +72,7 @@
     "husky": "^7.0.4",
     "import-sort-style-module": "^6.0.0",
     "iso-639-1": "^2.1.10",
-    "lemmy-js-client": "0.14.0-rc.1",
+    "lemmy-js-client": "0.15.0-rc.6",
     "lint-staged": "^12.1.2",
     "mini-css-extract-plugin": "^2.4.5",
     "node-fetch": "^2.6.1",
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 5bf79f1..82d0379 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -91,7 +91,11 @@ server.get("/*", async (req, res) => {
     if (routeData[0] && routeData[0].error) {
       let errCode = routeData[0].error;
       console.error(errCode);
-      return res.redirect(`/404?err=${errCode}`);
+      if (errCode == "instance_is_private") {
+        return res.redirect(`/signup`);
+      } else {
+        return res.redirect(`/404?err=${errCode}`);
+      }
     }
 
     let isoData: IsoData = {
diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx
index 2a40915..5755541 100644
--- a/src/shared/components/app/navbar.tsx
+++ b/src/shared/components/app/navbar.tsx
@@ -7,6 +7,8 @@ import {
   GetSiteResponse,
   GetUnreadCount,
   GetUnreadCountResponse,
+  GetUnreadRegistrationApplicationCount,
+  GetUnreadRegistrationApplicationCountResponse,
   PrivateMessageResponse,
   UserOperation,
 } from "lemmy-js-client";
@@ -41,6 +43,7 @@ interface NavbarState {
   expanded: boolean;
   unreadInboxCount: number;
   unreadReportCount: number;
+  unreadApplicationCount: number;
   searchParam: string;
   toggleSearch: boolean;
   showDropdown: boolean;
@@ -52,11 +55,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   private userSub: Subscription;
   private unreadInboxCountSub: Subscription;
   private unreadReportCountSub: Subscription;
+  private unreadApplicationCountSub: Subscription;
   private searchTextField: RefObject<HTMLInputElement>;
   emptyState: NavbarState = {
     isLoggedIn: !!this.props.site_res.my_user,
     unreadInboxCount: 0,
     unreadReportCount: 0,
+    unreadApplicationCount: 0,
     expanded: false,
     searchParam: "",
     toggleSearch: false,
@@ -115,6 +120,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
         UserService.Instance.unreadReportCountSub.subscribe(res => {
           this.setState({ unreadReportCount: res });
         });
+      // Subscribe to unread application count
+      this.unreadApplicationCountSub =
+        UserService.Instance.unreadApplicationCountSub.subscribe(res => {
+          this.setState({ unreadApplicationCount: res });
+        });
     }
   }
 
@@ -123,6 +133,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     this.userSub.unsubscribe();
     this.unreadInboxCountSub.unsubscribe();
     this.unreadReportCountSub.unsubscribe();
+    this.unreadApplicationCountSub.unsubscribe();
   }
 
   updateUrl() {
@@ -215,6 +226,31 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                   </li>
                 </ul>
               )}
+              {UserService.Instance.myUserInfo?.local_user_view.person
+                .admin && (
+                <ul class="navbar-nav ml-1">
+                  <li className="nav-item">
+                    <NavLink
+                      to="/registration_applications"
+                      className="p-1 navbar-toggler nav-link border-0"
+                      onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
+                      title={i18n.t("unread_registration_applications", {
+                        count: this.state.unreadApplicationCount,
+                        formattedCount: numToSI(
+                          this.state.unreadApplicationCount
+                        ),
+                      })}
+                    >
+                      <Icon icon="clipboard" />
+                      {this.state.unreadApplicationCount > 0 && (
+                        <span class="mx-1 badge badge-light">
+                          {numToSI(this.state.unreadApplicationCount)}
+                        </span>
+                      )}
+                    </NavLink>
+                  </li>
+                </ul>
+              )}
             </>
           )}
           <button
@@ -366,6 +402,31 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                     </li>
                   </ul>
                 )}
+                {UserService.Instance.myUserInfo?.local_user_view.person
+                  .admin && (
+                  <ul class="navbar-nav my-2">
+                    <li className="nav-item">
+                      <NavLink
+                        to="/registration_applications"
+                        className="nav-link"
+                        onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
+                        title={i18n.t("unread_registration_applications", {
+                          count: this.state.unreadApplicationCount,
+                          formattedCount: numToSI(
+                            this.state.unreadApplicationCount
+                          ),
+                        })}
+                      >
+                        <Icon icon="clipboard" />
+                        {this.state.unreadApplicationCount > 0 && (
+                          <span class="mx-1 badge badge-light">
+                            {numToSI(this.state.unreadApplicationCount)}
+                          </span>
+                        )}
+                      </NavLink>
+                    </li>
+                  </ul>
+                )}
                 <ul class="navbar-nav">
                   <li class="nav-item dropdown">
                     <button
@@ -537,6 +598,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       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;
+      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;
@@ -586,7 +653,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     let unreadForm: GetUnreadCount = {
       auth: authField(),
     };
-
     WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
 
     console.log("Fetching reports...");
@@ -594,8 +660,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     let reportCountForm: GetReportCount = {
       auth: authField(),
     };
-
     WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
+
+    if (UserService.Instance.myUserInfo?.local_user_view.person.admin) {
+      console.log("Fetching applications...");
+
+      let applicationCountForm: GetUnreadRegistrationApplicationCount = {
+        auth: authField(),
+      };
+      WebSocketService.Instance.send(
+        wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
+      );
+    }
   }
 
   get currentLocation() {
@@ -612,6 +688,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     );
   }
 
+  sendApplicationUnread() {
+    UserService.Instance.unreadApplicationCountSub.next(
+      this.state.unreadApplicationCount
+    );
+  }
+
   get canAdmin(): boolean {
     return (
       UserService.Instance.myUserInfo &&
diff --git a/src/shared/components/comment/comment_report.tsx b/src/shared/components/comment/comment-report.tsx
similarity index 92%
rename from src/shared/components/comment/comment_report.tsx
rename to src/shared/components/comment/comment-report.tsx
index 87f6ebc..8e04962 100644
--- a/src/shared/components/comment/comment_report.tsx
+++ b/src/shared/components/comment/comment-report.tsx
@@ -25,6 +25,9 @@ export class CommentReport extends Component<CommentReportProps, any> {
   render() {
     let r = this.props.report;
     let comment = r.comment;
+    let tippyContent = i18n.t(
+      r.comment_report.resolved ? "unresolve_report" : "resolve_report"
+    );
 
     // Set the original post data ( a troll could change it )
     comment.content = r.comment_report.original_comment_text;
@@ -78,12 +81,8 @@ export class CommentReport extends Component<CommentReportProps, any> {
         <button
           className="btn btn-link btn-animate text-muted py-0"
           onClick={linkEvent(this, this.handleResolveReport)}
-          data-tippy-content={
-            r.comment_report.resolved ? "unresolve_report" : "resolve_report"
-          }
-          aria-label={
-            r.comment_report.resolved ? "unresolve_report" : "resolve_report"
-          }
+          data-tippy-content={tippyContent}
+          aria-label={tippyContent}
         >
           <Icon
             icon="check"
diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx
index ea5bea1..5b1c8ee 100644
--- a/src/shared/components/common/markdown-textarea.tsx
+++ b/src/shared/components/common/markdown-textarea.tsx
@@ -17,7 +17,7 @@ import {
 import { Icon, Spinner } from "./icon";
 
 interface MarkdownTextAreaProps {
-  initialContent: string;
+  initialContent?: string;
   finished?: boolean;
   buttonTitle?: string;
   replyType?: boolean;
diff --git a/src/shared/components/common/registration-application.tsx b/src/shared/components/common/registration-application.tsx
new file mode 100644
index 0000000..cad47b8
--- /dev/null
+++ b/src/shared/components/common/registration-application.tsx
@@ -0,0 +1,150 @@
+import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
+import {
+  ApproveRegistrationApplication,
+  RegistrationApplicationView,
+} from "lemmy-js-client";
+import { i18n } from "../../i18next";
+import { WebSocketService } from "../../services";
+import { authField, mdToHtml, wsClient } from "../../utils";
+import { PersonListing } from "../person/person-listing";
+import { MarkdownTextArea } from "./markdown-textarea";
+import { MomentTime } from "./moment-time";
+
+interface RegistrationApplicationProps {
+  application: RegistrationApplicationView;
+}
+
+interface RegistrationApplicationState {
+  denyReason?: string;
+  denyExpanded: boolean;
+}
+
+export class RegistrationApplication extends Component<
+  RegistrationApplicationProps,
+  RegistrationApplicationState
+> {
+  private emptyState: RegistrationApplicationState = {
+    denyReason: this.props.application.registration_application.deny_reason,
+    denyExpanded: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
+  }
+
+  render() {
+    let a = this.props.application;
+    let ra = this.props.application.registration_application;
+    let accepted = a.creator_local_user.accepted_application;
+
+    return (
+      <div>
+        <div>
+          {i18n.t("applicant")}: <PersonListing person={a.creator} />
+        </div>
+        <div>
+          {i18n.t("created")}: <MomentTime showAgo data={ra} />
+        </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">
+                  #
+                  <PersonListing person={a.admin} />
+                </T>
+                <div>
+                  {i18n.t("deny_reason")}:{" "}
+                  <div
+                    className="md-div d-inline-flex"
+                    dangerouslySetInnerHTML={mdToHtml(ra.deny_reason || "")}
+                  />
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+
+        {this.state.denyExpanded && (
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">
+              {i18n.t("deny_reason")}
+            </label>
+            <div class="col-sm-10">
+              <MarkdownTextArea
+                initialContent={this.state.denyReason}
+                onContentChange={this.handleDenyReasonChange}
+                hideNavigationWarnings
+              />
+            </div>
+          </div>
+        )}
+        {(!ra.admin_id || (ra.admin_id && !accepted)) && (
+          <button
+            className="btn btn-secondary mr-2 my-2"
+            onClick={linkEvent(this, this.handleApprove)}
+            aria-label={i18n.t("approve")}
+          >
+            {i18n.t("approve")}
+          </button>
+        )}
+        {(!ra.admin_id || (ra.admin_id && accepted)) && (
+          <button
+            className="btn btn-secondary mr-2"
+            onClick={linkEvent(this, this.handleDeny)}
+            aria-label={i18n.t("deny")}
+          >
+            {i18n.t("deny")}
+          </button>
+        )}
+      </div>
+    );
+  }
+
+  handleApprove(i: RegistrationApplication) {
+    i.setState({ denyExpanded: false });
+    let form: ApproveRegistrationApplication = {
+      id: i.props.application.registration_application.id,
+      deny_reason: "",
+      approve: true,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(
+      wsClient.approveRegistrationApplication(form)
+    );
+  }
+
+  handleDeny(i: RegistrationApplication) {
+    if (i.state.denyExpanded) {
+      i.setState({ denyExpanded: false });
+      let form: ApproveRegistrationApplication = {
+        id: i.props.application.registration_application.id,
+        approve: false,
+        deny_reason: i.state.denyReason,
+        auth: authField(),
+      };
+      WebSocketService.Instance.send(
+        wsClient.approveRegistrationApplication(form)
+      );
+    } else {
+      i.setState({ denyExpanded: true });
+    }
+  }
+
+  handleDenyReasonChange(val: string) {
+    this.state.denyReason = val;
+    this.setState(this.state);
+  }
+}
diff --git a/src/shared/components/common/symbols.tsx b/src/shared/components/common/symbols.tsx
index 2035d3c..d730c15 100644
--- a/src/shared/components/common/symbols.tsx
+++ b/src/shared/components/common/symbols.tsx
@@ -12,6 +12,9 @@ export const SYMBOLS = (
     xmlnsXlink="http://www.w3.org/1999/xlink"
   >
     <defs>
+      <symbol id="icon-clipboard" viewBox="0 0 24 24">
+        <path d="M7 5c0 0.552 0.225 1.053 0.586 1.414s0.862 0.586 1.414 0.586h6c0.552 0 1.053-0.225 1.414-0.586s0.586-0.862 0.586-1.414h1c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM9 1c-0.552 0-1.053 0.225-1.414 0.586s-0.586 0.862-0.586 1.414h-1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h12c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-1c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586zM9 3h6v2h-6z"></path>
+      </symbol>
       <symbol id="icon-shield" viewBox="0 0 24 24">
         <path d="M12 20.862c-1.184-0.672-4.42-2.695-6.050-5.549-0.079-0.138-0.153-0.276-0.223-0.417-0.456-0.911-0.727-1.878-0.727-2.896v-6.307l7-2.625 7 2.625v6.307c0 1.018-0.271 1.985-0.726 2.897-0.070 0.14-0.145 0.279-0.223 0.417-1.631 2.854-4.867 4.876-6.050 5.549zM12.447 22.894c0 0 4.989-2.475 7.34-6.589 0.096-0.168 0.188-0.34 0.276-0.515 0.568-1.135 0.937-2.408 0.937-3.79v-7c0-0.426-0.267-0.79-0.649-0.936l-8-3c-0.236-0.089-0.485-0.082-0.702 0l-8 3c-0.399 0.149-0.646 0.527-0.649 0.936v7c0 1.382 0.369 2.655 0.938 3.791 0.087 0.175 0.179 0.346 0.276 0.515 2.351 4.114 7.34 6.589 7.34 6.589 0.292 0.146 0.62 0.136 0.894 0z"></path>
       </symbol>
diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx
index f44d4cc..d145145 100644
--- a/src/shared/components/home/home.tsx
+++ b/src/shared/components/home/home.tsx
@@ -239,6 +239,7 @@ export class Home extends Component<any, HomeState> {
       sort: SortType.Hot,
       limit: 6,
     };
+    setOptionalAuth(trendingCommunitiesForm, req.auth);
     promises.push(req.client.listCommunities(trendingCommunitiesForm));
 
     return promises;
diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx
index 73d7dbe..5a61d13 100644
--- a/src/shared/components/home/login.tsx
+++ b/src/shared/components/home/login.tsx
@@ -185,8 +185,6 @@ export class Login extends Component<any, State> {
     if (msg.error) {
       toast(i18n.t(msg.error), "danger");
       this.state = this.emptyState;
-      // Refetch another captcha
-      WebSocketService.Instance.send(wsClient.getCaptcha());
       this.setState(this.state);
       return;
     } else {
diff --git a/src/shared/components/home/signup.tsx b/src/shared/components/home/signup.tsx
index c3fcd20..75e7856 100644
--- a/src/shared/components/home/signup.tsx
+++ b/src/shared/components/home/signup.tsx
@@ -17,6 +17,7 @@ import {
   authField,
   isBrowser,
   joinLemmyUrl,
+  mdToHtml,
   setIsoData,
   toast,
   validEmail,
@@ -27,6 +28,7 @@ import {
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
+import { MarkdownTextArea } from "../common/markdown-textarea";
 
 const passwordStrengthOptions: Options<string> = [
   {
@@ -77,6 +79,7 @@ export class Signup extends Component<any, State> {
       captcha_uuid: undefined,
       captcha_answer: undefined,
       honeypot: undefined,
+      answer: undefined,
     },
     registerLoading: false,
     captcha: undefined,
@@ -88,6 +91,7 @@ export class Signup extends Component<any, State> {
     super(props, context);
 
     this.state = this.emptyState;
+    this.handleAnswerChange = this.handleAnswerChange.bind(this);
 
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
@@ -104,7 +108,13 @@ export class Signup extends Component<any, State> {
   }
 
   get documentTitle(): string {
-    return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
+    return `${this.titleName} - ${this.state.site_view.site.name}`;
+  }
+
+  get titleName(): string {
+    return `${i18n.t(
+      this.state.site_view.site.private_instance ? "apply_to_join" : "sign_up"
+    )}`;
   }
 
   get isLemmyMl(): boolean {
@@ -128,7 +138,7 @@ export class Signup extends Component<any, State> {
   registerForm() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h5>{i18n.t("sign_up")}</h5>
+        <h5>{this.titleName}</h5>
 
         <div class="form-group row">
           <label class="col-sm-2 col-form-label" htmlFor="register-username">
@@ -159,18 +169,24 @@ export class Signup extends Component<any, State> {
               type="email"
               id="register-email"
               class="form-control"
-              placeholder={i18n.t("optional")}
+              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}
             />
-            {!validEmail(this.state.registerForm.email) && (
-              <div class="mt-2 mb-0 alert alert-light" role="alert">
-                <Icon icon="alert-triangle" classes="icon-inline mr-2" />
-                {i18n.t("no_password_reset")}
-              </div>
-            )}
+            {!this.state.site_view.site.require_email_verification &&
+              !validEmail(this.state.registerForm.email) && (
+                <div class="mt-2 mb-0 alert alert-light" role="alert">
+                  <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+                  {i18n.t("no_password_reset")}
+                </div>
+              )}
           </div>
         </div>
 
@@ -219,6 +235,40 @@ export class Signup extends Component<any, State> {
           </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-light" role="alert">
+                  <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+                  {i18n.t("fill_out_application")}
+                </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>
+          </>
+        )}
+
         {this.state.captcha && (
           <div class="form-group row">
             <label class="col-sm-2" htmlFor="register-captcha">
@@ -286,7 +336,7 @@ export class Signup extends Component<any, State> {
         <div class="form-group row">
           <div class="col-sm-10">
             <button type="submit" class="btn btn-secondary">
-              {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
+              {this.state.registerLoading ? <Spinner /> : this.titleName}
             </button>
           </div>
         </div>
@@ -382,6 +432,11 @@ export class Signup extends Component<any, State> {
     i.setState(i.state);
   }
 
+  handleAnswerChange(val: string) {
+    this.state.registerForm.answer = val;
+    this.setState(this.state);
+  }
+
   handleHoneyPotChange(i: Signup, event: any) {
     i.state.registerForm.honeypot = event.target.value;
     i.setState(i.state);
@@ -434,13 +489,24 @@ export class Signup extends Component<any, State> {
         let data = wsJsonToRes<LoginResponse>(msg).data;
         this.state = this.emptyState;
         this.setState(this.state);
-        UserService.Instance.login(data);
-        WebSocketService.Instance.send(
-          wsClient.userJoin({
-            auth: authField(),
-          })
-        );
-        this.props.history.push("/communities");
+        // Only log them in if a jwt was set
+        if (data.jwt) {
+          UserService.Instance.login(data);
+          WebSocketService.Instance.send(
+            wsClient.userJoin({
+              auth: authField(),
+            })
+          );
+          this.props.history.push("/communities");
+        } else {
+          if (data.verify_email_sent) {
+            toast(i18n.t("verify_email_sent"));
+          }
+          if (data.registration_created) {
+            toast(i18n.t("registration_application_sent"));
+          }
+          this.props.history.push("/");
+        }
       } else if (op == UserOperation.GetCaptcha) {
         let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
         if (data.ok) {
diff --git a/src/shared/components/home/site-form.tsx b/src/shared/components/home/site-form.tsx
index 6bfed42..ba1ce38 100644
--- a/src/shared/components/home/site-form.tsx
+++ b/src/shared/components/home/site-form.tsx
@@ -3,12 +3,7 @@ import { Prompt } from "inferno-router";
 import { CreateSite, EditSite, Site } from "lemmy-js-client";
 import { i18n } from "../../i18next";
 import { WebSocketService } from "../../services";
-import {
-  authField,
-  capitalizeFirstLetter,
-  randomStr,
-  wsClient,
-} from "../../utils";
+import { authField, capitalizeFirstLetter, wsClient } from "../../utils";
 import { Spinner } from "../common/icon";
 import { ImageUploadForm } from "../common/image-upload-form";
 import { MarkdownTextArea } from "../common/markdown-textarea";
@@ -24,7 +19,6 @@ interface SiteFormState {
 }
 
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
-  private id = `site-form-${randomStr()}`;
   private emptyState: SiteFormState = {
     siteForm: {
       enable_downvotes: true,
@@ -33,6 +27,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
       name: null,
       icon: null,
       banner: null,
+      require_email_verification: null,
+      require_application: null,
+      application_question: null,
+      private_instance: null,
       auth: authField(),
     },
     loading: false,
@@ -43,6 +41,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
 
     this.state = this.emptyState;
     this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
+    this.handleSiteApplicationQuestionChange =
+      this.handleSiteApplicationQuestionChange.bind(this);
 
     this.handleIconUpload = this.handleIconUpload.bind(this);
     this.handleIconRemove = this.handleIconRemove.bind(this);
@@ -51,17 +51,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
     if (this.props.site) {
+      let site = this.props.site;
       this.state.siteForm = {
-        name: this.props.site.name,
-        sidebar: this.props.site.sidebar,
-        description: this.props.site.description,
-        enable_downvotes: this.props.site.enable_downvotes,
-        open_registration: this.props.site.open_registration,
-        enable_nsfw: this.props.site.enable_nsfw,
-        community_creation_admin_only:
-          this.props.site.community_creation_admin_only,
-        icon: this.props.site.icon,
-        banner: this.props.site.banner,
+        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,
         auth: authField(),
       };
     }
@@ -79,6 +83,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
       !this.props.site &&
       (this.state.siteForm.name ||
         this.state.siteForm.sidebar ||
+        this.state.siteForm.application_question ||
         this.state.siteForm.description)
     ) {
       window.onbeforeunload = () => true;
@@ -100,6 +105,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             !this.props.site &&
             (this.state.siteForm.name ||
               this.state.siteForm.sidebar ||
+              this.state.siteForm.application_question ||
               this.state.siteForm.description)
           }
           message={i18n.t("block_leaving")}
@@ -162,9 +168,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             </div>
           </div>
           <div class="form-group row">
-            <label class="col-12 col-form-label" htmlFor={this.id}>
-              {i18n.t("sidebar")}
-            </label>
+            <label class="col-12 col-form-label">{i18n.t("sidebar")}</label>
             <div class="col-12">
               <MarkdownTextArea
                 initialContent={this.state.siteForm.sidebar}
@@ -173,6 +177,20 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               />
             </div>
           </div>
+          {this.state.siteForm.require_application && (
+            <div class="form-group row">
+              <label class="col-12 col-form-label">
+                {i18n.t("application_questionnaire")}
+              </label>
+              <div class="col-12">
+                <MarkdownTextArea
+                  initialContent={this.state.siteForm.application_question}
+                  onContentChange={this.handleSiteApplicationQuestionChange}
+                  hideNavigationWarnings
+                />
+              </div>
+            </div>
+          )}
           <div class="form-group row">
             <div class="col-12">
               <div class="form-check">
@@ -255,6 +273,66 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               </div>
             </div>
           </div>
+          <div class="form-group row">
+            <div class="col-12">
+              <div class="form-check">
+                <input
+                  class="form-check-input"
+                  id="create-site-require-email-verification"
+                  type="checkbox"
+                  checked={this.state.siteForm.require_email_verification}
+                  onChange={linkEvent(
+                    this,
+                    this.handleSiteRequireEmailVerification
+                  )}
+                />
+                <label
+                  class="form-check-label"
+                  htmlFor="create-site-require-email-verification"
+                >
+                  {i18n.t("require_email_verification")}
+                </label>
+              </div>
+            </div>
+          </div>
+          <div class="form-group row">
+            <div class="col-12">
+              <div class="form-check">
+                <input
+                  class="form-check-input"
+                  id="create-site-require-application"
+                  type="checkbox"
+                  checked={this.state.siteForm.require_application}
+                  onChange={linkEvent(this, this.handleSiteRequireApplication)}
+                />
+                <label
+                  class="form-check-label"
+                  htmlFor="create-site-require-application"
+                >
+                  {i18n.t("require_registration_application")}
+                </label>
+              </div>
+            </div>
+          </div>
+          <div class="form-group row">
+            <div class="col-12">
+              <div class="form-check">
+                <input
+                  class="form-check-input"
+                  id="create-site-private-instance"
+                  type="checkbox"
+                  checked={this.state.siteForm.private_instance}
+                  onChange={linkEvent(this, this.handleSitePrivateInstance)}
+                />
+                <label
+                  class="form-check-label"
+                  htmlFor="create-site-private-instance"
+                >
+                  {i18n.t("private_instance")}
+                </label>
+              </div>
+            </div>
+          </div>
           <div class="form-group row">
             <div class="col-12">
               <button
@@ -311,6 +389,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     this.setState(this.state);
   }
 
+  handleSiteApplicationQuestionChange(val: string) {
+    this.state.siteForm.application_question = val;
+    this.setState(this.state);
+  }
+
   handleSiteDescChange(i: SiteForm, event: any) {
     i.state.siteForm.description = event.target.value;
     i.setState(i.state);
@@ -336,6 +419,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     i.setState(i.state);
   }
 
+  handleSiteRequireApplication(i: SiteForm, event: any) {
+    i.state.siteForm.require_application = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleSiteRequireEmailVerification(i: SiteForm, event: any) {
+    i.state.siteForm.require_email_verification = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleSitePrivateInstance(i: SiteForm, event: any) {
+    i.state.siteForm.private_instance = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleCancel(i: SiteForm) {
     i.props.onCancel();
   }
diff --git a/src/shared/components/modlog.tsx b/src/shared/components/modlog.tsx
index 2e8527d..cc3c7a1 100644
--- a/src/shared/components/modlog.tsx
+++ b/src/shared/components/modlog.tsx
@@ -25,9 +25,11 @@ import { i18n } from "../i18next";
 import { InitialFetchRequest } from "../interfaces";
 import { UserService, WebSocketService } from "../services";
 import {
+  authField,
   fetchLimit,
   isBrowser,
   setIsoData,
+  setOptionalAuth,
   toast,
   wsClient,
   wsJsonToRes,
@@ -482,6 +484,7 @@ export class Modlog extends Component<any, ModlogState> {
       community_id: this.state.communityId,
       page: this.state.page,
       limit: fetchLimit,
+      auth: authField(false),
     };
     WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
 
@@ -507,6 +510,7 @@ export class Modlog extends Component<any, ModlogState> {
     if (communityId) {
       modlogForm.community_id = Number(communityId);
     }
+    setOptionalAuth(modlogForm, req.auth);
 
     promises.push(req.client.getModlog(modlogForm));
 
@@ -514,6 +518,7 @@ export class Modlog extends Component<any, ModlogState> {
       let communityForm: GetCommunity = {
         id: Number(communityId),
       };
+      setOptionalAuth(communityForm, req.auth);
       promises.push(req.client.getCommunity(communityForm));
     }
     return promises;
diff --git a/src/shared/components/home/password_change.tsx b/src/shared/components/person/password-change.tsx
similarity index 100%
rename from src/shared/components/home/password_change.tsx
rename to src/shared/components/person/password-change.tsx
diff --git a/src/shared/components/person/registration-applications.tsx b/src/shared/components/person/registration-applications.tsx
new file mode 100644
index 0000000..9009f74
--- /dev/null
+++ b/src/shared/components/person/registration-applications.tsx
@@ -0,0 +1,249 @@
+import { Component, linkEvent } from "inferno";
+import {
+  ListRegistrationApplications,
+  ListRegistrationApplicationsResponse,
+  RegistrationApplicationResponse,
+  RegistrationApplicationView,
+  SiteView,
+  UserOperation,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { InitialFetchRequest } from "../../interfaces";
+import { UserService, WebSocketService } from "../../services";
+import {
+  authField,
+  fetchLimit,
+  isBrowser,
+  setIsoData,
+  setupTippy,
+  toast,
+  updateRegistrationApplicationRes,
+  wsClient,
+  wsJsonToRes,
+  wsSubscribe,
+  wsUserOp,
+} from "../../utils";
+import { HtmlTags } from "../common/html-tags";
+import { Spinner } from "../common/icon";
+import { Paginator } from "../common/paginator";
+import { RegistrationApplication } from "../common/registration-application";
+
+enum UnreadOrAll {
+  Unread,
+  All,
+}
+
+interface RegistrationApplicationsState {
+  applications: RegistrationApplicationView[];
+  page: number;
+  site_view: SiteView;
+  unreadOrAll: UnreadOrAll;
+  loading: boolean;
+}
+
+export class RegistrationApplications extends Component<
+  any,
+  RegistrationApplicationsState
+> {
+  private isoData = setIsoData(this.context);
+  private subscription: Subscription;
+  private emptyState: RegistrationApplicationsState = {
+    unreadOrAll: UnreadOrAll.Unread,
+    applications: [],
+    page: 1,
+    site_view: this.isoData.site_res.site_view,
+    loading: true,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handlePageChange = this.handlePageChange.bind(this);
+
+    if (!UserService.Instance.myUserInfo && isBrowser()) {
+      toast(i18n.t("not_logged_in"), "danger");
+      this.context.router.history.push(`/login`);
+    }
+
+    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) {
+      this.state.applications =
+        this.isoData.routeData[0].registration_applications || []; // TODO test
+      this.state.loading = false;
+    } else {
+      this.refetch();
+    }
+  }
+
+  componentDidMount() {
+    setupTippy();
+  }
+
+  componentWillUnmount() {
+    if (isBrowser()) {
+      this.subscription.unsubscribe();
+    }
+  }
+
+  get documentTitle(): string {
+    return `@${
+      UserService.Instance.myUserInfo.local_user_view.person.name
+    } ${i18n.t("registration_applications")} - ${
+      this.state.site_view.site.name
+    }`;
+  }
+
+  render() {
+    return (
+      <div class="container">
+        {this.state.loading ? (
+          <h5>
+            <Spinner large />
+          </h5>
+        ) : (
+          <div class="row">
+            <div class="col-12">
+              <HtmlTags
+                title={this.documentTitle}
+                path={this.context.router.route.match.url}
+              />
+              <h5 class="mb-2">{i18n.t("registration_applications")}</h5>
+              {this.selects()}
+              {this.applicationList()}
+              <Paginator
+                page={this.state.page}
+                onChange={this.handlePageChange}
+              />
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  unreadOrAllRadios() {
+    return (
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={UnreadOrAll.Unread}
+            checked={this.state.unreadOrAll == UnreadOrAll.Unread}
+            onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+          />
+          {i18n.t("unread")}
+        </label>
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={UnreadOrAll.All}
+            checked={this.state.unreadOrAll == UnreadOrAll.All}
+            onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+          />
+          {i18n.t("all")}
+        </label>
+      </div>
+    );
+  }
+
+  selects() {
+    return (
+      <div className="mb-2">
+        <span class="mr-3">{this.unreadOrAllRadios()}</span>
+      </div>
+    );
+  }
+
+  applicationList() {
+    return (
+      <div>
+        {this.state.applications.map(ra => (
+          <>
+            <hr />
+            <RegistrationApplication
+              key={ra.registration_application.id}
+              application={ra}
+            />
+          </>
+        ))}
+      </div>
+    );
+  }
+
+  handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
+    i.state.unreadOrAll = Number(event.target.value);
+    i.state.page = 1;
+    i.setState(i.state);
+    i.refetch();
+  }
+
+  handlePageChange(page: number) {
+    this.setState({ page });
+    this.refetch();
+  }
+
+  static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
+    let promises: Promise<any>[] = [];
+
+    let form: ListRegistrationApplications = {
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+      auth: req.auth,
+    };
+    promises.push(req.client.listRegistrationApplications(form));
+
+    return promises;
+  }
+
+  refetch() {
+    let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+    let form: ListRegistrationApplications = {
+      unread_only: unread_only,
+      page: this.state.page,
+      limit: fetchLimit,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(wsClient.listRegistrationApplications(form));
+  }
+
+  parseMessage(msg: any) {
+    let op = wsUserOp(msg);
+    console.log(msg);
+    if (msg.error) {
+      toast(i18n.t(msg.error), "danger");
+      return;
+    } else if (msg.reconnect) {
+      this.refetch();
+    } else if (op == UserOperation.ListRegistrationApplications) {
+      let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg).data;
+      this.state.applications = data.registration_applications;
+      this.state.loading = false;
+      window.scrollTo(0, 0);
+      this.setState(this.state);
+    } else if (op == UserOperation.ApproveRegistrationApplication) {
+      let data = wsJsonToRes<RegistrationApplicationResponse>(msg).data;
+      updateRegistrationApplicationRes(
+        data.registration_application,
+        this.state.applications
+      );
+      let uacs = UserService.Instance.unreadApplicationCountSub;
+      // Minor bug, where if the application switches from deny to approve, the count will still go down
+      uacs.next(uacs.getValue() - 1);
+      this.setState(this.state);
+    }
+  }
+}
diff --git a/src/shared/components/person/reports.tsx b/src/shared/components/person/reports.tsx
index 2c83ff2..99edf96 100644
--- a/src/shared/components/person/reports.tsx
+++ b/src/shared/components/person/reports.tsx
@@ -29,11 +29,11 @@ import {
   wsSubscribe,
   wsUserOp,
 } from "../../utils";
-import { CommentReport } from "../comment/comment_report";
+import { CommentReport } from "../comment/comment-report";
 import { HtmlTags } from "../common/html-tags";
 import { Spinner } from "../common/icon";
 import { Paginator } from "../common/paginator";
-import { PostReport } from "../post/post_report";
+import { PostReport } from "../post/post-report";
 
 enum UnreadOrAll {
   Unread,
diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx
index dff2959..322dfa9 100644
--- a/src/shared/components/person/settings.tsx
+++ b/src/shared/components/person/settings.tsx
@@ -1108,6 +1108,11 @@ export class Settings extends Component<any, SettingsState> {
     let op = wsUserOp(msg);
     console.log(msg);
     if (msg.error) {
+      this.setState({
+        saveUserSettingsLoading: false,
+        changePasswordLoading: false,
+        deleteAccountLoading: false,
+      });
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.SaveUserSettings) {
diff --git a/src/shared/components/person/verify-email.tsx b/src/shared/components/person/verify-email.tsx
new file mode 100644
index 0000000..d27a8bb
--- /dev/null
+++ b/src/shared/components/person/verify-email.tsx
@@ -0,0 +1,97 @@
+import { Component } from "inferno";
+import {
+  SiteView,
+  UserOperation,
+  VerifyEmail as VerifyEmailForm,
+  VerifyEmailResponse,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { WebSocketService } from "../../services";
+import {
+  isBrowser,
+  setIsoData,
+  toast,
+  wsClient,
+  wsJsonToRes,
+  wsSubscribe,
+  wsUserOp,
+} from "../../utils";
+import { HtmlTags } from "../common/html-tags";
+
+interface State {
+  verifyEmailForm: VerifyEmailForm;
+  site_view: SiteView;
+}
+
+export class VerifyEmail extends Component<any, State> {
+  private isoData = setIsoData(this.context);
+  private subscription: Subscription;
+
+  emptyState: State = {
+    verifyEmailForm: {
+      token: this.props.match.params.token,
+    },
+    site_view: this.isoData.site_res.site_view,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
+  }
+
+  componentDidMount() {
+    WebSocketService.Instance.send(
+      wsClient.verifyEmail(this.state.verifyEmailForm)
+    );
+  }
+
+  componentWillUnmount() {
+    if (isBrowser()) {
+      this.subscription.unsubscribe();
+    }
+  }
+
+  get documentTitle(): string {
+    return `${i18n.t("verify_email")} - ${this.state.site_view.site.name}`;
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <HtmlTags
+          title={this.documentTitle}
+          path={this.context.router.route.match.url}
+        />
+        <div class="row">
+          <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+            <h5>{i18n.t("verify_email")}</h5>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  parseMessage(msg: any) {
+    let op = wsUserOp(msg);
+    console.log(msg);
+    if (msg.error) {
+      toast(i18n.t(msg.error), "danger");
+      this.setState(this.state);
+      this.props.history.push("/");
+      return;
+    } else if (op == UserOperation.VerifyEmail) {
+      let data = wsJsonToRes<VerifyEmailResponse>(msg).data;
+      if (data) {
+        toast(i18n.t("email_verified"));
+        this.state = this.emptyState;
+        this.setState(this.state);
+        this.props.history.push("/login");
+      }
+    }
+  }
+}
diff --git a/src/shared/components/post/post_report.tsx b/src/shared/components/post/post-report.tsx
similarity index 92%
rename from src/shared/components/post/post_report.tsx
rename to src/shared/components/post/post-report.tsx
index f2e1734..ff3368e 100644
--- a/src/shared/components/post/post_report.tsx
+++ b/src/shared/components/post/post-report.tsx
@@ -20,6 +20,9 @@ export class PostReport extends Component<PostReportProps, any> {
   render() {
     let r = this.props.report;
     let post = r.post;
+    let tippyContent = i18n.t(
+      r.post_report.resolved ? "unresolve_report" : "resolve_report"
+    );
 
     // Set the original post data ( a troll could change it )
     post.name = r.post_report.original_post_name;
@@ -70,12 +73,8 @@ export class PostReport extends Component<PostReportProps, any> {
         <button
           className="btn btn-link btn-animate text-muted py-0"
           onClick={linkEvent(this, this.handleResolveReport)}
-          data-tippy-content={
-            r.post_report.resolved ? "unresolve_report" : "resolve_report"
-          }
-          aria-label={
-            r.post_report.resolved ? "unresolve_report" : "resolve_report"
-          }
+          data-tippy-content={tippyContent}
+          aria-label={tippyContent}
         >
           <Icon
             icon="check"
diff --git a/src/shared/routes.ts b/src/shared/routes.ts
index ddc3b62..86c0b3c 100644
--- a/src/shared/routes.ts
+++ b/src/shared/routes.ts
@@ -6,14 +6,16 @@ import { AdminSettings } from "./components/home/admin-settings";
 import { Home } from "./components/home/home";
 import { Instances } from "./components/home/instances";
 import { Login } from "./components/home/login";
-import { PasswordChange } from "./components/home/password_change";
 import { Setup } from "./components/home/setup";
 import { Signup } from "./components/home/signup";
 import { Modlog } from "./components/modlog";
 import { Inbox } from "./components/person/inbox";
+import { PasswordChange } from "./components/person/password-change";
 import { Profile } from "./components/person/profile";
+import { RegistrationApplications } from "./components/person/registration-applications";
 import { Reports } from "./components/person/reports";
 import { Settings } from "./components/person/settings";
+import { VerifyEmail } from "./components/person/verify-email";
 import { CreatePost } from "./components/post/create-post";
 import { Post } from "./components/post/post";
 import { CreatePrivateMessage } from "./components/private_message/create-private-message";
@@ -128,6 +130,11 @@ export const routes: IRoutePropsWithFetch[] = [
     component: Reports,
     fetchInitialData: req => Reports.fetchInitialData(req),
   },
+  {
+    path: `/registration_applications`,
+    component: RegistrationApplications,
+    fetchInitialData: req => RegistrationApplications.fetchInitialData(req),
+  },
   {
     path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
     component: Search,
@@ -142,5 +149,9 @@ export const routes: IRoutePropsWithFetch[] = [
     path: `/password_change/:token`,
     component: PasswordChange,
   },
+  {
+    path: `/verify_email/:token`,
+    component: VerifyEmail,
+  },
   { path: `/instances`, component: Instances },
 ];
diff --git a/src/shared/services/UserService.ts b/src/shared/services/UserService.ts
index 0c87f7d..031cf7d 100644
--- a/src/shared/services/UserService.ts
+++ b/src/shared/services/UserService.ts
@@ -20,6 +20,8 @@ export class UserService {
     new BehaviorSubject<number>(0);
   public unreadReportCountSub: BehaviorSubject<number> =
     new BehaviorSubject<number>(0);
+  public unreadApplicationCountSub: BehaviorSubject<number> =
+    new BehaviorSubject<number>(0);
 
   private constructor() {
     if (this.auth) {
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 28929a2..784a30f 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -18,6 +18,7 @@ import {
   PostReportView,
   PostView,
   PrivateMessageView,
+  RegistrationApplicationView,
   Search,
   SearchResponse,
   SearchType,
@@ -1105,6 +1106,20 @@ export function updateCommentReportRes(
   }
 }
 
+export function updateRegistrationApplicationRes(
+  data: RegistrationApplicationView,
+  applications: RegistrationApplicationView[]
+) {
+  let found = applications.find(
+    ra => ra.registration_application.id == data.registration_application.id
+  );
+  if (found) {
+    found.registration_application = data.registration_application;
+    found.admin = data.admin;
+    found.creator_local_user = data.creator_local_user;
+  }
+}
+
 export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
   let nodes: CommentNodeI[] = [];
   for (let comment of comments) {
diff --git a/yarn.lock b/yarn.lock
index 62df36c..0815ba3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4997,10 +4997,10 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-lemmy-js-client@0.14.0-rc.1:
-  version "0.14.0-rc.1"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.14.0-rc.1.tgz#c714d5f308fa20d5244db3844630f7b197eafa1c"
-  integrity sha512-UF3I+80WTYWwQg2+96HTl0O2Yv0wy6rYFjlLNyzfqMXUZBnsr1O/SdJD1/9yAFPFbGkKgWusdncLoGgzFyn8eg==
+lemmy-js-client@0.15.0-rc.6:
+  version "0.15.0-rc.6"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.15.0-rc.6.tgz#5f8552488ed82b8c0962c158edccb8ce1d56389e"
+  integrity sha512-eSEZ5+F2ScKVtx+wwjdReHirJBNLQL2YdTV4aMCBWaSsxfsXUcz18/urbNxo+fNMc7Q4u0aRd3737yKBeMP9Kw==
 
 levn@^0.4.1:
   version "0.4.1"
-- 
2.44.1