]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/app/navbar.tsx
Merge remote-tracking branch 'origin/main' into feat/add-post-body-preview-to-desktop
[lemmy-ui.git] / src / shared / components / app / navbar.tsx
index d508c4abae7db2d9085a9eb2332aa9084b8f4aea..2ede00e183be2d0234adc3c56c205324f1fac140 100644 (file)
@@ -1,36 +1,19 @@
-import { Component, linkEvent } from "inferno";
+import { myAuth, showAvatars } from "@utils/app";
+import { isBrowser } from "@utils/browser";
+import { numToSI, poll } from "@utils/helpers";
+import { amAdmin, canCreateCommunity } from "@utils/roles";
+import { Component, createRef, linkEvent } from "inferno";
 import { NavLink } from "inferno-router";
 import {
-  CommentResponse,
-  GetReportCount,
   GetReportCountResponse,
   GetSiteResponse,
-  GetUnreadCount,
   GetUnreadCountResponse,
-  GetUnreadRegistrationApplicationCount,
   GetUnreadRegistrationApplicationCountResponse,
-  PrivateMessageResponse,
-  UserOperation,
-  wsJsonToRes,
-  wsUserOp,
 } from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { i18n } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
-import {
-  amAdmin,
-  canCreateCommunity,
-  donateLemmyUrl,
-  isBrowser,
-  myAuth,
-  notifyComment,
-  notifyPrivateMessage,
-  numToSI,
-  showAvatars,
-  toast,
-  wsClient,
-  wsSubscribe,
-} from "../../utils";
+import { donateLemmyUrl, updateUnreadCountsInterval } from "../../config";
+import { I18NextService, UserService } from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
+import { toast } from "../../toast";
 import { Icon } from "../common/icon";
 import { PictrsImage } from "../common/pictrs-image";
 
@@ -39,543 +22,487 @@ interface NavbarProps {
 }
 
 interface NavbarState {
-  expanded: boolean;
-  unreadInboxCount: number;
-  unreadReportCount: number;
-  unreadApplicationCount: number;
-  showDropdown: boolean;
+  unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
+  unreadReportCountRes: RequestState<GetReportCountResponse>;
+  unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
   onSiteBanner?(url: string): any;
 }
 
+function handleCollapseClick(i: Navbar) {
+  if (
+    i.collapseButtonRef.current?.attributes &&
+    i.collapseButtonRef.current?.attributes.getNamedItem("aria-expanded")
+      ?.value === "true"
+  ) {
+    i.collapseButtonRef.current?.click();
+  }
+}
+
+function handleLogOut(i: Navbar) {
+  UserService.Instance.logout();
+  handleCollapseClick(i);
+}
+
 export class Navbar extends Component<NavbarProps, NavbarState> {
-  private wsSub: Subscription;
-  private userSub: Subscription;
-  private unreadInboxCountSub: Subscription;
-  private unreadReportCountSub: Subscription;
-  private unreadApplicationCountSub: Subscription;
   state: NavbarState = {
-    unreadInboxCount: 0,
-    unreadReportCount: 0,
-    unreadApplicationCount: 0,
-    expanded: false,
-    showDropdown: false,
+    unreadInboxCountRes: { state: "empty" },
+    unreadReportCountRes: { state: "empty" },
+    unreadApplicationCountRes: { state: "empty" },
   };
-  subscription: any;
+  collapseButtonRef = createRef<HTMLButtonElement>();
+  mobileMenuRef = createRef<HTMLDivElement>();
 
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
+    this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
   }
 
-  componentDidMount() {
+  async componentDidMount() {
     // Subscribe to jwt changes
     if (isBrowser()) {
       // On the first load, check the unreads
-      let auth = myAuth(false);
-      if (auth && UserService.Instance.myUserInfo) {
-        this.requestNotificationPermission();
-        WebSocketService.Instance.send(
-          wsClient.userJoin({
-            auth,
-          })
-        );
-
-        this.fetchUnreads();
-      }
-
+      this.requestNotificationPermission();
+      this.fetchUnreads();
       this.requestNotificationPermission();
 
-      // Subscribe to unread count changes
-      this.unreadInboxCountSub =
-        UserService.Instance.unreadInboxCountSub.subscribe(res => {
-          this.setState({ unreadInboxCount: res });
-        });
-      // Subscribe to unread report count changes
-      this.unreadReportCountSub =
-        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 });
-        });
+      document.addEventListener("mouseup", this.handleOutsideMenuClick);
     }
   }
 
   componentWillUnmount() {
-    this.wsSub.unsubscribe();
-    this.userSub.unsubscribe();
-    this.unreadInboxCountSub.unsubscribe();
-    this.unreadReportCountSub.unsubscribe();
-    this.unreadApplicationCountSub.unsubscribe();
+    document.removeEventListener("mouseup", this.handleOutsideMenuClick);
   }
 
+  // TODO class active corresponding to current pages
   render() {
-    return this.navbar();
-  }
-
-  // TODO class active corresponding to current page
-  navbar() {
-    let siteView = this.props.siteRes?.site_view;
-    let person = UserService.Instance.myUserInfo?.local_user_view.person;
+    const siteView = this.props.siteRes?.site_view;
+    const person = UserService.Instance.myUserInfo?.local_user_view.person;
     return (
-      <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
-        <div className="container-lg">
-          <NavLink
-            to="/"
-            onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-            title={siteView?.site.description ?? siteView?.site.name ?? "Lemmy"}
-            className="d-flex align-items-center navbar-brand mr-md-3"
-          >
-            {siteView?.site.icon && showAvatars() && (
-              <PictrsImage src={siteView.site.icon} icon />
-            )}
-            {siteView?.site.name ?? "Lemmy"}
-          </NavLink>
-          {UserService.Instance.myUserInfo && (
-            <>
-              <ul className="navbar-nav ml-auto">
-                <li className="nav-item">
-                  <NavLink
-                    to="/inbox"
-                    className="p-1 navbar-toggler nav-link border-0"
-                    onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                    title={i18n.t("unread_messages", {
-                      count: Number(this.state.unreadInboxCount),
-                      formattedCount: numToSI(this.state.unreadInboxCount),
-                    })}
-                  >
-                    <Icon icon="bell" />
-                    {this.state.unreadInboxCount > 0 && (
-                      <span className="mx-1 badge badge-light">
-                        {numToSI(this.state.unreadInboxCount)}
-                      </span>
-                    )}
-                  </NavLink>
-                </li>
-              </ul>
-              {this.moderatesSomething && (
-                <ul className="navbar-nav ml-1">
-                  <li className="nav-item">
-                    <NavLink
-                      to="/reports"
-                      className="p-1 navbar-toggler nav-link border-0"
-                      onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                      title={i18n.t("unread_reports", {
-                        count: Number(this.state.unreadReportCount),
-                        formattedCount: numToSI(this.state.unreadReportCount),
-                      })}
-                    >
-                      <Icon icon="shield" />
-                      {this.state.unreadReportCount > 0 && (
-                        <span className="mx-1 badge badge-light">
-                          {numToSI(this.state.unreadReportCount)}
-                        </span>
-                      )}
-                    </NavLink>
-                  </li>
-                </ul>
-              )}
-              {amAdmin() && (
-                <ul className="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: Number(this.state.unreadApplicationCount),
-                        formattedCount: numToSI(
-                          this.state.unreadApplicationCount
-                        ),
-                      })}
-                    >
-                      <Icon icon="clipboard" />
-                      {this.state.unreadApplicationCount > 0 && (
-                        <span className="mx-1 badge badge-light">
-                          {numToSI(this.state.unreadApplicationCount)}
-                        </span>
-                      )}
-                    </NavLink>
-                  </li>
-                </ul>
-              )}
-            </>
+      <nav
+        className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
+        id="navbar"
+      >
+        <NavLink
+          id="navTitle"
+          to="/"
+          title={siteView?.site.description ?? siteView?.site.name}
+          className="d-flex align-items-center navbar-brand me-md-3"
+          onMouseUp={linkEvent(this, handleCollapseClick)}
+        >
+          {siteView?.site.icon && showAvatars() && (
+            <PictrsImage src={siteView.site.icon} icon />
           )}
-          <button
-            className="navbar-toggler border-0 p-1"
-            type="button"
-            aria-label="menu"
-            onClick={linkEvent(this, this.handleToggleExpandNavbar)}
-            data-tippy-content={i18n.t("expand_here")}
-          >
-            <Icon icon="menu" />
-          </button>
-          <div
-            className={`${!this.state.expanded && "collapse"} navbar-collapse`}
-          >
-            <ul className="navbar-nav my-2 mr-auto">
-              <li className="nav-item">
+          {siteView?.site.name}
+        </NavLink>
+        {person && (
+          <ul className="navbar-nav d-flex flex-row ms-auto d-md-none">
+            <li id="navMessages" className="nav-item nav-item-icon">
+              <NavLink
+                to="/inbox"
+                className="p-1 nav-link border-0 nav-messages"
+                title={I18NextService.i18n.t("unread_messages", {
+                  count: Number(this.state.unreadApplicationCountRes.state),
+                  formattedCount: numToSI(this.unreadInboxCount),
+                })}
+                onMouseUp={linkEvent(this, handleCollapseClick)}
+              >
+                <Icon icon="bell" />
+                {this.unreadInboxCount > 0 && (
+                  <span className="mx-1 badge text-bg-light">
+                    {numToSI(this.unreadInboxCount)}
+                  </span>
+                )}
+              </NavLink>
+            </li>
+            {this.moderatesSomething && (
+              <li className="nav-item nav-item-icon">
                 <NavLink
-                  to="/communities"
-                  className="nav-link"
-                  onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                  title={i18n.t("communities")}
+                  to="/reports"
+                  className="p-1 nav-link border-0"
+                  title={I18NextService.i18n.t("unread_reports", {
+                    count: Number(this.unreadReportCount),
+                    formattedCount: numToSI(this.unreadReportCount),
+                  })}
+                  onMouseUp={linkEvent(this, handleCollapseClick)}
                 >
-                  {i18n.t("communities")}
+                  <Icon icon="shield" />
+                  {this.unreadReportCount > 0 && (
+                    <span className="mx-1 badge text-bg-light">
+                      {numToSI(this.unreadReportCount)}
+                    </span>
+                  )}
                 </NavLink>
               </li>
-              <li className="nav-item">
-                {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
+            )}
+            {amAdmin() && (
+              <li className="nav-item nav-item-icon">
                 <NavLink
-                  to={{
-                    pathname: "/create_post",
-                    search: "",
-                    hash: "",
-                    key: "",
-                    state: { prevPath: this.currentLocation },
-                  }}
-                  className="nav-link"
-                  onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                  title={i18n.t("create_post")}
+                  to="/registration_applications"
+                  className="p-1 nav-link border-0"
+                  title={I18NextService.i18n.t(
+                    "unread_registration_applications",
+                    {
+                      count: Number(this.unreadApplicationCount),
+                      formattedCount: numToSI(this.unreadApplicationCount),
+                    }
+                  )}
+                  onMouseUp={linkEvent(this, handleCollapseClick)}
                 >
-                  {i18n.t("create_post")}
+                  <Icon icon="clipboard" />
+                  {this.unreadApplicationCount > 0 && (
+                    <span className="mx-1 badge text-bg-light">
+                      {numToSI(this.unreadApplicationCount)}
+                    </span>
+                  )}
                 </NavLink>
               </li>
-              {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
-                <li className="nav-item">
-                  <NavLink
-                    to="/create_community"
-                    className="nav-link"
-                    onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                    title={i18n.t("create_community")}
-                  >
-                    {i18n.t("create_community")}
-                  </NavLink>
-                </li>
-              )}
+            )}
+          </ul>
+        )}
+        <button
+          className="navbar-toggler border-0 p-1"
+          type="button"
+          aria-label="menu"
+          data-tippy-content={I18NextService.i18n.t("expand_here")}
+          data-bs-toggle="collapse"
+          data-bs-target="#navbarDropdown"
+          aria-controls="navbarDropdown"
+          aria-expanded="false"
+          ref={this.collapseButtonRef}
+        >
+          <Icon icon="menu" />
+        </button>
+        <div
+          className="collapse navbar-collapse my-2"
+          id="navbarDropdown"
+          ref={this.mobileMenuRef}
+        >
+          <ul id="navbarLinks" className="me-auto navbar-nav">
+            <li className="nav-item">
+              <NavLink
+                to="/communities"
+                className="nav-link"
+                title={I18NextService.i18n.t("communities")}
+                onMouseUp={linkEvent(this, handleCollapseClick)}
+              >
+                {I18NextService.i18n.t("communities")}
+              </NavLink>
+            </li>
+            <li className="nav-item">
+              {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
+              <NavLink
+                to={{
+                  pathname: "/create_post",
+                  search: "",
+                  hash: "",
+                  key: "",
+                  state: { prevPath: this.currentLocation },
+                }}
+                className="nav-link"
+                title={I18NextService.i18n.t("create_post")}
+                onMouseUp={linkEvent(this, handleCollapseClick)}
+              >
+                {I18NextService.i18n.t("create_post")}
+              </NavLink>
+            </li>
+            {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
               <li className="nav-item">
-                <a
+                <NavLink
+                  to="/create_community"
                   className="nav-link"
-                  title={i18n.t("support_lemmy")}
-                  href={donateLemmyUrl}
+                  title={I18NextService.i18n.t("create_community")}
+                  onMouseUp={linkEvent(this, handleCollapseClick)}
                 >
-                  <Icon icon="heart" classes="small" />
-                </a>
+                  {I18NextService.i18n.t("create_community")}
+                </NavLink>
               </li>
-            </ul>
-            <ul className="navbar-nav my-2">
-              {amAdmin() && (
-                <li className="nav-item">
-                  <NavLink
-                    to="/admin"
-                    className="nav-link"
-                    onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                    title={i18n.t("admin_settings")}
-                  >
-                    <Icon icon="settings" />
-                  </NavLink>
-                </li>
-              )}
-            </ul>
-            {!this.context.router.history.location.pathname.match(
-              /^\/search/
-            ) && (
-              <ul className="navbar-nav">
-                <li className="nav-item">
+            )}
+            <li className="nav-item">
+              <a
+                className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                title={I18NextService.i18n.t("support_lemmy")}
+                href={donateLemmyUrl}
+              >
+                <Icon icon="heart" classes="small" />
+                <span className="d-inline ms-1 d-md-none ms-md-0">
+                  {I18NextService.i18n.t("support_lemmy")}
+                </span>
+              </a>
+            </li>
+          </ul>
+          <ul id="navbarIcons" className="navbar-nav">
+            <li id="navSearch" className="nav-item">
+              <NavLink
+                to="/search"
+                className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                title={I18NextService.i18n.t("search")}
+                onMouseUp={linkEvent(this, handleCollapseClick)}
+              >
+                <Icon icon="search" />
+                <span className="d-inline ms-1 d-md-none ms-md-0">
+                  {I18NextService.i18n.t("search")}
+                </span>
+              </NavLink>
+            </li>
+            {amAdmin() && (
+              <li id="navAdmin" className="nav-item">
+                <NavLink
+                  to="/admin"
+                  className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                  title={I18NextService.i18n.t("admin_settings")}
+                  onMouseUp={linkEvent(this, handleCollapseClick)}
+                >
+                  <Icon icon="settings" />
+                  <span className="d-inline ms-1 d-md-none ms-md-0">
+                    {I18NextService.i18n.t("admin_settings")}
+                  </span>
+                </NavLink>
+              </li>
+            )}
+            {person ? (
+              <>
+                <li id="navMessages" className="nav-item">
                   <NavLink
-                    to="/search"
-                    className="nav-link"
-                    title={i18n.t("search")}
+                    className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                    to="/inbox"
+                    title={I18NextService.i18n.t("unread_messages", {
+                      count: Number(this.unreadInboxCount),
+                      formattedCount: numToSI(this.unreadInboxCount),
+                    })}
+                    onMouseUp={linkEvent(this, handleCollapseClick)}
                   >
-                    <Icon icon="search" />
+                    <Icon icon="bell" />
+                    <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
+                      {I18NextService.i18n.t("unread_messages", {
+                        count: Number(this.unreadInboxCount),
+                        formattedCount: numToSI(this.unreadInboxCount),
+                      })}
+                    </span>
+                    {this.unreadInboxCount > 0 && (
+                      <span className="mx-1 badge text-bg-light">
+                        {numToSI(this.unreadInboxCount)}
+                      </span>
+                    )}
                   </NavLink>
                 </li>
-              </ul>
-            )}
-            {UserService.Instance.myUserInfo ? (
-              <>
-                <ul className="navbar-nav my-2">
-                  <li className="nav-item">
+                {this.moderatesSomething && (
+                  <li id="navModeration" className="nav-item">
                     <NavLink
-                      className="nav-link"
-                      to="/inbox"
-                      onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                      title={i18n.t("unread_messages", {
-                        count: Number(this.state.unreadInboxCount),
-                        formattedCount: numToSI(this.state.unreadInboxCount),
+                      className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                      to="/reports"
+                      title={I18NextService.i18n.t("unread_reports", {
+                        count: Number(this.unreadReportCount),
+                        formattedCount: numToSI(this.unreadReportCount),
                       })}
+                      onMouseUp={linkEvent(this, handleCollapseClick)}
                     >
-                      <Icon icon="bell" />
-                      {this.state.unreadInboxCount > 0 && (
-                        <span className="ml-1 badge badge-light">
-                          {numToSI(this.state.unreadInboxCount)}
+                      <Icon icon="shield" />
+                      <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
+                        {I18NextService.i18n.t("unread_reports", {
+                          count: Number(this.unreadReportCount),
+                          formattedCount: numToSI(this.unreadReportCount),
+                        })}
+                      </span>
+                      {this.unreadReportCount > 0 && (
+                        <span className="mx-1 badge text-bg-light">
+                          {numToSI(this.unreadReportCount)}
                         </span>
                       )}
                     </NavLink>
                   </li>
-                </ul>
-                {this.moderatesSomething && (
-                  <ul className="navbar-nav my-2">
-                    <li className="nav-item">
-                      <NavLink
-                        className="nav-link"
-                        to="/reports"
-                        onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                        title={i18n.t("unread_reports", {
-                          count: Number(this.state.unreadReportCount),
-                          formattedCount: numToSI(this.state.unreadReportCount),
-                        })}
-                      >
-                        <Icon icon="shield" />
-                        {this.state.unreadReportCount > 0 && (
-                          <span className="ml-1 badge badge-light">
-                            {numToSI(this.state.unreadReportCount)}
-                          </span>
-                        )}
-                      </NavLink>
-                    </li>
-                  </ul>
                 )}
                 {amAdmin() && (
-                  <ul className="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: Number(this.state.unreadApplicationCount),
-                          formattedCount: numToSI(
-                            this.state.unreadApplicationCount
-                          ),
-                        })}
-                      >
-                        <Icon icon="clipboard" />
-                        {this.state.unreadApplicationCount > 0 && (
-                          <span className="mx-1 badge badge-light">
-                            {numToSI(this.state.unreadApplicationCount)}
-                          </span>
+                  <li id="navApplications" className="nav-item">
+                    <NavLink
+                      to="/registration_applications"
+                      className="nav-link d-inline-flex align-items-center d-md-inline-block"
+                      title={I18NextService.i18n.t(
+                        "unread_registration_applications",
+                        {
+                          count: Number(this.unreadApplicationCount),
+                          formattedCount: numToSI(this.unreadApplicationCount),
+                        }
+                      )}
+                      onMouseUp={linkEvent(this, handleCollapseClick)}
+                    >
+                      <Icon icon="clipboard" />
+                      <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
+                        {I18NextService.i18n.t(
+                          "unread_registration_applications",
+                          {
+                            count: Number(this.unreadApplicationCount),
+                            formattedCount: numToSI(
+                              this.unreadApplicationCount
+                            ),
+                          }
                         )}
-                      </NavLink>
-                    </li>
-                  </ul>
+                      </span>
+                      {this.unreadApplicationCount > 0 && (
+                        <span className="mx-1 badge text-bg-light">
+                          {numToSI(this.unreadApplicationCount)}
+                        </span>
+                      )}
+                    </NavLink>
+                  </li>
                 )}
                 {person && (
-                  <ul className="navbar-nav">
-                    <li className="nav-item dropdown">
-                      <button
-                        className="nav-link btn btn-link dropdown-toggle"
-                        onClick={linkEvent(this, this.handleToggleDropdown)}
-                        id="navbarDropdown"
-                        role="button"
-                        aria-expanded="false"
-                      >
-                        <span>
-                          {showAvatars() && person.avatar && (
-                            <PictrsImage src={person.avatar} icon />
-                          )}
-                          {person.display_name ?? person.name}
-                        </span>
-                      </button>
-                      {this.state.showDropdown && (
-                        <div
-                          className="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 className="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>
+                  <div id="dropdownUser" className="dropdown">
+                    <button
+                      className="btn dropdown-toggle"
+                      role="button"
+                      aria-expanded="false"
+                      data-bs-toggle="dropdown"
+                    >
+                      {showAvatars() && person.avatar && (
+                        <PictrsImage src={person.avatar} icon />
                       )}
-                    </li>
-                  </ul>
+                      {person.display_name ?? person.name}
+                    </button>
+                    <ul
+                      className="dropdown-menu"
+                      style={{ "min-width": "fit-content" }}
+                    >
+                      <li>
+                        <NavLink
+                          to={`/u/${person.name}`}
+                          className="dropdown-item px-2"
+                          title={I18NextService.i18n.t("profile")}
+                          onMouseUp={linkEvent(this, handleCollapseClick)}
+                        >
+                          <Icon icon="user" classes="me-1" />
+                          {I18NextService.i18n.t("profile")}
+                        </NavLink>
+                      </li>
+                      <li>
+                        <NavLink
+                          to="/settings"
+                          className="dropdown-item px-2"
+                          title={I18NextService.i18n.t("settings")}
+                          onMouseUp={linkEvent(this, handleCollapseClick)}
+                        >
+                          <Icon icon="settings" classes="me-1" />
+                          {I18NextService.i18n.t("settings")}
+                        </NavLink>
+                      </li>
+                      <li>
+                        <hr className="dropdown-divider" />
+                      </li>
+                      <li>
+                        <button
+                          className="dropdown-item btn btn-link px-2"
+                          onClick={linkEvent(this, handleLogOut)}
+                        >
+                          <Icon icon="log-out" classes="me-1" />
+                          {I18NextService.i18n.t("logout")}
+                        </button>
+                      </li>
+                    </ul>
+                  </div>
                 )}
               </>
             ) : (
-              <ul className="navbar-nav my-2">
+              <>
                 <li className="nav-item">
                   <NavLink
                     to="/login"
                     className="nav-link"
-                    onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                    title={i18n.t("login")}
+                    title={I18NextService.i18n.t("login")}
+                    onMouseUp={linkEvent(this, handleCollapseClick)}
                   >
-                    {i18n.t("login")}
+                    {I18NextService.i18n.t("login")}
                   </NavLink>
                 </li>
                 <li className="nav-item">
                   <NavLink
                     to="/signup"
                     className="nav-link"
-                    onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
-                    title={i18n.t("sign_up")}
+                    title={I18NextService.i18n.t("sign_up")}
+                    onMouseUp={linkEvent(this, handleCollapseClick)}
                   >
-                    {i18n.t("sign_up")}
+                    {I18NextService.i18n.t("sign_up")}
                   </NavLink>
                 </li>
-              </ul>
+              </>
             )}
-          </div>
+          </ul>
         </div>
       </nav>
     );
   }
 
+  handleOutsideMenuClick(event: MouseEvent) {
+    if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
+      handleCollapseClick(this);
+    }
+  }
+
   get moderatesSomething(): boolean {
-    let mods = UserService.Instance.myUserInfo?.moderates;
-    let moderatesS = (mods && mods.length > 0) || false;
+    const mods = UserService.Instance.myUserInfo?.moderates;
+    const moderatesS = (mods && mods.length > 0) || false;
     return amAdmin() || moderatesS;
   }
 
-  handleToggleExpandNavbar(i: Navbar) {
-    i.setState({ expanded: !i.state.expanded });
-  }
+  fetchUnreads() {
+    poll(async () => {
+      if (window.document.visibilityState !== "hidden") {
+        const auth = myAuth();
+        if (auth) {
+          this.setState({
+            unreadInboxCountRes: await HttpService.client.getUnreadCount({
+              auth,
+            }),
+          });
 
-  handleHideExpandNavbar(i: Navbar) {
-    i.setState({ expanded: false, showDropdown: false });
-  }
+          if (this.moderatesSomething) {
+            this.setState({
+              unreadReportCountRes: await HttpService.client.getReportCount({
+                auth,
+              }),
+            });
+          }
 
-  handleLogoutClick(i: Navbar) {
-    i.setState({ showDropdown: false, expanded: false });
-    UserService.Instance.logout();
+          if (amAdmin()) {
+            this.setState({
+              unreadApplicationCountRes:
+                await HttpService.client.getUnreadRegistrationApplicationCount({
+                  auth,
+                }),
+            });
+          }
+        }
+      }
+    }, updateUnreadCountsInterval);
   }
 
-  handleToggleDropdown(i: Navbar) {
-    i.setState({ showDropdown: !i.state.showDropdown });
+  get unreadInboxCount(): number {
+    if (this.state.unreadInboxCountRes.state == "success") {
+      const data = this.state.unreadInboxCountRes.data;
+      return data.replies + data.mentions + data.private_messages;
+    } else {
+      return 0;
+    }
   }
 
-  parseMessage(msg: any) {
-    let op = wsUserOp(msg);
-    console.log(msg);
-    if (msg.error) {
-      if (msg.error == "not_logged_in") {
-        UserService.Instance.logout();
-      }
-      return;
-    } else if (msg.reconnect) {
-      console.log(i18n.t("websocket_reconnected"));
-      let auth = myAuth(false);
-      if (UserService.Instance.myUserInfo && auth) {
-        WebSocketService.Instance.send(
-          wsClient.userJoin({
-            auth,
-          })
-        );
-        this.fetchUnreads();
-      }
-    } else if (op == UserOperation.GetUnreadCount) {
-      let data = wsJsonToRes<GetUnreadCountResponse>(msg);
-      this.setState({
-        unreadInboxCount: data.replies + data.mentions + data.private_messages,
-      });
-      this.sendUnreadCount();
-    } else if (op == UserOperation.GetReportCount) {
-      let data = wsJsonToRes<GetReportCountResponse>(msg);
-      this.setState({
-        unreadReportCount:
-          data.post_reports +
-          data.comment_reports +
-          (data.private_message_reports ?? 0),
-      });
-      this.sendReportUnread();
-    } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
-      let data =
-        wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
-      this.setState({ unreadApplicationCount: data.registration_applications });
-      this.sendApplicationUnread();
-    } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg);
-      let mui = UserService.Instance.myUserInfo;
-      if (
-        mui &&
-        data.recipient_ids.includes(mui.local_user_view.local_user.id)
-      ) {
-        this.setState({
-          unreadInboxCount: this.state.unreadInboxCount + 1,
-        });
-        this.sendUnreadCount();
-        notifyComment(data.comment_view, this.context.router);
-      }
-    } else if (op == UserOperation.CreatePrivateMessage) {
-      let data = wsJsonToRes<PrivateMessageResponse>(msg);
-
-      if (
-        data.private_message_view.recipient.id ==
-        UserService.Instance.myUserInfo?.local_user_view.person.id
-      ) {
-        this.setState({
-          unreadInboxCount: this.state.unreadInboxCount + 1,
-        });
-        this.sendUnreadCount();
-        notifyPrivateMessage(data.private_message_view, this.context.router);
-      }
+  get unreadReportCount(): number {
+    if (this.state.unreadReportCountRes.state == "success") {
+      const data = this.state.unreadReportCountRes.data;
+      return (
+        data.post_reports +
+        data.comment_reports +
+        (data.private_message_reports ?? 0)
+      );
+    } else {
+      return 0;
     }
   }
 
-  fetchUnreads() {
-    console.log("Fetching inbox unreads...");
-
-    let auth = myAuth();
-    if (auth) {
-      let unreadForm: GetUnreadCount = {
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
-
-      console.log("Fetching reports...");
-
-      let reportCountForm: GetReportCount = {
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
-
-      if (amAdmin()) {
-        console.log("Fetching applications...");
-
-        let applicationCountForm: GetUnreadRegistrationApplicationCount = {
-          auth,
-        };
-        WebSocketService.Instance.send(
-          wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
-        );
-      }
+  get unreadApplicationCount(): number {
+    if (this.state.unreadApplicationCountRes.state == "success") {
+      const data = this.state.unreadApplicationCountRes.data;
+      return data.registration_applications;
+    } else {
+      return 0;
     }
   }
 
@@ -583,27 +510,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     return this.context.router.history.location.pathname;
   }
 
-  sendUnreadCount() {
-    UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
-  }
-
-  sendReportUnread() {
-    UserService.Instance.unreadReportCountSub.next(
-      this.state.unreadReportCount
-    );
-  }
-
-  sendApplicationUnread() {
-    UserService.Instance.unreadApplicationCountSub.next(
-      this.state.unreadApplicationCount
-    );
-  }
-
   requestNotificationPermission() {
     if (UserService.Instance.myUserInfo) {
       document.addEventListener("DOMContentLoaded", function () {
         if (!Notification) {
-          toast(i18n.t("notifications_error"), "danger");
+          toast(I18NextService.i18n.t("notifications_error"), "danger");
           return;
         }
 
@@ -612,4 +523,4 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       });
     }
   }
-}
\ No newline at end of file
+}