From: SleeplessOne1917 Date: Wed, 14 Jun 2023 23:46:11 +0000 (+0000) Subject: Merge branch 'main' into comment-depth X-Git-Url: http://these/git/?a=commitdiff_plain;h=31e61e7449a0aef46b7d9ccd0ecff3304e3614f5;hp=48c16cc977957db400920ec355e9b5e535dda33a;p=lemmy-ui.git Merge branch 'main' into comment-depth --- diff --git a/.eslintrc.json b/.eslintrc.json index cc1bff1..3a60a6b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,6 +18,7 @@ "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, "arrow-body-style": 0, "curly": 0, "eol-last": 0, @@ -37,7 +38,7 @@ "no-useless-constructor": 0, "no-useless-escape": 0, "no-var": 0, - "prefer-const": 0, + "prefer-const": 1, "prefer-rest-params": 0, "quote-props": 0, "unicorn/filename-case": 0 diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md deleted file mode 100644 index 69b116f..0000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "\U0001F41E Bug Report" -about: Create a report to help us improve Lemmy -title: "" -labels: bug -assignees: "" ---- - -Found a bug? Please fill out the sections below. 👍 - -For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) - -### Issue Summary - -A summary of the bug. - -### Steps to Reproduce - -1. (for example) I clicked login, and an endless spinner show up. -2. I tried to install lemmy via this guide, and I'm getting this error. -3. ... - -### Technical details - -- Please post your log: `sudo docker-compose logs > lemmy_log.out`. -- What OS are you trying to install lemmy on? -- Any browser console errors? diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml new file mode 100644 index 0000000..6457909 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -0,0 +1,47 @@ +name: "\U0001F41E Bug Report" +description: Create a report to help us improve lemmy-ui +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Found a bug? Please fill out the sections below. 👍 + Thanks for taking the time to fill out this bug report! + For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) + - type: textarea + id: summary + attributes: + label: Summary + description: A summary of the bug. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to Reproduce + description: | + Describe the steps to reproduce the bug. + The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution. + value: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: technical + attributes: + label: Technical Details + description: | + - Any browser console errors? + validations: + required: true + - type: input + id: lemmy-ui-version + attributes: + label: Version + description: Which Lemmy UI version do you use? Displayed in the footer. + placeholder: ex. 0.17.4-rc.4 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md deleted file mode 100644 index bfeca29..0000000 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: "\U0001F680 Feature request" -about: Suggest an idea for improving Lemmy -title: "" -labels: enhancement -assignees: "" ---- - -For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) - -### Is your proposal related to a problem? - - - -(Write your answer here.) - -### Describe the solution you'd like - - - -(Describe your proposed solution here.) - -### Describe alternatives you've considered - - - -(Write your answer here.) - -### Additional context - - - -(Write your answer here.) diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml new file mode 100644 index 0000000..375d06d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -0,0 +1,41 @@ +name: "\U0001F680 Feature request" +description: Suggest an idea for improving Lemmy +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Have a suggestion about Lemmy's UI? + For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) + - type: textarea + id: problem + attributes: + label: Is your proposal related to a problem? + description: | + Provide a clear and concise description of what the problem is. + For example, "I'm always frustrated when..." + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like. + description: | + Provide a clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered. + description: | + Let us know about other solutions you've tried or researched. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context + description: | + Is there anything else you can add about the proposal? + You might want to link to related issues here, if you haven't already. diff --git a/.github/ISSUE_TEMPLATE/QUESTION.md b/.github/ISSUE_TEMPLATE/QUESTION.md deleted file mode 100644 index 1532587..0000000 --- a/.github/ISSUE_TEMPLATE/QUESTION.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: "? Question" -about: General questions about Lemmy -title: "" -labels: question -assignees: "" ---- - -What's the question you have about lemmy? diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml new file mode 100644 index 0000000..460d9a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/QUESTION.yml @@ -0,0 +1,17 @@ +name: "? Question" +description: General questions about Lemmy +title: "Question: " +labels: ["question", "triage"] +body: + - type: markdown + attributes: + value: | + Have a question about Lemmy's UI? + Please check the docs first: https://join-lemmy.org/docs/en/index.html + - type: textarea + id: question + attributes: + label: Question + description: What's the question you have about Lemmy's UI? + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/hexbear.md b/.github/ISSUE_TEMPLATE/hexbear.md deleted file mode 100644 index 65483df..0000000 --- a/.github/ISSUE_TEMPLATE/hexbear.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Hexbear -about: For hexbear issues -title: "" -labels: hexbear -assignees: "" ---- - -For hexbear-related issues diff --git a/.github/ISSUE_TEMPLATE/hexbear.yml b/.github/ISSUE_TEMPLATE/hexbear.yml new file mode 100644 index 0000000..199b97e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/hexbear.yml @@ -0,0 +1,11 @@ +name: "Hexbear" +description: For hexbear issues +labels: ["hexbear", "triage"] +body: + - type: textarea + id: question + attributes: + label: Question + description: What's the question you have about hexbear? + validations: + required: true \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml index 8d3c6f1..656903a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,6 @@ pipeline: fetch_git_submodules: - image: node:14-alpine + image: node:alpine commands: - apk add git - git submodule init @@ -8,93 +8,27 @@ pipeline: # - git fetch --tags yarn: - image: node:14-alpine + image: node:alpine commands: - yarn yarn_lint: - image: node:14-alpine + image: node:alpine commands: - yarn lint yarn_build_dev: - image: node:14-alpine + image: node:alpine commands: - yarn build:dev - nightly_build: - image: plugins/docker + publish_release_docker: + image: woodpeckerci/plugin-docker-buildx + secrets: [docker_username, docker_password] settings: - dockerfile: Dockerfile - repo: dessalines/lemmy-ui - username: - from_secret: docker_username - password: - from_secret: docker_password - tags: - - dev - when: - event: - - cron - - publish_release_docker_image_amd: - image: plugins/docker - settings: - dockerfile: Dockerfile repo: dessalines/lemmy-ui - auto_tag: true - auto_tag_suffix: linux-amd64 - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: tag - platform: linux/arm64 - - publish_release_docker_image_arm: - image: plugins/docker - settings: dockerfile: Dockerfile - repo: dessalines/lemmy-ui + platforms: linux/amd64 auto_tag: true - auto_tag_suffix: linux-arm64 - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: tag - platform: linux/amd64 - - publish_release_docker_manifest: - image: plugins/manifest - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - target: "dessalines/lemmy-ui:${CI_COMMIT_TAG}" - template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH" - platforms: - - linux/amd64 - - linux/arm64 - ignore_missing: true - when: - event: tag - - publish_latest_release_docker_manifest: - image: plugins/manifest - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - target: "dessalines/lemmy-ui:latest" - template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH" - platforms: - - linux/amd64 - - linux/arm64 - ignore_missing: true when: event: tag diff --git a/README.md b/README.md index 6c9ef63..f1917bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# lemmy-ui +# Lemmy-UI The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno. @@ -13,7 +13,6 @@ The following environment variables can be used to configure lemmy-ui: | `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. | | `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. | | `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. | -| `LEMMY_UI_LEMMY_WS_HOST` | `string` | `0.0.0.0:8536` | An alternate location for lemmy's websocket address. Not usually necessary. | | `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. | | `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. | | `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. | diff --git a/deploy.sh b/deploy.sh index ce125fc..e919779 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,7 +4,8 @@ set -e new_tag="$1" # Old deploy -# sudo docker build . --tag dessalines/lemmy-ui:$new_tag +# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 --push +# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 # sudo docker push dessalines/lemmy-ui:$new_tag # Upgrade version diff --git a/lemmy-translations b/lemmy-translations index ddf0d3a..f45ddff 160000 --- a/lemmy-translations +++ b/lemmy-translations @@ -1 +1 @@ -Subproject commit ddf0d3a4dcfba5eddbcdb702db2470b52abb3815 +Subproject commit f45ddff206adb52ab0ac7555bf14978edac5d2f2 diff --git a/package.json b/package.json index 43b8883..fd7cf4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.17.1", + "version": "0.18.0-beta.6", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", @@ -17,16 +17,9 @@ "start": "yarn build:dev --watch" }, "lint-staged": { - "*.{ts,tsx,js}": [ - "prettier --write", - "eslint --fix" - ], - "*.{css, scss}": [ - "prettier --write" - ], - "package.json": [ - "sortpack" - ] + "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"], + "*.{css, scss}": ["prettier --write"], + "package.json": ["sortpack"] }, "dependencies": { "@babel/plugin-proposal-decorators": "^7.21.0", @@ -49,6 +42,7 @@ "emoji-mart": "^5.4.0", "emoji-short-name": "^2.0.0", "express": "~4.18.2", + "history": "^5.3.0", "html-to-text": "^9.0.5", "i18next": "^22.4.15", "inferno": "^8.1.1", @@ -60,7 +54,7 @@ "inferno-server": "^8.1.1", "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", - "lemmy-js-client": "0.17.2-rc.17", + "lemmy-js-client": "0.17.2-rc.24", "lodash": "^4.17.21", "markdown-it": "^13.0.1", "markdown-it-container": "^3.0.0", @@ -73,7 +67,6 @@ "moment": "^2.29.4", "register-service-worker": "^1.7.2", "run-node-webpack-plugin": "^1.3.0", - "rxjs": "^7.8.1", "sanitize-html": "^2.10.0", "sass": "^1.62.1", "sass-loader": "^13.2.2", @@ -85,8 +78,7 @@ "tributejs": "^5.1.3", "webpack": "5.82.1", "webpack-cli": "^5.1.1", - "webpack-node-externals": "^3.0.0", - "websocket-ts": "^1.1.1" + "webpack-node-externals": "^3.0.0" }, "devDependencies": { "@babel/core": "^7.21.8", @@ -120,6 +112,7 @@ "typescript": "^5.0.4", "webpack-dev-server": "4.15.0" }, + "packageManager": "yarn@1.22.19", "engines": { "node": ">=8.9.0" }, diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 5315aa3..e1adfc5 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -75,6 +75,11 @@ font-size: 1.2rem; } +.md-div pre { + white-space: pre; + overflow-x: auto; +} + .md-div table { border-collapse: collapse; width: 100%; @@ -275,6 +280,10 @@ hr { -ms-filter: blur(10px); } +.img-cover { + object-fit: cover; +} + .img-expanded { max-height: 90vh; } diff --git a/src/assets/css/themes/_variables.bootstra_386-tmp.scss b/src/assets/css/themes/_variables.bootstra_386-tmp.scss deleted file mode 100644 index 7804f9b..0000000 --- a/src/assets/css/themes/_variables.bootstra_386-tmp.scss +++ /dev/null @@ -1,865 +0,0 @@ -// -// Variables -// -------------------------------------------------- - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -//// colors from bs-2 -// Grays -// ------------------------- -$black: #000; -$grayDark: #555; -$gray: #bbb; -$grayLight: #bbb; -$white: #fff; - -// Accent colors -// ------------------------- -$blue: #5555ff; -$cyan: #55ffff; -$cyanDark: #00aaaa; -$blueDark: #000084; -$green: #55ff55; -$greenDark: #00aa00; -$magenta: #ff55ff; -$magentaDark: #aa00aa; -$red: #ff5555; -$redDark: #aa0000; -$yellow: #fefe54; -$brown: #aa5500; -$orange: #a85400; -$pink: #fe54fe; -$purple: #fe5454; - -// end colors - -$gray-base: $gray; -$gray-darker: $grayDark; -$gray-dark: $grayDark; -$gray-light: $grayLight; -$gray-lighter: $grayLight; - -$brand-primary: $gray; -$brand-primary-bg: $cyanDark; -$brand-success: $greenDark; -$brand-info: $brown; -$brand-warning: $magentaDark; -$brand-danger: $redDark; - -//== Scaffolding -// -//## Settings for some of the most global styles. - -//** Background color for ``. -$body-bg: $blueDark; -//** Global text color on ``. -$text-color: $gray-light; - -//** Global textual link color. -$link-color: $brand-primary; -//** Link hover color set via `darken()` function. -$link-hover-color: $white; -//** Link hover decoration. -$link-hover-decoration: none; - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace; -$font-family-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace; -//** Default monospace fonts for ``, ``, and `
`.
-$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$font-family-base: $font-family-sans-serif;
-
-$baseWidth: 10px;
-$font-size-base: 18px;
-$font-size-large: $font-size-base;
-$font-size-small: $font-size-base;
-
-$font-size-h1: $font-size-base;
-$font-size-h2: $font-size-base;
-$font-size-h3: $font-size-base;
-$font-size-h4: $font-size-base;
-$font-size-h5: $font-size-base;
-$font-size-h6: $font-size-base;
-
-//** Unit-less `line-height` for use in components like buttons.
-$baseLineHeight: 19px;
-$line-height-base: $baseLineHeight;
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-$line-height-computed: $line-height-base;
-
-//** By default, this inherits from the ``.
-$headings-font-family: inherit;
-$headings-font-weight: normal;
-$headings-line-height: $line-height-base;
-$headings-color: inherit;
-
-$space: $baseWidth;
-$halfbaseLineHeight: ($baseLineHeight / 2);
-$borderWidth: 2px;
-$baseLineWidth: ($baseLineHeight / 2);
-$halfSpace: ($baseWidth / 2);
-$lhsNB: ($baseWidth / 2 + 1);
-$rhsNB: ($baseWidth / 2 - 1);
-$lhs: ($lhsNB - ($borderWidth));
-$rhs: ($rhsNB - ($borderWidth / 2));
-$tsNB: ($baseLineHeight / 2);
-$bsNB: $tsNB;
-$ts: ($tsNB - ($borderWidth / 2));
-$bs: $ts;
-$tsMargin: 3px;
-
-//== Iconography
-//
-//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-//** Load fonts from this directory.
-$icon-font-path: "../fonts/";
-//** File name for all font files.
-$icon-font-name: "glyphicons-halflings-regular";
-//** Element ID within SVG icon file.
-$icon-font-svg-id: "glyphicons_halflingsregular";
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-$padding-base-vertical: 0px;
-$padding-base-horizontal: 0px;
-
-$padding-large-vertical: 0px;
-$padding-large-horizontal: $halfSpace;
-
-$padding-small-vertical: 0px;
-$padding-small-horizontal: 0px;
-
-$padding-xs-vertical: 0px;
-$padding-xs-horizontal: 0px;
-
-$line-height-large: $baseLineHeight;
-$line-height-small: $baseLineHeight;
-
-$border-radius-base: 0;
-$border-radius-large: 0;
-$border-radius-small: 0;
-
-//** Global color for active items (e.g., navs or dropdowns).
-$component-active-color: $white;
-//** Global background color for active items (e.g., navs or dropdowns).
-$component-active-bg: $black;
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-$caret-width-base: 4px;
-//** Carets increase slightly in size for larger components.
-$caret-width-large: 5px;
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for ``s and ``s.
-$table-cell-padding: $ts $rhs $bs $lhs;
-//** Padding for cells in `.table-condensed`.
-$table-condensed-cell-padding: $ts $rhs $bs $lhs;
-
-//** Default background color used for all tables.
-$table-bg: transparent;
-//** Background color used for `.table-striped`.
-$table-bg-accent: $black;
-//** Background color used for `.table-hover`.
-$table-bg-hover: #f5f5f5;
-$table-bg-active: $table-bg-hover;
-
-//** Border color for table and cell borders.
-$table-border-color: $gray;
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-$btn-font-weight: normal;
-
-$btn-default-color: $black;
-$btn-default-bg: $grayLight;
-$btn-default-border: $grayLight;
-
-$btn-primary-color: $black;
-$btn-primary-bg: $cyanDark;
-$btn-primary-border: $grayLight;
-
-$btn-success-color: #fff;
-$btn-success-bg: $brand-success;
-$btn-success-border: $btn-success-bg;
-
-$btn-info-color: #fff;
-$btn-info-bg: $brand-info;
-$btn-info-border: $btn-info-bg;
-
-$btn-warning-color: #fff;
-$btn-warning-bg: $brand-warning;
-$btn-warning-border: $btn-warning-bg;
-
-$btn-danger-color: #fff;
-$btn-danger-bg: $brand-danger;
-$btn-danger-border: $btn-danger-bg;
-
-$btn-link-disabled-color: $gray-light;
-
-//== Forms
-//
-//##
-
-//** `` background color
-$input-bg: $cyanDark;
-//** `` background color
-$input-bg-disabled: $gray-lighter;
-
-//** Text color for ``s
-$input-color: $white;
-//** `` border color
-$input-border: #ccc;
-
-// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
-//** Default `.form-control` border radius
-// This has no effect on ``s in CSS.
-$input-border-radius: $border-radius-base;
-//** Large `.form-control` border radius
-$input-border-radius-large: $border-radius-large;
-//** Small `.form-control` border radius
-$input-border-radius-small: $border-radius-small;
-
-//** Border color for inputs on focus
-$input-border-focus: $black;
-
-//** Placeholder text color
-$input-color-placeholder: $black;
-
-//** Default `.form-control` height
-$input-height-base: $line-height-computed;
-//** Large `.form-control` height
-$input-height-large: $input-height-base;
-//** Small `.form-control` height
-$input-height-small: $input-height-base;
-
-$legend-color: $gray-dark;
-$legend-border-color: #e5e5e5;
-
-//** Background color for textual input addons
-$input-group-addon-bg: $gray-lighter;
-//** Border color for textual input addons
-$input-group-addon-border-color: $input-border;
-
-//** Disabled cursor for form controls and buttons.
-$cursor-disabled: not-allowed;
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-$dropdown-bg: $gray;
-//** Dropdown menu `border-color`.
-$dropdown-border: rgb(0, 0, 0);
-//** Dropdown menu `border-color` **for IE8**.
-$dropdown-fallback-border: #ccc;
-//** Divider color for between dropdown items.
-$dropdown-divider-bg: $black;
-
-//** Dropdown link text color.
-$dropdown-link-color: $black;
-//** Hover color for dropdown links.
-$dropdown-link-hover-color: $gray;
-//** Hover background for dropdown links.
-$dropdown-link-hover-bg: $black;
-
-//** Active dropdown menu item text color.
-$dropdown-link-active-color: $component-active-color;
-//** Active dropdown menu item background color.
-$dropdown-link-active-bg: $component-active-bg;
-
-//** Disabled dropdown menu item background color.
-$dropdown-link-disabled-color: $gray-light;
-
-//** Text color for headers within dropdown menus.
-$dropdown-header-color: $black;
-
-//** Deprecated `$dropdown-caret-color` as of v3.1.0
-$dropdown-caret-color: #000;
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-$zindex-navbar: 1000;
-$zindex-dropdown: 1000;
-$zindex-popover: 1060;
-$zindex-tooltip: 1070;
-$zindex-navbar-fixed: 1030;
-$zindex-modal: 1040;
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-//** Deprecated `$screen-xs` as of v3.0.1
-$screen-xs: 480px;
-//** Deprecated `$screen-xs-min` as of v3.2.0
-$screen-xs-min: $screen-xs;
-//** Deprecated `$screen-phone` as of v3.0.1
-$screen-phone: $screen-xs-min;
-
-// Small screen / tablet
-//** Deprecated `$screen-sm` as of v3.0.1
-$screen-sm: 768px;
-$screen-sm-min: $screen-sm;
-//** Deprecated `$screen-tablet` as of v3.0.1
-$screen-tablet: $screen-sm-min;
-
-// Medium screen / desktop
-//** Deprecated `$screen-md` as of v3.0.1
-$screen-md: 992px;
-$screen-md-min: $screen-md;
-//** Deprecated `$screen-desktop` as of v3.0.1
-$screen-desktop: $screen-md-min;
-
-// Large screen / wide desktop
-//** Deprecated `$screen-lg` as of v3.0.1
-$screen-lg: 1200px;
-$screen-lg-min: $screen-lg;
-//** Deprecated `$screen-lg-desktop` as of v3.0.1
-$screen-lg-desktop: $screen-lg-min;
-
-// So media queries don't overlap when required, provide a maximum
-$screen-xs-max: ($screen-sm-min - 1);
-$screen-sm-max: ($screen-md-min - 1);
-$screen-md-max: ($screen-lg-min - 1);
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-$grid-columns: 12;
-//** Padding between columns. Gets divided in half for the left and right.
-$grid-gutter-width: ($baseWidth * 2);
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-$grid-float-breakpoint: $screen-sm-min;
-//** Point at which the navbar begins collapsing.
-$grid-float-breakpoint-max: ($grid-float-breakpoint);
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-$container-tablet: (720px + $grid-gutter-width);
-//** For `$screen-sm-min` and up.
-$container-sm: $container-tablet;
-
-// Medium screen / desktop
-$container-desktop: (940px + $grid-gutter-width);
-//** For `$screen-md-min` and up.
-$container-md: $container-desktop;
-
-// Large screen / wide desktop
-$container-large-desktop: (1140px + $grid-gutter-width);
-//** For `$screen-lg-min` and up.
-$container-lg: $container-large-desktop;
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-$navbar-height: 0px;
-$navbar-margin-bottom: $line-height-computed;
-$navbar-border-radius: $border-radius-base;
-$navbar-padding-horizontal: ($baseWidth * 2);
-$navbar-padding-vertical: 0;
-$navbar-collapse-max-height: 340px;
-
-$navbar-default-color: $black;
-$navbar-default-bg: $grayLight;
-$navbar-default-border: $navbar-default-bg;
-
-// Navbar links
-$navbar-default-link-color: $black;
-$navbar-default-link-hover-color: $white;
-$navbar-default-link-hover-bg: $black;
-$navbar-default-link-active-color: $white;
-$navbar-default-link-active-bg: $black;
-$navbar-default-link-disabled-color: $gray;
-$navbar-default-link-disabled-bg: transparent;
-
-// Navbar brand label
-$navbar-default-brand-color: $navbar-default-link-color;
-$navbar-default-brand-hover-color: $navbar-default-brand-color;
-$navbar-default-brand-hover-bg: transparent;
-
-// Navbar toggle
-$navbar-default-toggle-hover-bg: #ddd;
-$navbar-default-toggle-icon-bar-bg: #888;
-$navbar-default-toggle-border-color: #ddd;
-
-// Inverted navbar
-// Reset inverted navbar basics
-$navbar-inverse-color: $gray;
-$navbar-inverse-bg: $black;
-$navbar-inverse-border: $navbar-inverse-bg;
-
-// Inverted navbar links
-$navbar-inverse-link-color: $gray-light;
-$navbar-inverse-link-hover-color: $black;
-$navbar-inverse-link-hover-bg: $grayLight;
-$navbar-inverse-link-active-color: $white;
-$navbar-inverse-link-active-bg: $grayDark;
-$navbar-inverse-link-disabled-color: $gray;
-$navbar-inverse-link-disabled-bg: transparent;
-
-// Inverted navbar brand label
-$navbar-inverse-brand-color: $navbar-inverse-link-color;
-$navbar-inverse-brand-hover-color: #fff;
-$navbar-inverse-brand-hover-bg: transparent;
-
-// Inverted navbar toggle
-$navbar-inverse-toggle-hover-bg: $grayLight;
-$navbar-inverse-toggle-icon-bar-bg: #fff;
-$navbar-inverse-toggle-border-color: #333;
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-$nav-link-padding: 0 $baseWidth;
-$nav-link-hover-bg: $gray-lighter;
-
-$nav-disabled-link-color: $gray-light;
-$nav-disabled-link-hover-color: $gray-light;
-
-//== Tabs
-$nav-tabs-border-color: #ddd;
-
-$nav-tabs-link-hover-border-color: $gray-lighter;
-
-$nav-tabs-active-link-hover-bg: $black;
-$nav-tabs-active-link-hover-color: $white;
-
-$nav-tabs-justified-active-link-border-color: $body-bg;
-
-//== Pills
-$nav-pills-border-radius: $border-radius-base;
-$nav-pills-active-link-hover-bg: $component-active-bg;
-$nav-pills-active-link-hover-color: $component-active-color;
-
-//== Pagination
-//
-//##
-
-$pagination-color: $black;
-$pagination-bg: $gray;
-$pagination-border: #ddd;
-
-$pagination-hover-color: $link-hover-color;
-$pagination-hover-bg: $gray-lighter;
-$pagination-hover-border: #ddd;
-
-$pagination-active-color: #fff;
-$pagination-active-bg: $brand-primary;
-$pagination-active-border: $brand-primary;
-
-$pagination-disabled-color: $gray-light;
-$pagination-disabled-bg: #fff;
-$pagination-disabled-border: #ddd;
-
-//== Pager
-//
-//##
-
-$pager-bg: $pagination-bg;
-$pager-border: $pagination-border;
-$pager-border-radius: 0;
-
-$pager-hover-bg: $pagination-hover-bg;
-
-$pager-active-bg: $pagination-active-bg;
-$pager-active-color: $pagination-active-color;
-
-$pager-disabled-color: $pagination-disabled-color;
-
-//== Jumbotron
-//
-//##
-
-$jumbotron-padding: ($ts) ($rhs + $baseWidth) ($bs) ($lhs + $baseWidth);
-$jumbotron-color: $white;
-$jumbotron-bg: transparent;
-$jumbotron-heading-color: inherit;
-$jumbotron-font-size: $font-size-base;
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-$state-success-text: $green;
-$state-success-bg: $greenDark;
-$state-success-border: $state-success-bg;
-
-$state-info-text: $yellow;
-$state-info-bg: $brown;
-$state-info-border: $state-info-bg;
-
-$state-warning-text: $magenta;
-$state-warning-bg: $magentaDark;
-$state-warning-border: $state-warning-bg;
-
-$state-danger-text: $red;
-$state-danger-bg: $black;
-$state-danger-border: $state-danger-bg;
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-$tooltip-max-width: ($baseWidth * 25);
-//** Tooltip text color
-$tooltip-color: $white;
-//** Tooltip background color
-$tooltip-bg: $grayDark;
-$tooltip-opacity: 1;
-
-//** Tooltip arrow width
-$tooltip-arrow-width: 0px;
-//** Tooltip arrow color
-$tooltip-arrow-color: $tooltip-bg;
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-$popover-bg: $gray;
-//** Popover maximum width
-$popover-max-width: ($baseWidth * 20);
-//** Popover border color
-$popover-border-color: rgb(0, 0, 0);
-//** Popover fallback border color
-$popover-fallback-border-color: #ccc;
-
-//** Popover title background color
-$popover-title-bg: $greenDark;
-
-//** Popover arrow width
-$popover-arrow-width: 10px;
-//** Popover arrow color
-$popover-arrow-color: $popover-bg;
-
-//** Popover outer arrow width
-$popover-arrow-outer-width: ($popover-arrow-width + 1);
-//** Popover outer arrow color
-$popover-arrow-outer-color: $popover-border-color;
-//** Popover outer arrow fallback color
-$popover-arrow-outer-fallback-color: $popover-fallback-border-color;
-
-//== Labels
-//
-//##
-
-//** Default label background color
-$label-default-bg: $gray-light;
-//** Primary label background color
-$label-primary-bg: $brand-primary-bg;
-//** Success label background color
-$label-success-bg: $brand-success;
-//** Info label background color
-$label-info-bg: $brand-info;
-//** Warning label background color
-$label-warning-bg: $brand-warning;
-//** Danger label background color
-$label-danger-bg: $brand-danger;
-
-//** Default label text color
-$label-color: #fff;
-//** Default text color of a linked label
-$label-link-hover-color: #fff;
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-$modal-inner-padding: 0 $baseWidth;
-
-//** Padding applied to the modal title
-$modal-title-padding: 0 $baseWidth;
-//** Modal title line-height
-$modal-title-line-height: $line-height-base;
-
-//** Background color of modal content area
-$modal-content-bg: $gray;
-//** Modal content border color
-$modal-content-border-color: rgb(0, 0, 0);
-//** Modal content border color **for IE8**
-$modal-content-fallback-border-color: #999;
-
-//** Modal backdrop background color
-$modal-backdrop-bg: #000;
-//** Modal backdrop opacity
-// $modal-backdrop-opacity:      @include 5;
-//** Modal header border color
-$modal-header-border-color: #e5e5e5;
-//** Modal footer border color
-$modal-footer-border-color: $modal-header-border-color;
-
-$modal-lg: 900px;
-$modal-md: 600px;
-$modal-sm: 300px;
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-$alert-padding: $line-height-base ($baseWidth * 2);
-$alert-border-radius: $border-radius-base;
-$alert-link-font-weight: normal;
-
-$alert-success-bg: $state-success-bg;
-$alert-success-text: $state-success-text;
-$alert-success-border: $state-success-border;
-
-$alert-info-bg: $state-info-bg;
-$alert-info-text: $state-info-text;
-$alert-info-border: $state-info-border;
-
-$alert-warning-bg: $state-warning-bg;
-$alert-warning-text: $state-warning-text;
-$alert-warning-border: $state-warning-border;
-
-$alert-danger-bg: $state-danger-bg;
-$alert-danger-text: $state-danger-text;
-$alert-danger-border: $state-danger-border;
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-$progress-bg: $black;
-//** Progress bar text color
-$progress-bar-color: $black;
-//** Variable for setting rounded corners on progress bar.
-$progress-border-radius: $border-radius-base;
-
-//** Default progress bar color
-$progress-bar-bg: $brand-primary;
-//** Success progress bar color
-$progress-bar-success-bg: $brand-success;
-//** Warning progress bar color
-$progress-bar-warning-bg: $brand-warning;
-//** Danger progress bar color
-$progress-bar-danger-bg: $brand-danger;
-//** Info progress bar color
-$progress-bar-info-bg: $brand-info;
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-$list-group-bg: $gray;
-//** `.list-group-item` border color
-$list-group-border: #ddd;
-//** List group border radius
-$list-group-border-radius: $border-radius-base;
-
-//** Background color of single list items on hover
-$list-group-hover-bg: $black;
-//** Text color of active list items
-$list-group-active-color: $component-active-color;
-//** Background color of active list items
-$list-group-active-bg: $component-active-bg;
-//** Border color of active list elements
-$list-group-active-border: $list-group-active-bg;
-//** Text color for content within active list items
-$list-group-active-text-color: $component-active-color;
-
-//** Text color of disabled list items
-$list-group-disabled-color: $gray-dark;
-//** Background color of disabled list items
-$list-group-disabled-bg: $gray-lighter;
-//** Text color for content within disabled list items
-$list-group-disabled-text-color: $list-group-disabled-color;
-
-$list-group-link-color: $black;
-$list-group-link-hover-color: $list-group-link-color;
-$list-group-link-heading-color: #333;
-
-//== Panels
-//
-//##
-
-$panel-bg: $gray;
-$panel-body-padding: 0 $rhsNB 0 $lhsNB;
-$panel-heading-padding: 0 $rhsNB 0 $lhsNB;
-$panel-footer-padding: $panel-heading-padding;
-$panel-border-radius: $border-radius-base;
-
-//** Border color for elements within panels
-$panel-inner-border: #ddd;
-$panel-footer-bg: #f5f5f5;
-
-$panel-default-text: $white;
-$panel-default-border: #ddd;
-$panel-default-heading-bg: $grayDark;
-
-$panel-primary-text: $white;
-$panel-primary-border: $brand-primary;
-$panel-primary-heading-bg: $cyanDark;
-
-$panel-success-text: $state-success-text;
-$panel-success-border: $state-success-border;
-$panel-success-heading-bg: $state-success-bg;
-
-$panel-info-text: $state-info-text;
-$panel-info-border: $state-info-border;
-$panel-info-heading-bg: $state-info-bg;
-
-$panel-warning-text: $state-warning-text;
-$panel-warning-border: $state-warning-border;
-$panel-warning-heading-bg: $state-warning-bg;
-
-$panel-danger-text: $state-danger-text;
-$panel-danger-border: $state-danger-border;
-$panel-danger-heading-bg: $state-danger-bg;
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-$thumbnail-padding: 4px;
-//** Thumbnail background color
-$thumbnail-bg: $body-bg;
-//** Thumbnail border color
-$thumbnail-border: #ddd;
-//** Thumbnail border radius
-$thumbnail-border-radius: $border-radius-base;
-
-//** Custom text color for thumbnail captions
-$thumbnail-caption-color: $text-color;
-//** Padding around the thumbnail caption
-$thumbnail-caption-padding: 9px;
-
-//== Wells
-//
-//##
-
-$well-bg: $greenDark;
-$well-border: $well-bg;
-
-//== Badges
-//
-//##
-
-$badge-color: $black;
-//** Linked badge text color on hover
-$badge-link-hover-color: #fff;
-$badge-bg: $gray-light;
-
-//** Badge text color in active nav link
-$badge-active-color: $link-color;
-//** Badge background color in active nav link
-$badge-active-bg: $black;
-
-$badge-font-weight: normal;
-$badge-line-height: $line-height-base;
-$badge-border-radius: 0;
-
-//== Breadcrumbs
-//
-//##
-
-$breadcrumb-padding-vertical: 8px;
-$breadcrumb-padding-horizontal: 15px;
-//** Breadcrumb background color
-$breadcrumb-bg: #f5f5f5;
-//** Breadcrumb text color
-$breadcrumb-color: #ccc;
-//** Text color of current page in the breadcrumb
-$breadcrumb-active-color: $gray-light;
-//** Textual separator for between breadcrumb elements
-$breadcrumb-separator: "/";
-
-//== Carousel
-//
-//##
-
-$carousel-text-shadow: none;
-
-$carousel-control-color: #fff;
-$carousel-control-width: 15%;
-$carousel-control-opacity: 1;
-$carousel-control-font-size: $font-size-base;
-
-$carousel-indicator-active-bg: #fff;
-$carousel-indicator-border-color: #fff;
-
-$carousel-caption-color: #fff;
-
-//== Close
-//
-//##
-
-$close-font-weight: normal;
-$close-color: #000;
-$close-text-shadow: none;
-
-//== Code
-//
-//##
-
-$code-color: #c7254e;
-$code-bg: #f9f2f4;
-
-$kbd-color: #fff;
-$kbd-bg: #333;
-
-$pre-bg: #f5f5f5;
-$pre-color: $gray-dark;
-$pre-border-color: #ccc;
-$pre-scrollable-max-height: 340px;
-
-//== Type
-//
-//##
-
-//** Horizontal offset for forms and lists.
-$component-offset-horizontal: 180px;
-//** Text muted color
-$text-muted: $gray-dark;
-//** Abbreviations and acronyms border color
-$abbr-border-color: $gray-light;
-//** Headings small color
-$headings-small-color: $gray-light;
-//** Blockquote small color
-$blockquote-small-color: $gray-light;
-//** Blockquote font size
-$blockquote-font-size: $font-size-base;
-//** Blockquote border color
-$blockquote-border-color: $gray-lighter;
-//** Page header border color
-$page-header-border-color: $gray-lighter;
-//** Width of horizontal description list titles
-$dl-horizontal-offset: $component-offset-horizontal;
-//** Horizontal line color.
-$hr-border: $black;
diff --git a/src/assets/css/themes/_variables.i386.scss b/src/assets/css/themes/_variables.i386.scss
deleted file mode 100644
index 259646f..0000000
--- a/src/assets/css/themes/_variables.i386.scss
+++ /dev/null
@@ -1,39 +0,0 @@
-$blue: #5555ff;
-$cyan: #55ffff;
-$green: #55ff55;
-$indigo: #ff55ff;
-$red: #ff5555;
-$yellow: #fefe54;
-$orange: #a85400;
-$pink: #fe54fe;
-$purple: #fe5454;
-$primary: #fefe54;
-$body-bg: #000084;
-$gray-300: #bbb;
-$body-color: $gray-300;
-$link-hover-color: $white;
-$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$navbar-dark-color: $gray-300;
-$navbar-light-brand-color: $gray-300;
-$success: #00aa00;
-$danger: #aa0000;
-$info: #00aaaa;
-$warning: #aa00aa;
-$navbar-dark-active-color: $gray-100;
-$enable-rounded: false;
-$input-color: $white;
-$input-bg: rgb(102, 102, 102);
-$input-disabled-bg: $gray-800;
-$nav-tabs-link-active-color: $gray-100;
-$navbar-dark-hover-color: rgba($gray-300, 0.75);
-$light: $gray-800;
-$navbar-light-disabled-color: $gray-800;
-$navbar-light-active-color: $gray-100;
-$navbar-light-hover-color: $gray-200;
-$navbar-light-color: $gray-300;
-$card-bg: $gray-800;
-$card-border-color: $white;
-$input-placeholder-color: $gray-500;
-$mark-bg: #463b00;
-$secondary: $gray-900;
diff --git a/src/assets/css/themes/_variables.vaporwave-dark.scss b/src/assets/css/themes/_variables.vaporwave-dark.scss
deleted file mode 100644
index cbccc99..0000000
--- a/src/assets/css/themes/_variables.vaporwave-dark.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-$blue: #01cdfe;
-$indigo: #b967ff;
-$purple: #b967ff;
-$pink: rgb(255, 64, 186);
-$red: rgb(255, 95, 110);
-$orange: rgb(255, 167, 93);
-$yellow: #fffb96;
-$green: #05ffa1;
-$teal: #01cdfe;
-$cyan: #01cdfe;
-$enable-shadows: true;
-$enable-gradients: true;
-$enable-responsive-font-sizes: true;
-$body-bg: $gray-900;
-$body-color: $gray-200;
-$border-radius: 1rem;
-$border-radius-lg: 1rem;
-$font-family-monospace: Arial, "Noto Sans", sans-serif;
-$yiq-text-light: $gray-300;
-$secondary: $blue;
-$text-muted: $gray-500;
-$primary: $pink;
-$navbar-light-hover-color: rgba($primary, 0.7);
-$light: darken($gray-100, 1.5);
-$font-family-sans-serif: "Lucida Console", Monaco, monospace;
-$card-bg: $body-bg;
-$navbar-dark-color: rgba($body-bg, 0.5);
-$navbar-light-active-color: rgba($gray-200, 0.9);
-$navbar-light-disabled-color: rgba($gray-200, 0.3);
-$navbar-light-color: rgba($white, 0.5);
-$input-bg: $gray-700;
-$input-color: $gray-200;
-$input-disabled-bg: $gray-800;
-$input-border-color: $gray-800;
-$mark-bg: $gray-600;
-$pre-color: $gray-200;
-mark-bg: $gray-600;
diff --git a/src/assets/css/themes/_variables.vaporwave-light.scss b/src/assets/css/themes/_variables.vaporwave-light.scss
deleted file mode 100644
index 7749578..0000000
--- a/src/assets/css/themes/_variables.vaporwave-light.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-$blue: #01cdfe;
-$indigo: #b967ff;
-$purple: #b967ff;
-$pink: rgb(255, 64, 186);
-$red: rgb(255, 95, 110);
-$orange: rgb(255, 167, 93);
-$yellow: #fffb96;
-$green: #05ffa1;
-$teal: #01cdfe;
-$cyan: #01cdfe;
-$enable-shadows: true;
-$enable-gradients: true;
-$enable-responsive-font-sizes: true;
-$body-bg: $gray-100;
-$body-color: $gray-700;
-$border-radius: 1rem;
-$border-radius-lg: 1rem;
-$font-family-monospace: Arial, "Noto Sans", sans-serif;
-$yiq-text-light: $gray-300;
-$secondary: $blue;
-$text-muted: $gray-500;
-$primary: $pink;
-$navbar-light-hover-color: rgba($primary, 0.7);
-$light: darken($gray-100, 1.5);
-$font-family-sans-serif: "Lucida Console", Monaco, monospace;
diff --git a/src/client/index.tsx b/src/client/index.tsx
index 99f1237..7b6b6b1 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -1,18 +1,19 @@
 import { hydrate } from "inferno-hydrate";
-import { BrowserRouter } from "inferno-router";
+import { Router } from "inferno-router";
 import { App } from "../shared/components/app/app";
 import { initializeSite } from "../shared/utils";
 
 import "bootstrap/js/dist/collapse";
 import "bootstrap/js/dist/dropdown";
+import { HistoryService } from "../shared/services/HistoryService";
 
 const site = window.isoData.site_res;
 initializeSite(site);
 
 const wrapper = (
-  
+  
     
-  
+  
 );
 
 const root = document.getElementById("root");
diff --git a/src/server/index.tsx b/src/server/index.tsx
index a93595e..4302407 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -6,19 +6,20 @@ import { Helmet } from "inferno-helmet";
 import { matchPath, StaticRouter } from "inferno-router";
 import { renderToString } from "inferno-server";
 import IsomorphicCookie from "isomorphic-cookie";
-import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client";
+import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
 import path from "path";
 import process from "process";
 import serialize from "serialize-javascript";
 import sharp from "sharp";
 import { App } from "../shared/components/app/app";
-import { getHttpBase, getHttpBaseInternal } from "../shared/env";
+import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
 import {
   ILemmyConfig,
   InitialFetchRequest,
   IsoDataOptionalSite,
 } from "../shared/interfaces";
 import { routes } from "../shared/routes";
+import { RequestState, wrapClient } from "../shared/services/HttpService";
 import {
   ErrorPageData,
   favIconPngUrl,
@@ -38,7 +39,7 @@ if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
   server.use(function (_req, res, next) {
     res.setHeader(
       "Content-Security-Policy",
-      `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *`
+      `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
     );
     next();
   });
@@ -64,7 +65,13 @@ Disallow: /search/
 
 server.get("/service-worker.js", async (_req, res) => {
   res.setHeader("Content-Type", "application/javascript");
-  res.sendFile(path.resolve("./dist/service-worker.js"));
+  res.sendFile(
+    path.resolve(
+      `./dist/service-worker${
+        process.env.NODE_ENV === "development" ? "-development" : ""
+      }.js`
+    )
+  );
 });
 
 server.get("/robots.txt", async (_req, res) => {
@@ -121,7 +128,7 @@ server.get("/*", async (req, res) => {
     const getSiteForm: GetSite = { auth };
 
     const headers = setForwardedHeaders(req.headers);
-    const client = new LemmyHttp(getHttpBaseInternal(), headers);
+    const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
 
     const { path, url, query } = req;
 
@@ -129,27 +136,30 @@ server.get("/*", async (req, res) => {
     // This bypasses errors, so that the client can hit the error on its own,
     // in order to remove the jwt on the browser. Necessary for wrong jwts
     let site: GetSiteResponse | undefined = undefined;
-    let routeData: any[] = [];
-    let errorPageData: ErrorPageData | undefined;
-    try {
-      let try_site: any = await client.getSite(getSiteForm);
-      if (try_site.error == "not_logged_in") {
-        console.error(
-          "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
-        );
-        getSiteForm.auth = undefined;
-        auth = undefined;
-        try_site = await client.getSite(getSiteForm);
-      }
+    const routeData: RequestState[] = [];
+    let errorPageData: ErrorPageData | undefined = undefined;
+    let try_site = await client.getSite(getSiteForm);
+    if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
+      console.error(
+        "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
+      );
+      getSiteForm.auth = undefined;
+      auth = undefined;
+      try_site = await client.getSite(getSiteForm);
+    }
 
-      if (!auth && isAuthPath(path)) {
-        res.redirect("/login");
-        return;
-      }
+    if (!auth && isAuthPath(path)) {
+      return res.redirect("/login");
+    }
 
-      site = try_site;
+    if (try_site.state === "success") {
+      site = try_site.data;
       initializeSite(site);
 
+      if (path != "/setup" && !site.site_view.local_site.site_setup) {
+        return res.redirect("/setup");
+      }
+
       if (site) {
         const initialFetchReq: InitialFetchRequest = {
           client,
@@ -160,23 +170,25 @@ server.get("/*", async (req, res) => {
         };
 
         if (activeRoute?.fetchInitialData) {
-          routeData = await Promise.all([
-            ...activeRoute.fetchInitialData(initialFetchReq),
-          ]);
+          routeData.push(
+            ...(await Promise.all([
+              ...activeRoute.fetchInitialData(initialFetchReq),
+            ]))
+          );
         }
       }
-    } catch (error) {
-      errorPageData = getErrorPageData(error, site);
+    } else if (try_site.state === "failed") {
+      errorPageData = getErrorPageData(new Error(try_site.msg), site);
     }
 
     // Redirect to the 404 if there's an API error
-    if (routeData[0] && routeData[0].error) {
-      const error = routeData[0].error;
+    if (routeData[0] && routeData[0].state === "failed") {
+      const error = routeData[0].msg;
       console.error(error);
       if (error === "instance_is_private") {
         return res.redirect(`/signup`);
       } else {
-        errorPageData = getErrorPageData(error, site);
+        errorPageData = getErrorPageData(new Error(error), site);
       }
     }
 
@@ -213,15 +225,15 @@ server.listen(Number(port), hostname, () => {
 function setForwardedHeaders(headers: IncomingHttpHeaders): {
   [key: string]: string;
 } {
-  let out: { [key: string]: string } = {};
+  const out: { [key: string]: string } = {};
   if (headers.host) {
     out.host = headers.host;
   }
-  let realIp = headers["x-real-ip"];
+  const realIp = headers["x-real-ip"];
   if (realIp) {
     out["x-real-ip"] = realIp as string;
   }
-  let forwardedFor = headers["x-forwarded-for"];
+  const forwardedFor = headers["x-forwarded-for"];
   if (forwardedFor) {
     out["x-forwarded-for"] = forwardedFor as string;
   }
@@ -234,7 +246,7 @@ process.on("SIGINT", () => {
   process.exit(0);
 });
 
-const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
+const iconSizes = [72, 96, 144, 192, 512];
 const defaultLogoPathDirectory = path.join(
   process.cwd(),
   "dist",
@@ -242,12 +254,15 @@ const defaultLogoPathDirectory = path.join(
   "icons"
 );
 
-export async function generateManifestBase64(site: Site) {
-  const url = (
-    process.env.NODE_ENV === "development"
-      ? "http://localhost:1236/"
-      : getHttpBase()
-  ).replace(/\/$/g, "");
+export async function generateManifestBase64({
+  my_user,
+  site_view: {
+    site,
+    local_site: { community_creation_admin_only },
+  },
+}: GetSiteResponse) {
+  const url = getHttpBaseExternal();
+
   const icon = site.icon ? await fetchIconPng(site.icon) : null;
 
   const manifest = {
@@ -281,15 +296,58 @@ export async function generateManifestBase64(site: Site) {
         };
       })
     ),
+    shortcuts: [
+      {
+        name: "Search",
+        short_name: "Search",
+        description: "Perform a search.",
+        url: "/search",
+      },
+      {
+        name: "Communities",
+        url: "/communities",
+        short_name: "Communities",
+        description: "Browse communities",
+      },
+    ]
+      .concat(
+        my_user
+          ? [
+              {
+                name: "Create Post",
+                url: "/create_post",
+                short_name: "Create Post",
+                description: "Create a post.",
+              },
+            ]
+          : []
+      )
+      .concat(
+        my_user?.local_user_view.person.admin || !community_creation_admin_only
+          ? [
+              {
+                name: "Create Community",
+                url: "/create_community",
+                short_name: "Create Community",
+                description: "Create a community",
+              },
+            ]
+          : []
+      ),
+    related_applications: [
+      {
+        platform: "f-droid",
+        url: "https://f-droid.org/packages/com.jerboa/",
+        id: "com.jerboa",
+      },
+    ],
   };
 
   return Buffer.from(JSON.stringify(manifest)).toString("base64");
 }
 
 async function fetchIconPng(iconUrl: string) {
-  return await fetch(
-    iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal())
-  )
+  return await fetch(iconUrl)
     .then(res => res.blob())
     .then(blob => blob.arrayBuffer());
 }
@@ -330,14 +388,15 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
         .then(buf => buf.toString("base64"))}`
     : favIconPngUrl;
 
-  const eruda = (
-    <>
-      
-      
-    
-  );
-
-  const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
+  const erudaStr =
+    process.env["LEMMY_UI_DEBUG"] === "true"
+      ? renderToString(
+          <>
+            
+            
+          
+        )
+      : "";
 
   const helmet = Helmet.renderStatic();
 
@@ -345,9 +404,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
 
   return `
   
-  
+  
   
-  
+  
   
 
   
@@ -375,9 +434,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
     site &&
     ``
   }
   
diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx
index 9e6e9bd..96857f3 100644
--- a/src/shared/components/app/app.tsx
+++ b/src/shared/components/app/app.tsx
@@ -1,8 +1,8 @@
 import { Component } from "inferno";
 import { Provider } from "inferno-i18next-dess";
 import { Route, Switch } from "inferno-router";
-import { IsoDataOptionalSite } from "shared/interfaces";
 import { i18n } from "../../i18next";
+import { IsoDataOptionalSite } from "../../interfaces";
 import { routes } from "../../routes";
 import { isAuthPath, setIsoData } from "../../utils";
 import AuthGuard from "../common/auth-guard";
diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx
index 25c3a30..bdbac9f 100644
--- a/src/shared/components/app/navbar.tsx
+++ b/src/shared/components/app/navbar.tsx
@@ -1,35 +1,23 @@
 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 { UserService } from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
 import {
   amAdmin,
   canCreateCommunity,
   donateLemmyUrl,
   isBrowser,
   myAuth,
-  notifyComment,
-  notifyPrivateMessage,
   numToSI,
   showAvatars,
   toast,
-  wsClient,
-  wsSubscribe,
 } from "../../utils";
 import { Icon } from "../common/icon";
 import { PictrsImage } from "../common/pictrs-image";
@@ -39,9 +27,9 @@ interface NavbarProps {
 }
 
 interface NavbarState {
-  unreadInboxCount: number;
-  unreadReportCount: number;
-  unreadApplicationCount: number;
+  unreadInboxCountRes: RequestState;
+  unreadReportCountRes: RequestState;
+  unreadApplicationCountRes: RequestState;
   onSiteBanner?(url: string): any;
 }
 
@@ -51,77 +39,48 @@ function handleCollapseClick(i: Navbar) {
   }
 }
 
-function handleLogOut() {
+function handleLogOut(i: Navbar) {
   UserService.Instance.logout();
+  handleCollapseClick(i);
 }
 
 export class Navbar extends Component {
-  private wsSub: Subscription;
-  private userSub: Subscription;
-  private unreadInboxCountSub: Subscription;
-  private unreadReportCountSub: Subscription;
-  private unreadApplicationCountSub: Subscription;
   state: NavbarState = {
-    unreadInboxCount: 0,
-    unreadReportCount: 0,
-    unreadApplicationCount: 0,
+    unreadInboxCountRes: { state: "empty" },
+    unreadReportCountRes: { state: "empty" },
+    unreadApplicationCountRes: { state: "empty" },
   };
-  subscription: any;
   collapseButtonRef = createRef();
+  mobileMenuRef = createRef();
 
   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();
+      await 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 page
   render() {
+    return this.navbar();
+  }
+
+  // TODO class active corresponding to current page
+  navbar() {
     const siteView = this.props.siteRes?.site_view;
     const person = UserService.Instance.myUserInfo?.local_user_view.person;
     return (
@@ -144,15 +103,15 @@ export class Navbar extends Component {
                 to="/inbox"
                 className="p-1 nav-link border-0"
                 title={i18n.t("unread_messages", {
-                  count: Number(this.state.unreadInboxCount),
-                  formattedCount: numToSI(this.state.unreadInboxCount),
+                  count: Number(this.state.unreadApplicationCountRes.state),
+                  formattedCount: numToSI(this.unreadInboxCount),
                 })}
                 onMouseUp={linkEvent(this, handleCollapseClick)}
               >
                 
-                {this.state.unreadInboxCount > 0 && (
+                {this.unreadInboxCount > 0 && (
                   
-                    {numToSI(this.state.unreadInboxCount)}
+                    {numToSI(this.unreadInboxCount)}
                   
                 )}
               
@@ -163,15 +122,15 @@ export class Navbar extends Component {
                   to="/reports"
                   className="p-1 nav-link border-0"
                   title={i18n.t("unread_reports", {
-                    count: Number(this.state.unreadReportCount),
-                    formattedCount: numToSI(this.state.unreadReportCount),
+                    count: Number(this.unreadReportCount),
+                    formattedCount: numToSI(this.unreadReportCount),
                   })}
                   onMouseUp={linkEvent(this, handleCollapseClick)}
                 >
                   
-                  {this.state.unreadReportCount > 0 && (
+                  {this.unreadReportCount > 0 && (
                     
-                      {numToSI(this.state.unreadReportCount)}
+                      {numToSI(this.unreadReportCount)}
                     
                   )}
                 
@@ -183,15 +142,15 @@ export class Navbar extends Component {
                   to="/registration_applications"
                   className="p-1 nav-link border-0"
                   title={i18n.t("unread_registration_applications", {
-                    count: Number(this.state.unreadApplicationCount),
-                    formattedCount: numToSI(this.state.unreadApplicationCount),
+                    count: Number(this.unreadApplicationCount),
+                    formattedCount: numToSI(this.unreadApplicationCount),
                   })}
                   onMouseUp={linkEvent(this, handleCollapseClick)}
                 >
                   
-                  {this.state.unreadApplicationCount > 0 && (
+                  {this.unreadApplicationCount > 0 && (
                     
-                      {numToSI(this.state.unreadApplicationCount)}
+                      {numToSI(this.unreadApplicationCount)}
                     
                   )}
                 
@@ -212,7 +171,11 @@ export class Navbar extends Component {
         >
           
         
-        
           
         )}
 
         {this.state.showPurgeDialog && (
-          
+