mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-27 19:09:12 +00:00
Merge branch 'main' into breakout-role-utils
This commit is contained in:
commit
02ffa85b58
|
@ -3,11 +3,12 @@
|
|||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint", "jsx-a11y"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:inferno/recommended"
|
||||
"plugin:inferno/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
|
@ -20,6 +21,16 @@
|
|||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"arrow-body-style": 0,
|
||||
"jsx-a11y/alt-text": 1,
|
||||
"jsx-a11y/anchor-is-valid": 1,
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||
"jsx-a11y/aria-role": 1,
|
||||
"jsx-a11y/click-events-have-key-events": 1,
|
||||
"jsx-a11y/iframe-has-title": 1,
|
||||
"jsx-a11y/interactive-supports-focus": 1,
|
||||
"jsx-a11y/no-redundant-roles": 1,
|
||||
"jsx-a11y/no-static-element-interactions": 1,
|
||||
"jsx-a11y/role-has-required-aria-props": 1,
|
||||
"curly": 0,
|
||||
"eol-last": 0,
|
||||
"eqeqeq": 0,
|
||||
|
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
|
@ -8,7 +8,7 @@ body:
|
|||
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)
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
|
|
6
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
6
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
|
@ -1,16 +1,16 @@
|
|||
name: "\U0001F680 Feature request"
|
||||
description: Suggest an idea for improving Lemmy
|
||||
description: Suggest an idea for improving Lemmy's UI
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Have a suggestion about Lemmy's UI?
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
description: Before you create a bug report please do the following.
|
||||
description: Before you create a feature request please do the following.
|
||||
options:
|
||||
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
||||
required: true
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
src/shared/translations
|
||||
lemmy-translations
|
||||
lemmy-translations
|
||||
src/assets/css/themes/*.css
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
"bootstrap-v4": "npm:bootstrap@^4.6.2",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-plugin-inferno": "^7.32.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.3",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
|
|
|
@ -1,108 +1,13 @@
|
|||
$white: #fff;
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #ebebeb;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-400: #ced4da;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #888;
|
||||
$gray-700: #444;
|
||||
$gray-800: #303030;
|
||||
$gray-900: #222;
|
||||
$black: #000;
|
||||
$blue: #375a7f;
|
||||
$indigo: #6610f2;
|
||||
$purple: #6f42c1;
|
||||
$pink: #e83e8c;
|
||||
$red: #e74c3c;
|
||||
$orange: #fd7e14;
|
||||
$yellow: #f39c12;
|
||||
$green: #00bc8c;
|
||||
$teal: #20c997;
|
||||
$cyan: #3498db;
|
||||
@import "variables.darkly";
|
||||
|
||||
$primary: $blue;
|
||||
$secondary: #444;
|
||||
$success: $green;
|
||||
$info: $cyan;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
$light: $gray-800;
|
||||
$dark: $gray-300;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
$body-bg: $gray-900;
|
||||
$body-color: $gray-300;
|
||||
|
||||
$theme-colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"light": $light,
|
||||
);
|
||||
|
||||
$link-color: $red;
|
||||
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-size-base: 0.9375rem;
|
||||
$h1-font-size: 3rem;
|
||||
$h2-font-size: 2.5rem;
|
||||
$h3-font-size: 2rem;
|
||||
$text-muted: $gray-600;
|
||||
$table-accent-bg: $gray-800;
|
||||
$table-border-color: $gray-700;
|
||||
$input-border-color: $body-bg;
|
||||
$input-group-addon-color: $gray-500;
|
||||
$input-group-addon-bg: $gray-700;
|
||||
$custom-file-color: $gray-500;
|
||||
$custom-file-border-color: $body-bg;
|
||||
$dropdown-bg: $gray-900;
|
||||
$dropdown-border-color: $gray-700;
|
||||
$dropdown-divider-bg: $gray-700;
|
||||
$dropdown-link-color: $white;
|
||||
$dropdown-link-hover-color: $white;
|
||||
$dropdown-link-hover-bg: $primary;
|
||||
$nav-link-padding-x: 2rem;
|
||||
$nav-link-disabled-color: $gray-500;
|
||||
$nav-tabs-border-color: $gray-700;
|
||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
||||
transparent;
|
||||
$nav-tabs-link-active-color: $white;
|
||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
||||
$nav-tabs-border-color transparent;
|
||||
$navbar-padding-y: 1rem;
|
||||
$navbar-dark-color: rgba($white, 0.6);
|
||||
$navbar-dark-hover-color: $white;
|
||||
$navbar-light-color: rgba($white, 0.6);
|
||||
$navbar-light-hover-color: $white;
|
||||
$navbar-light-active-color: $white;
|
||||
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
|
||||
$pagination-color: $white;
|
||||
$pagination-bg: $success;
|
||||
$pagination-border-width: 0;
|
||||
$pagination-border-color: transparent;
|
||||
$pagination-hover-color: $white;
|
||||
$pagination-hover-bg: lighten($success, 10%);
|
||||
$pagination-hover-border-color: transparent;
|
||||
$pagination-active-bg: $pagination-hover-bg;
|
||||
$pagination-active-border-color: transparent;
|
||||
$pagination-disabled-color: $white;
|
||||
$pagination-disabled-bg: darken($success, 15%);
|
||||
$pagination-disabled-border-color: transparent;
|
||||
$jumbotron-bg: $gray-800;
|
||||
$card-cap-bg: $gray-700;
|
||||
$card-bg: $gray-800;
|
||||
$popover-bg: $gray-800;
|
||||
$popover-header-bg: $gray-700;
|
||||
$toast-background-color: $gray-700;
|
||||
$toast-header-background-color: $gray-800;
|
||||
$modal-content-bg: $gray-800;
|
||||
$modal-content-border-color: $gray-700;
|
||||
$modal-header-border-color: $gray-700;
|
||||
$progress-bg: $gray-700;
|
||||
$list-group-bg: $gray-800;
|
||||
$list-group-border-color: $gray-700;
|
||||
$list-group-hover-bg: $gray-700;
|
||||
$breadcrumb-bg: $gray-700;
|
||||
$close-color: $white;
|
||||
$close-text-shadow: none;
|
||||
$pre-color: inherit;
|
||||
$mark-bg: #333;
|
||||
$custom-select-bg: $secondary;
|
||||
$custom-select-color: $white;
|
||||
$input-bg: $secondary;
|
||||
$input-color: $white;
|
||||
$input-disabled-bg: darken($secondary, 10%);
|
||||
$light: $gray-800;
|
||||
$navbar-light-brand-color: $white;
|
||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
||||
|
|
|
@ -1,35 +1,59 @@
|
|||
// Colors
|
||||
$white: #fff;
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #ebebeb;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-400: #ced4da;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #888;
|
||||
$gray-700: #444;
|
||||
$gray-800: #303030;
|
||||
$gray-900: #222;
|
||||
$black: #000;
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$grays: (
|
||||
"gray-200": $gray-200,
|
||||
"gray-600": $gray-600,
|
||||
"gray-700": $gray-700,
|
||||
"gray-800": $gray-800,
|
||||
"gray-900": $gray-900,
|
||||
);
|
||||
|
||||
$blue: #375a7f;
|
||||
$indigo: #6610f2;
|
||||
$purple: #6f42c1;
|
||||
$pink: #e83e8c;
|
||||
$red: #e74c3c;
|
||||
$orange: #fd7e14;
|
||||
$yellow: #f39c12;
|
||||
$green: #00bc8c;
|
||||
$teal: #20c997;
|
||||
$cyan: #3498db;
|
||||
$primary: $blue;
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$colors: (
|
||||
"blue": $blue,
|
||||
"red": $red,
|
||||
"yellow": $yellow,
|
||||
"green": $green,
|
||||
"cyan": $cyan,
|
||||
);
|
||||
|
||||
$primary: $green;
|
||||
$secondary: $gray-700;
|
||||
$success: $green;
|
||||
$info: $cyan;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
$dark: $gray-300;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
$body-bg: $gray-900;
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$theme-colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"dark": $dark,
|
||||
);
|
||||
|
||||
$body-color: $gray-300;
|
||||
$body-bg: $gray-900;
|
||||
$link-color: $success;
|
||||
$mark-bg: #333;
|
||||
$text-muted: $gray-600;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
|
||||
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
@ -37,28 +61,10 @@ $font-size-base: 0.9375rem;
|
|||
$h1-font-size: 3rem;
|
||||
$h2-font-size: 2.5rem;
|
||||
$h3-font-size: 2rem;
|
||||
$text-muted: $gray-600;
|
||||
$table-accent-bg: $gray-800;
|
||||
$table-border-color: $gray-700;
|
||||
$input-border-color: $body-bg;
|
||||
$input-group-addon-color: $gray-500;
|
||||
$input-group-addon-bg: $gray-700;
|
||||
$custom-file-color: $gray-500;
|
||||
$custom-file-border-color: $body-bg;
|
||||
$dropdown-bg: $gray-900;
|
||||
$dropdown-border-color: $gray-700;
|
||||
$dropdown-divider-bg: $gray-700;
|
||||
$dropdown-link-color: $white;
|
||||
$dropdown-link-hover-color: $white;
|
||||
$dropdown-link-hover-bg: $primary;
|
||||
$nav-link-padding-x: 2rem;
|
||||
$nav-link-disabled-color: $gray-500;
|
||||
$nav-tabs-border-color: $gray-700;
|
||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
||||
transparent;
|
||||
$nav-tabs-link-active-color: $white;
|
||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
||||
$nav-tabs-border-color transparent;
|
||||
|
||||
$card-cap-bg: $gray-700;
|
||||
$card-bg: $gray-800;
|
||||
|
||||
$navbar-padding-y: 1rem;
|
||||
$navbar-dark-color: rgba($white, 0.6);
|
||||
$navbar-dark-hover-color: $white;
|
||||
|
@ -66,6 +72,41 @@ $navbar-light-color: rgba($white, 0.6);
|
|||
$navbar-light-hover-color: $white;
|
||||
$navbar-light-active-color: $white;
|
||||
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
|
||||
$navbar-light-brand-color: $white;
|
||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
||||
|
||||
$nav-link-padding-x: 2rem;
|
||||
$nav-link-disabled-color: $gray-500;
|
||||
|
||||
$nav-tabs-border-color: $gray-700;
|
||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
||||
transparent;
|
||||
$nav-tabs-link-active-color: $white;
|
||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
||||
$nav-tabs-border-color transparent;
|
||||
|
||||
$input-bg: $gray-700;
|
||||
$input-color: $white;
|
||||
$input-disabled-bg: darken($gray-700, 10%);
|
||||
$input-border-color: $body-bg;
|
||||
$input-group-addon-color: $gray-500;
|
||||
$input-group-addon-bg: $gray-700;
|
||||
|
||||
$hr-border-color: rgba($body-color, 0.25);
|
||||
|
||||
$table-accent-bg: $gray-800;
|
||||
$table-border-color: $gray-700;
|
||||
|
||||
$custom-file-color: $gray-500;
|
||||
$custom-file-border-color: $body-bg;
|
||||
|
||||
$dropdown-bg: $gray-900;
|
||||
$dropdown-border-color: $gray-700;
|
||||
$dropdown-divider-bg: $gray-700;
|
||||
$dropdown-link-color: $white;
|
||||
$dropdown-link-hover-color: $white;
|
||||
$dropdown-link-hover-bg: $primary;
|
||||
|
||||
$pagination-color: $white;
|
||||
$pagination-bg: $success;
|
||||
$pagination-border-width: 0;
|
||||
|
@ -78,9 +119,8 @@ $pagination-active-border-color: transparent;
|
|||
$pagination-disabled-color: $white;
|
||||
$pagination-disabled-bg: darken($success, 15%);
|
||||
$pagination-disabled-border-color: transparent;
|
||||
|
||||
$jumbotron-bg: $gray-800;
|
||||
$card-cap-bg: $gray-700;
|
||||
$card-bg: $gray-800;
|
||||
$popover-bg: $gray-800;
|
||||
$popover-header-bg: $gray-700;
|
||||
$toast-background-color: $gray-700;
|
||||
|
@ -96,12 +136,6 @@ $breadcrumb-bg: $gray-700;
|
|||
$close-color: $white;
|
||||
$close-text-shadow: none;
|
||||
$pre-color: inherit;
|
||||
$mark-bg: #333;
|
||||
$custom-select-bg: $secondary;
|
||||
$custom-select-bg: $gray-700;
|
||||
$custom-select-color: $white;
|
||||
$input-bg: $secondary;
|
||||
$input-color: $white;
|
||||
$input-disabled-bg: darken($secondary, 10%);
|
||||
$light: $gray-800;
|
||||
$navbar-light-brand-color: $white;
|
||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
||||
|
|
|
@ -1,47 +1,9 @@
|
|||
$white: #fff;
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #e9ecef;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-400: #ced4da;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #6c757d;
|
||||
$gray-700: #495057;
|
||||
$gray-800: #343a40;
|
||||
$gray-900: #212529;
|
||||
$black: #000;
|
||||
$blue: #007bff;
|
||||
$indigo: #6610f2;
|
||||
$white: #ffffff;
|
||||
$orange: #f1641e;
|
||||
$cyan: #02bdc2;
|
||||
$green: #00c853;
|
||||
$primary: #f1641e;
|
||||
@import "variables.darkly";
|
||||
|
||||
$secondary: #c80000;
|
||||
$info: $blue;
|
||||
$body-color: $gray-700;
|
||||
$link-color: $primary;
|
||||
$red: #d8486a;
|
||||
$border-radius: 0.5rem;
|
||||
$border-radius-lg: 0.5rem;
|
||||
$border-radius-sm: 1rem;
|
||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
||||
"Segoe UI", "Helvetica", Arial, sans-serif;
|
||||
$headings-color: $gray-700;
|
||||
$input-btn-focus-color: rgba($primary, 0.75);
|
||||
$form-feedback-valid-color: $info;
|
||||
$navbar-light-color: $gray-600;
|
||||
$black: #222222;
|
||||
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
||||
$navbar-light-active-color: $gray-900;
|
||||
$card-color: $gray-700;
|
||||
$card-cap-color: $gray-700;
|
||||
$info: $blue;
|
||||
$body-bg: #fff;
|
||||
$success: $indigo;
|
||||
$danger: darken($primary, 24%);
|
||||
$navbar-light-hover-color: $gray-900;
|
||||
$card-bg: $gray-100;
|
||||
$border-color: $gray-700;
|
||||
$mark-bg: rgb(255, 252, 239);
|
||||
$font-weight-bold: 600;
|
||||
$rounded-pill: 0.25rem;
|
||||
|
||||
$theme-colors: (
|
||||
"secondary": $secondary,
|
||||
"danger": $danger,
|
||||
);
|
||||
|
|
|
@ -1,47 +1,80 @@
|
|||
$white: #fff;
|
||||
// Colors
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #e9ecef;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-400: #ced4da;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #6c757d;
|
||||
$gray-700: #495057;
|
||||
$gray-800: #343a40;
|
||||
$gray-900: #212529;
|
||||
$black: #000;
|
||||
$black: #222;
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$grays: (
|
||||
"gray-200": $gray-200,
|
||||
"gray-600": $gray-600,
|
||||
"gray-700": $gray-700,
|
||||
"gray-800": $gray-800,
|
||||
"gray-900": $gray-900,
|
||||
);
|
||||
|
||||
$blue: #007bff;
|
||||
$indigo: #6610f2;
|
||||
$white: #ffffff;
|
||||
$red: #d8486a;
|
||||
$orange: #f1641e;
|
||||
$cyan: #02bdc2;
|
||||
$green: #00c853;
|
||||
$cyan: #02bdc2;
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$colors: (
|
||||
"red": $red,
|
||||
"orange": $orange,
|
||||
"cyan": $cyan,
|
||||
"green": $green,
|
||||
);
|
||||
|
||||
$primary: $orange;
|
||||
$secondary: $green;
|
||||
$info: $cyan;
|
||||
$success: $indigo;
|
||||
$info: $blue;
|
||||
$danger: darken($primary, 25%);
|
||||
|
||||
// Writing these maps is necessary for Bootstrap theming:
|
||||
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||
$theme-colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"success": $success,
|
||||
"info": $info,
|
||||
"danger": $danger,
|
||||
);
|
||||
|
||||
$body-color: $gray-700;
|
||||
$body-bg: #fff;
|
||||
$link-color: $primary;
|
||||
$red: #d8486a;
|
||||
$border-color: $gray-700;
|
||||
$mark-bg: rgb(255, 252, 239);
|
||||
$headings-color: $gray-700;
|
||||
|
||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
||||
"Segoe UI", "Helvetica", Arial, sans-serif;
|
||||
$font-weight-bold: 600;
|
||||
|
||||
$card-color: $gray-700;
|
||||
$card-cap-color: $gray-700;
|
||||
$card-bg: $gray-100;
|
||||
|
||||
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
||||
$navbar-light-color: $gray-600;
|
||||
$navbar-light-hover-color: $gray-900;
|
||||
$navbar-light-active-color: $gray-900;
|
||||
|
||||
$form-feedback-valid-color: $info;
|
||||
$input-btn-focus-color: rgba($primary, 0.75);
|
||||
|
||||
$border-radius: 0.5rem;
|
||||
$border-radius-lg: 0.5rem;
|
||||
$border-radius-sm: 1rem;
|
||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
||||
"Segoe UI", "Helvetica", Arial, sans-serif;
|
||||
$headings-color: $gray-700;
|
||||
$input-btn-focus-color: rgba($primary, 0.75);
|
||||
$form-feedback-valid-color: $info;
|
||||
$navbar-light-color: $gray-600;
|
||||
$black: #222222;
|
||||
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
||||
$navbar-light-active-color: $gray-900;
|
||||
$card-color: $gray-700;
|
||||
$card-cap-color: $gray-700;
|
||||
$info: $blue;
|
||||
$body-bg: #fff;
|
||||
$success: $indigo;
|
||||
$danger: darken($primary, 25%);
|
||||
$navbar-light-hover-color: $gray-900;
|
||||
$card-bg: $gray-100;
|
||||
$border-color: $gray-700;
|
||||
$mark-bg: rgb(255, 252, 239);
|
||||
$font-weight-bold: 600;
|
||||
$rounded-pill: 0.25rem;
|
||||
|
||||
$hr-border-color: rgba($body-color, 0.25);
|
||||
|
|
|
@ -450,7 +450,7 @@ hr {
|
|||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid rgba(222, 226, 230, 0.25);
|
||||
}
|
||||
|
||||
small,
|
||||
|
@ -3245,7 +3245,7 @@ input[type="button"].btn-block {
|
|||
.dropdown-item:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
|
|
|
@ -19,14 +19,13 @@
|
|||
--white: #fff;
|
||||
--gray: #888;
|
||||
--gray-dark: #303030;
|
||||
--primary: #375a7f;
|
||||
--primary: #00bc8c;
|
||||
--secondary: #444;
|
||||
--success: #00bc8c;
|
||||
--info: #3498db;
|
||||
--warning: #f39c12;
|
||||
--danger: #e74c3c;
|
||||
--light: #303030;
|
||||
--medium-light: var(--secondary);
|
||||
--dark: #dee2e6;
|
||||
--breakpoint-xs: 0;
|
||||
--breakpoint-sm: 576px;
|
||||
|
@ -451,7 +450,7 @@ hr {
|
|||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid rgba(222, 226, 230, 0.25);
|
||||
}
|
||||
|
||||
small,
|
||||
|
@ -1625,21 +1624,21 @@ pre code {
|
|||
.table-primary,
|
||||
.table-primary > th,
|
||||
.table-primary > td {
|
||||
background-color: #c7d1db;
|
||||
background-color: #b8ecdf;
|
||||
}
|
||||
.table-primary th,
|
||||
.table-primary td,
|
||||
.table-primary thead th,
|
||||
.table-primary tbody + tbody {
|
||||
border-color: #97a9bc;
|
||||
border-color: #7adcc3;
|
||||
}
|
||||
|
||||
.table-hover .table-primary:hover {
|
||||
background-color: #b7c4d1;
|
||||
background-color: #a4e7d6;
|
||||
}
|
||||
.table-hover .table-primary:hover > td,
|
||||
.table-hover .table-primary:hover > th {
|
||||
background-color: #b7c4d1;
|
||||
background-color: #a4e7d6;
|
||||
}
|
||||
|
||||
.table-secondary,
|
||||
|
@ -1908,9 +1907,9 @@ pre code {
|
|||
.form-control:focus {
|
||||
color: #fff;
|
||||
background-color: #444;
|
||||
border-color: #739ac2;
|
||||
border-color: #3dffcd;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.form-control::placeholder {
|
||||
color: #888;
|
||||
|
@ -2408,7 +2407,7 @@ textarea.form-control.is-invalid {
|
|||
.btn:focus,
|
||||
.btn.focus {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.btn.disabled,
|
||||
.btn:disabled {
|
||||
|
@ -2424,38 +2423,38 @@ fieldset:disabled a.btn {
|
|||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #2b4764;
|
||||
border-color: #28415b;
|
||||
background-color: #009670;
|
||||
border-color: #008966;
|
||||
}
|
||||
.btn-primary:focus,
|
||||
.btn-primary.focus {
|
||||
color: #fff;
|
||||
background-color: #2b4764;
|
||||
border-color: #28415b;
|
||||
box-shadow: 0 0 0 0.2rem rgba(85, 115, 146, 0.5);
|
||||
background-color: #009670;
|
||||
border-color: #008966;
|
||||
box-shadow: 0 0 0 0.2rem rgba(38, 198, 157, 0.5);
|
||||
}
|
||||
.btn-primary.disabled,
|
||||
.btn-primary:disabled {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.btn-primary:not(:disabled):not(.disabled):active,
|
||||
.btn-primary:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-primary.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #28415b;
|
||||
border-color: #243a53;
|
||||
background-color: #008966;
|
||||
border-color: #007c5d;
|
||||
}
|
||||
.btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||
.btn-primary:not(:disabled):not(.disabled).active:focus,
|
||||
.show > .btn-primary.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(85, 115, 146, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(38, 198, 157, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
|
@ -2711,34 +2710,34 @@ fieldset:disabled a.btn {
|
|||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.btn-outline-primary:focus,
|
||||
.btn-outline-primary.focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
.btn-outline-primary.disabled,
|
||||
.btn-outline-primary:disabled {
|
||||
color: #375a7f;
|
||||
color: #00bc8c;
|
||||
background-color: transparent;
|
||||
}
|
||||
.btn-outline-primary:not(:disabled):not(.disabled):active,
|
||||
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-outline-primary.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
|
||||
.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
|
||||
.show > .btn-outline-primary.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
|
@ -3246,13 +3245,13 @@ input[type="button"].btn-block {
|
|||
.dropdown-item:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
.dropdown-item.disabled,
|
||||
.dropdown-item:disabled {
|
||||
|
@ -3617,19 +3616,19 @@ input[type="button"].btn-block {
|
|||
}
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
color: #fff;
|
||||
border-color: #375a7f;
|
||||
background-color: #375a7f;
|
||||
border-color: #00bc8c;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
.custom-control-input:focus ~ .custom-control-label::before {
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
|
||||
border-color: #739ac2;
|
||||
border-color: #3dffcd;
|
||||
}
|
||||
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
|
||||
color: #fff;
|
||||
background-color: #97b3d2;
|
||||
border-color: #97b3d2;
|
||||
background-color: #70ffda;
|
||||
border-color: #70ffda;
|
||||
}
|
||||
.custom-control-input[disabled] ~ .custom-control-label,
|
||||
.custom-control-input:disabled ~ .custom-control-label {
|
||||
|
@ -3677,8 +3676,8 @@ input[type="button"].btn-block {
|
|||
.custom-checkbox
|
||||
.custom-control-input:indeterminate
|
||||
~ .custom-control-label::before {
|
||||
border-color: #375a7f;
|
||||
background-color: #375a7f;
|
||||
border-color: #00bc8c;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
.custom-checkbox
|
||||
.custom-control-input:indeterminate
|
||||
|
@ -3688,12 +3687,12 @@ input[type="button"].btn-block {
|
|||
.custom-checkbox
|
||||
.custom-control-input:disabled:checked
|
||||
~ .custom-control-label::before {
|
||||
background-color: rgba(55, 90, 127, 0.5);
|
||||
background-color: rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
.custom-checkbox
|
||||
.custom-control-input:disabled:indeterminate
|
||||
~ .custom-control-label::before {
|
||||
background-color: rgba(55, 90, 127, 0.5);
|
||||
background-color: rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
|
||||
.custom-radio .custom-control-label::before {
|
||||
|
@ -3705,7 +3704,7 @@ input[type="button"].btn-block {
|
|||
.custom-radio
|
||||
.custom-control-input:disabled:checked
|
||||
~ .custom-control-label::before {
|
||||
background-color: rgba(55, 90, 127, 0.5);
|
||||
background-color: rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
|
||||
.custom-switch {
|
||||
|
@ -3739,7 +3738,7 @@ input[type="button"].btn-block {
|
|||
.custom-switch
|
||||
.custom-control-input:disabled:checked
|
||||
~ .custom-control-label::before {
|
||||
background-color: rgba(55, 90, 127, 0.5);
|
||||
background-color: rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
|
@ -3760,9 +3759,9 @@ input[type="button"].btn-block {
|
|||
appearance: none;
|
||||
}
|
||||
.custom-select:focus {
|
||||
border-color: #739ac2;
|
||||
border-color: #3dffcd;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-select:focus::-ms-value {
|
||||
color: #fff;
|
||||
|
@ -3820,8 +3819,8 @@ input[type="button"].btn-block {
|
|||
opacity: 0;
|
||||
}
|
||||
.custom-file-input:focus ~ .custom-file-label {
|
||||
border-color: #739ac2;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
border-color: #3dffcd;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-file-input[disabled] ~ .custom-file-label,
|
||||
.custom-file-input:disabled ~ .custom-file-label {
|
||||
|
@ -3878,13 +3877,13 @@ input[type="button"].btn-block {
|
|||
outline: 0;
|
||||
}
|
||||
.custom-range:focus::-webkit-slider-thumb {
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-range:focus::-moz-range-thumb {
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-range:focus::-ms-thumb {
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
.custom-range::-moz-focus-outer {
|
||||
border: 0;
|
||||
|
@ -3893,7 +3892,7 @@ input[type="button"].btn-block {
|
|||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: -0.25rem;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
|
@ -3906,7 +3905,7 @@ input[type="button"].btn-block {
|
|||
}
|
||||
}
|
||||
.custom-range::-webkit-slider-thumb:active {
|
||||
background-color: #97b3d2;
|
||||
background-color: #70ffda;
|
||||
}
|
||||
.custom-range::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
|
@ -3920,7 +3919,7 @@ input[type="button"].btn-block {
|
|||
.custom-range::-moz-range-thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
|
@ -3933,7 +3932,7 @@ input[type="button"].btn-block {
|
|||
}
|
||||
}
|
||||
.custom-range::-moz-range-thumb:active {
|
||||
background-color: #97b3d2;
|
||||
background-color: #70ffda;
|
||||
}
|
||||
.custom-range::-moz-range-track {
|
||||
width: 100%;
|
||||
|
@ -3950,7 +3949,7 @@ input[type="button"].btn-block {
|
|||
margin-top: 0;
|
||||
margin-right: 0.2rem;
|
||||
margin-left: 0.2rem;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
|
@ -3963,7 +3962,7 @@ input[type="button"].btn-block {
|
|||
}
|
||||
}
|
||||
.custom-range::-ms-thumb:active {
|
||||
background-color: #97b3d2;
|
||||
background-color: #70ffda;
|
||||
}
|
||||
.custom-range::-ms-track {
|
||||
width: 100%;
|
||||
|
@ -4075,7 +4074,7 @@ input[type="button"].btn-block {
|
|||
.nav-pills .nav-link.active,
|
||||
.nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
|
||||
.nav-fill > .nav-link,
|
||||
|
@ -4762,7 +4761,7 @@ input[type="button"].btn-block {
|
|||
.page-link:focus {
|
||||
z-index: 3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||
}
|
||||
|
||||
.page-item:first-child .page-link {
|
||||
|
@ -4856,17 +4855,17 @@ a.badge:focus {
|
|||
|
||||
.badge-primary {
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
}
|
||||
a.badge-primary:hover,
|
||||
a.badge-primary:focus {
|
||||
color: #fff;
|
||||
background-color: #28415b;
|
||||
background-color: #008966;
|
||||
}
|
||||
a.badge-primary:focus,
|
||||
a.badge-primary.focus {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
|
@ -5021,15 +5020,15 @@ a.badge-dark.focus {
|
|||
}
|
||||
|
||||
.alert-primary {
|
||||
color: #1d2f42;
|
||||
background-color: #d7dee5;
|
||||
border-color: #c7d1db;
|
||||
color: #006249;
|
||||
background-color: #ccf2e8;
|
||||
border-color: #b8ecdf;
|
||||
}
|
||||
.alert-primary hr {
|
||||
border-top-color: #b7c4d1;
|
||||
border-top-color: #a4e7d6;
|
||||
}
|
||||
.alert-primary .alert-link {
|
||||
color: #0d161f;
|
||||
color: #002f23;
|
||||
}
|
||||
|
||||
.alert-secondary {
|
||||
|
@ -5142,7 +5141,7 @@ a.badge-dark.focus {
|
|||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
@ -5232,8 +5231,8 @@ a.badge-dark.focus {
|
|||
.list-group-item.active {
|
||||
z-index: 2;
|
||||
color: #fff;
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
background-color: #00bc8c;
|
||||
border-color: #00bc8c;
|
||||
}
|
||||
.list-group-item + .list-group-item {
|
||||
border-top-width: 0;
|
||||
|
@ -5373,18 +5372,18 @@ a.badge-dark.focus {
|
|||
}
|
||||
|
||||
.list-group-item-primary {
|
||||
color: #1d2f42;
|
||||
background-color: #c7d1db;
|
||||
color: #006249;
|
||||
background-color: #b8ecdf;
|
||||
}
|
||||
.list-group-item-primary.list-group-item-action:hover,
|
||||
.list-group-item-primary.list-group-item-action:focus {
|
||||
color: #1d2f42;
|
||||
background-color: #b7c4d1;
|
||||
color: #006249;
|
||||
background-color: #a4e7d6;
|
||||
}
|
||||
.list-group-item-primary.list-group-item-action.active {
|
||||
color: #fff;
|
||||
background-color: #1d2f42;
|
||||
border-color: #1d2f42;
|
||||
background-color: #006249;
|
||||
border-color: #006249;
|
||||
}
|
||||
|
||||
.list-group-item-secondary {
|
||||
|
@ -6290,14 +6289,14 @@ a.close.disabled {
|
|||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #375a7f !important;
|
||||
background-color: #00bc8c !important;
|
||||
}
|
||||
|
||||
a.bg-primary:hover,
|
||||
a.bg-primary:focus,
|
||||
button.bg-primary:hover,
|
||||
button.bg-primary:focus {
|
||||
background-color: #28415b !important;
|
||||
background-color: #008966 !important;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
|
@ -6426,7 +6425,7 @@ button.bg-dark:focus {
|
|||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: #375a7f !important;
|
||||
border-color: #00bc8c !important;
|
||||
}
|
||||
|
||||
.border-secondary {
|
||||
|
@ -9447,12 +9446,12 @@ button.bg-dark:focus {
|
|||
}
|
||||
|
||||
.text-primary {
|
||||
color: #375a7f !important;
|
||||
color: #00bc8c !important;
|
||||
}
|
||||
|
||||
a.text-primary:hover,
|
||||
a.text-primary:focus {
|
||||
color: #20344a !important;
|
||||
color: #007053 !important;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
116
src/server/handlers/catch-all-handler.tsx
Normal file
116
src/server/handlers/catch-all-handler.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import type { Request, Response } from "express";
|
||||
import { StaticRouter, matchPath } from "inferno-router";
|
||||
import { renderToString } from "inferno-server";
|
||||
import IsomorphicCookie from "isomorphic-cookie";
|
||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||
import { App } from "../../shared/components/app/app";
|
||||
import { getHttpBaseInternal } from "../../shared/env";
|
||||
import {
|
||||
InitialFetchRequest,
|
||||
IsoDataOptionalSite,
|
||||
RouteData,
|
||||
} from "../../shared/interfaces";
|
||||
import { routes } from "../../shared/routes";
|
||||
import {
|
||||
FailedRequestState,
|
||||
wrapClient,
|
||||
} from "../../shared/services/HttpService";
|
||||
import { ErrorPageData, initializeSite, isAuthPath } from "../../shared/utils";
|
||||
import { createSsrHtml } from "../utils/create-ssr-html";
|
||||
import { getErrorPageData } from "../utils/get-error-page-data";
|
||||
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
try {
|
||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
||||
|
||||
const getSiteForm: GetSite = { auth };
|
||||
|
||||
const headers = setForwardedHeaders(req.headers);
|
||||
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
||||
|
||||
const { path, url, query } = req;
|
||||
|
||||
// Get site data first
|
||||
// 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: RouteData = {};
|
||||
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)) {
|
||||
return res.redirect("/login");
|
||||
}
|
||||
|
||||
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 && activeRoute?.fetchInitialData) {
|
||||
const initialFetchReq: InitialFetchRequest = {
|
||||
client,
|
||||
auth,
|
||||
path,
|
||||
query,
|
||||
site,
|
||||
};
|
||||
|
||||
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
||||
}
|
||||
} else if (try_site.state === "failed") {
|
||||
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||
}
|
||||
|
||||
const error = Object.values(routeData).find(
|
||||
res => res.state === "failed"
|
||||
) as FailedRequestState | undefined;
|
||||
|
||||
// Redirect to the 404 if there's an API error
|
||||
if (error) {
|
||||
console.error(error.msg);
|
||||
if (error.msg === "instance_is_private") {
|
||||
return res.redirect(`/signup`);
|
||||
} else {
|
||||
errorPageData = getErrorPageData(new Error(error.msg), site);
|
||||
}
|
||||
}
|
||||
|
||||
const isoData: IsoDataOptionalSite = {
|
||||
path,
|
||||
site_res: site,
|
||||
routeData,
|
||||
errorPageData,
|
||||
};
|
||||
|
||||
const wrapper = (
|
||||
<StaticRouter location={url} context={isoData}>
|
||||
<App />
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
const root = renderToString(wrapper);
|
||||
|
||||
res.send(await createSsrHtml(root, isoData));
|
||||
} catch (err) {
|
||||
// If an error is caught here, the error page couldn't even be rendered
|
||||
console.error(err);
|
||||
res.statusCode = 500;
|
||||
return res.send(
|
||||
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
||||
);
|
||||
}
|
||||
};
|
18
src/server/handlers/robots-handler.ts
Normal file
18
src/server/handlers/robots-handler.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type { Response } from "express";
|
||||
|
||||
export default async ({ res }: { res: Response }) => {
|
||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||
|
||||
res.send(`User-Agent: *
|
||||
Disallow: /login
|
||||
Disallow: /settings
|
||||
Disallow: /create_community
|
||||
Disallow: /create_post
|
||||
Disallow: /create_private_message
|
||||
Disallow: /inbox
|
||||
Disallow: /setup
|
||||
Disallow: /admin
|
||||
Disallow: /password_change
|
||||
Disallow: /search/
|
||||
`);
|
||||
};
|
14
src/server/handlers/service-worker-handler.ts
Normal file
14
src/server/handlers/service-worker-handler.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import type { Response } from "express";
|
||||
import path from "path";
|
||||
|
||||
export default async ({ res }: { res: Response }) => {
|
||||
res
|
||||
.setHeader("Content-Type", "application/javascript")
|
||||
.sendFile(
|
||||
path.resolve(
|
||||
`./dist/service-worker${
|
||||
process.env.NODE_ENV === "development" ? "-development" : ""
|
||||
}.js`
|
||||
)
|
||||
);
|
||||
};
|
32
src/server/handlers/theme-handler.ts
Normal file
32
src/server/handlers/theme-handler.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type { Request, Response } from "express";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const extraThemesFolder =
|
||||
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
res.contentType("text/css");
|
||||
|
||||
const theme = req.params.name;
|
||||
|
||||
if (!theme.endsWith(".css")) {
|
||||
res.statusCode = 400;
|
||||
res.send("Theme must be a css file");
|
||||
}
|
||||
|
||||
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
|
||||
|
||||
if (existsSync(customTheme)) {
|
||||
res.sendFile(customTheme);
|
||||
} else {
|
||||
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
|
||||
|
||||
// If the theme doesn't exist, just send litely
|
||||
if (existsSync(internalTheme)) {
|
||||
res.sendFile(internalTheme);
|
||||
} else {
|
||||
res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
|
||||
}
|
||||
}
|
||||
};
|
6
src/server/handlers/themes-list-handler.ts
Normal file
6
src/server/handlers/themes-list-handler.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import type { Response } from "express";
|
||||
import { buildThemeList } from "../utils/build-themes-list";
|
||||
|
||||
export default async ({ res }: { res: Response }) => {
|
||||
res.type("json").send(JSON.stringify(await buildThemeList()));
|
||||
};
|
|
@ -1,463 +1,38 @@
|
|||
import express from "express";
|
||||
import { existsSync } from "fs";
|
||||
import { readdir, readFile } from "fs/promises";
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { Helmet } from "inferno-helmet";
|
||||
import { matchPath, StaticRouter } from "inferno-router";
|
||||
import { renderToString } from "inferno-server";
|
||||
import IsomorphicCookie from "isomorphic-cookie";
|
||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
import serialize from "serialize-javascript";
|
||||
import sharp from "sharp";
|
||||
import { App } from "../shared/components/app/app";
|
||||
import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
|
||||
import {
|
||||
ILemmyConfig,
|
||||
InitialFetchRequest,
|
||||
IsoDataOptionalSite,
|
||||
RouteData,
|
||||
} from "../shared/interfaces";
|
||||
import { routes } from "../shared/routes";
|
||||
import { FailedRequestState, wrapClient } from "../shared/services/HttpService";
|
||||
import {
|
||||
ErrorPageData,
|
||||
favIconPngUrl,
|
||||
favIconUrl,
|
||||
initializeSite,
|
||||
isAuthPath,
|
||||
} from "../shared/utils";
|
||||
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||
import RobotsHandler from "./handlers/robots-handler";
|
||||
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
||||
import ThemeHandler from "./handlers/theme-handler";
|
||||
import ThemesListHandler from "./handlers/themes-list-handler";
|
||||
import setDefaultCsp from "./middleware/set-default-csp";
|
||||
|
||||
const server = express();
|
||||
|
||||
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||
? process.env["LEMMY_UI_HOST"].split(":")
|
||||
: ["0.0.0.0", "1234"];
|
||||
const extraThemesFolder =
|
||||
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||
|
||||
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 *; media-src *`
|
||||
);
|
||||
next();
|
||||
});
|
||||
}
|
||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||
|
||||
server.use(express.json());
|
||||
server.use(express.urlencoded({ extended: false }));
|
||||
server.use("/static", express.static(path.resolve("./dist")));
|
||||
|
||||
const robotstxt = `User-Agent: *
|
||||
Disallow: /login
|
||||
Disallow: /settings
|
||||
Disallow: /create_community
|
||||
Disallow: /create_post
|
||||
Disallow: /create_private_message
|
||||
Disallow: /inbox
|
||||
Disallow: /setup
|
||||
Disallow: /admin
|
||||
Disallow: /password_change
|
||||
Disallow: /search/
|
||||
`;
|
||||
|
||||
server.get("/service-worker.js", async (_req, res) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.sendFile(
|
||||
path.resolve(
|
||||
`./dist/service-worker${
|
||||
process.env.NODE_ENV === "development" ? "-development" : ""
|
||||
}.js`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
server.get("/robots.txt", async (_req, res) => {
|
||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||
res.send(robotstxt);
|
||||
});
|
||||
|
||||
server.get("/css/themes/:name", async (req, res) => {
|
||||
res.contentType("text/css");
|
||||
const theme = req.params.name;
|
||||
if (!theme.endsWith(".css")) {
|
||||
res.statusCode = 400;
|
||||
res.send("Theme must be a css file");
|
||||
}
|
||||
|
||||
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
|
||||
if (existsSync(customTheme)) {
|
||||
res.sendFile(customTheme);
|
||||
} else {
|
||||
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
|
||||
|
||||
// If the theme doesn't exist, just send litely
|
||||
if (existsSync(internalTheme)) {
|
||||
res.sendFile(internalTheme);
|
||||
} else {
|
||||
res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function buildThemeList(): Promise<string[]> {
|
||||
const themes = ["darkly", "darkly-red", "litely", "litely-red"];
|
||||
if (existsSync(extraThemesFolder)) {
|
||||
const dirThemes = await readdir(extraThemesFolder);
|
||||
const cssThemes = dirThemes
|
||||
.filter(d => d.endsWith(".css"))
|
||||
.map(d => d.replace(".css", ""));
|
||||
themes.push(...cssThemes);
|
||||
}
|
||||
return themes;
|
||||
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||
server.use(setDefaultCsp);
|
||||
}
|
||||
|
||||
server.get("/css/themelist", async (_req, res) => {
|
||||
res.type("json");
|
||||
res.send(JSON.stringify(await buildThemeList()));
|
||||
});
|
||||
|
||||
// server.use(cookieParser());
|
||||
server.get("/*", async (req, res) => {
|
||||
try {
|
||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
||||
|
||||
const getSiteForm: GetSite = { auth };
|
||||
|
||||
const headers = setForwardedHeaders(req.headers);
|
||||
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
||||
|
||||
const { path, url, query } = req;
|
||||
|
||||
// Get site data first
|
||||
// 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: RouteData = {};
|
||||
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)) {
|
||||
return res.redirect("/login");
|
||||
}
|
||||
|
||||
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 && activeRoute?.fetchInitialData) {
|
||||
const initialFetchReq: InitialFetchRequest = {
|
||||
client,
|
||||
auth,
|
||||
path,
|
||||
query,
|
||||
site,
|
||||
};
|
||||
|
||||
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
||||
}
|
||||
} else if (try_site.state === "failed") {
|
||||
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||
}
|
||||
|
||||
const error = Object.values(routeData).find(
|
||||
res => res.state === "failed"
|
||||
) as FailedRequestState | undefined;
|
||||
|
||||
// Redirect to the 404 if there's an API error
|
||||
if (error) {
|
||||
console.error(error.msg);
|
||||
if (error.msg === "instance_is_private") {
|
||||
return res.redirect(`/signup`);
|
||||
} else {
|
||||
errorPageData = getErrorPageData(new Error(error.msg), site);
|
||||
}
|
||||
}
|
||||
|
||||
const isoData: IsoDataOptionalSite = {
|
||||
path,
|
||||
site_res: site,
|
||||
routeData,
|
||||
errorPageData,
|
||||
};
|
||||
|
||||
const wrapper = (
|
||||
<StaticRouter location={url} context={isoData}>
|
||||
<App />
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
const root = renderToString(wrapper);
|
||||
|
||||
res.send(await createSsrHtml(root, isoData));
|
||||
} catch (err) {
|
||||
// If an error is caught here, the error page couldn't even be rendered
|
||||
console.error(err);
|
||||
res.statusCode = 500;
|
||||
return res.send(
|
||||
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
||||
);
|
||||
}
|
||||
});
|
||||
server.get("/robots.txt", RobotsHandler);
|
||||
server.get("/service-worker.js", ServiceWorkerHandler);
|
||||
server.get("/css/themes/:name", ThemeHandler);
|
||||
server.get("/css/themelist", ThemesListHandler);
|
||||
server.get("/*", CatchAllHandler);
|
||||
|
||||
server.listen(Number(port), hostname, () => {
|
||||
console.log(`http://${hostname}:${port}`);
|
||||
});
|
||||
|
||||
function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
||||
[key: string]: string;
|
||||
} {
|
||||
const out: { [key: string]: string } = {};
|
||||
if (headers.host) {
|
||||
out.host = headers.host;
|
||||
}
|
||||
const realIp = headers["x-real-ip"];
|
||||
if (realIp) {
|
||||
out["x-real-ip"] = realIp as string;
|
||||
}
|
||||
const forwardedFor = headers["x-forwarded-for"];
|
||||
if (forwardedFor) {
|
||||
out["x-forwarded-for"] = forwardedFor as string;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
console.info("Interrupted");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
const iconSizes = [72, 96, 144, 192, 512];
|
||||
const defaultLogoPathDirectory = path.join(
|
||||
process.cwd(),
|
||||
"dist",
|
||||
"assets",
|
||||
"icons"
|
||||
);
|
||||
|
||||
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 = {
|
||||
name: site.name,
|
||||
description: site.description ?? "A link aggregator for the fediverse",
|
||||
start_url: url,
|
||||
scope: url,
|
||||
display: "standalone",
|
||||
id: "/",
|
||||
background_color: "#222222",
|
||||
theme_color: "#222222",
|
||||
icons: await Promise.all(
|
||||
iconSizes.map(async size => {
|
||||
let src = await readFile(
|
||||
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`)
|
||||
).then(buf => buf.toString("base64"));
|
||||
|
||||
if (icon) {
|
||||
src = await sharp(icon)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"));
|
||||
}
|
||||
|
||||
return {
|
||||
sizes: `${size}x${size}`,
|
||||
type: "image/png",
|
||||
src: `data:image/png;base64,${src}`,
|
||||
purpose: "any maskable",
|
||||
};
|
||||
})
|
||||
),
|
||||
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)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.arrayBuffer());
|
||||
}
|
||||
|
||||
function getErrorPageData(error: Error, site?: GetSiteResponse) {
|
||||
const errorPageData: ErrorPageData = {};
|
||||
|
||||
if (site) {
|
||||
errorPageData.error = error.message;
|
||||
}
|
||||
|
||||
const adminMatrixIds = site?.admins
|
||||
.map(({ person: { matrix_user_id } }) => matrix_user_id)
|
||||
.filter(id => id) as string[] | undefined;
|
||||
if (adminMatrixIds && adminMatrixIds.length > 0) {
|
||||
errorPageData.adminMatrixIds = adminMatrixIds;
|
||||
}
|
||||
|
||||
return errorPageData;
|
||||
}
|
||||
|
||||
async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
|
||||
const site = isoData.site_res;
|
||||
const appleTouchIcon = site?.site_view.site.icon
|
||||
? `data:image/png;base64,${sharp(
|
||||
await fetchIconPng(site.site_view.site.icon)
|
||||
)
|
||||
.resize(180, 180)
|
||||
.extend({
|
||||
bottom: 20,
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
background: "#222222",
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"))}`
|
||||
: favIconPngUrl;
|
||||
|
||||
const erudaStr =
|
||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||
? renderToString(
|
||||
<>
|
||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||
<script>eruda.init();</script>
|
||||
</>
|
||||
)
|
||||
: "";
|
||||
|
||||
const helmet = Helmet.renderStatic();
|
||||
|
||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html ${helmet.htmlAttributes.toString()}>
|
||||
<head>
|
||||
<script>window.isoData = ${serialize(isoData)}</script>
|
||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
||||
|
||||
<!-- A remote debugging utility for mobile -->
|
||||
${erudaStr}
|
||||
|
||||
<!-- Custom injected script -->
|
||||
${customHtmlHeader}
|
||||
|
||||
${helmet.title.toString()}
|
||||
${helmet.meta.toString()}
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta name="Description" content="Lemmy">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
||||
<link
|
||||
id="favicon"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
href=${site?.site_view.site.icon ?? favIconUrl}
|
||||
/>
|
||||
|
||||
<!-- Web app manifest -->
|
||||
${
|
||||
site &&
|
||||
`<link
|
||||
rel="manifest"
|
||||
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
||||
site
|
||||
)}`}
|
||||
/>`
|
||||
}
|
||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
||||
|
||||
<!-- Current theme and more -->
|
||||
${helmet.link.toString()}
|
||||
|
||||
</head>
|
||||
|
||||
<body ${helmet.bodyAttributes.toString()}>
|
||||
<noscript>
|
||||
<div class="alert alert-danger rounded-0" role="alert">
|
||||
<b>Javascript is disabled. Actions will not work.</b>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id='root'>${root}</div>
|
||||
<script defer src='/static/js/client.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
|
10
src/server/middleware/set-default-csp.ts
Normal file
10
src/server/middleware/set-default-csp.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type { NextFunction, Response } from "express";
|
||||
|
||||
export default function ({ res, next }: { res: Response; next: NextFunction }) {
|
||||
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 *; media-src *`
|
||||
);
|
||||
|
||||
next();
|
||||
}
|
18
src/server/utils/build-themes-list.ts
Normal file
18
src/server/utils/build-themes-list.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { existsSync } from "fs";
|
||||
import { readdir } from "fs/promises";
|
||||
|
||||
const extraThemesFolder =
|
||||
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||
|
||||
const themes = ["darkly", "darkly-red", "litely", "litely-red"];
|
||||
|
||||
export async function buildThemeList(): Promise<string[]> {
|
||||
if (existsSync(extraThemesFolder)) {
|
||||
const dirThemes = await readdir(extraThemesFolder);
|
||||
const cssThemes = dirThemes
|
||||
.filter(d => d.endsWith(".css"))
|
||||
.map(d => d.replace(".css", ""));
|
||||
themes.push(...cssThemes);
|
||||
}
|
||||
return themes;
|
||||
}
|
109
src/server/utils/create-ssr-html.tsx
Normal file
109
src/server/utils/create-ssr-html.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { Helmet } from "inferno-helmet";
|
||||
import { renderToString } from "inferno-server";
|
||||
import serialize from "serialize-javascript";
|
||||
import sharp from "sharp";
|
||||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
||||
import { favIconPngUrl, favIconUrl } from "../../shared/utils";
|
||||
import { fetchIconPng } from "./fetch-icon-png";
|
||||
import { generateManifestBase64 } from "./generate-manifest-base64";
|
||||
|
||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||
|
||||
export async function createSsrHtml(
|
||||
root: string,
|
||||
isoData: IsoDataOptionalSite
|
||||
) {
|
||||
const site = isoData.site_res;
|
||||
|
||||
const appleTouchIcon = site?.site_view.site.icon
|
||||
? `data:image/png;base64,${sharp(
|
||||
await fetchIconPng(site.site_view.site.icon)
|
||||
)
|
||||
.resize(180, 180)
|
||||
.extend({
|
||||
bottom: 20,
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
background: "#222222",
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"))}`
|
||||
: favIconPngUrl;
|
||||
|
||||
const erudaStr =
|
||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||
? renderToString(
|
||||
<>
|
||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||
<script>eruda.init();</script>
|
||||
</>
|
||||
)
|
||||
: "";
|
||||
|
||||
const helmet = Helmet.renderStatic();
|
||||
|
||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html ${helmet.htmlAttributes.toString()}>
|
||||
<head>
|
||||
<script>window.isoData = ${serialize(isoData)}</script>
|
||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
||||
|
||||
<!-- A remote debugging utility for mobile -->
|
||||
${erudaStr}
|
||||
|
||||
<!-- Custom injected script -->
|
||||
${customHtmlHeader}
|
||||
|
||||
${helmet.title.toString()}
|
||||
${helmet.meta.toString()}
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta name="Description" content="Lemmy">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
||||
<link
|
||||
id="favicon"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
href=${site?.site_view.site.icon ?? favIconUrl}
|
||||
/>
|
||||
|
||||
<!-- Web app manifest -->
|
||||
${
|
||||
site &&
|
||||
`<link
|
||||
rel="manifest"
|
||||
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
||||
site
|
||||
)}`}
|
||||
/>`
|
||||
}
|
||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
||||
|
||||
<!-- Current theme and more -->
|
||||
${helmet.link.toString()}
|
||||
|
||||
</head>
|
||||
|
||||
<body ${helmet.bodyAttributes.toString()}>
|
||||
<noscript>
|
||||
<div class="alert alert-danger rounded-0" role="alert">
|
||||
<b>Javascript is disabled. Actions will not work.</b>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id='root'>${root}</div>
|
||||
<script defer src='/static/js/client.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
5
src/server/utils/fetch-icon-png.ts
Normal file
5
src/server/utils/fetch-icon-png.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export async function fetchIconPng(iconUrl: string) {
|
||||
return await fetch(iconUrl)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.arrayBuffer());
|
||||
}
|
107
src/server/utils/generate-manifest-base64.ts
Normal file
107
src/server/utils/generate-manifest-base64.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { readFile } from "fs/promises";
|
||||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import { getHttpBaseExternal } from "../../shared/env";
|
||||
import { fetchIconPng } from "./fetch-icon-png";
|
||||
|
||||
const iconSizes = [72, 96, 144, 192, 512];
|
||||
|
||||
const defaultLogoPathDirectory = path.join(
|
||||
process.cwd(),
|
||||
"dist",
|
||||
"assets",
|
||||
"icons"
|
||||
);
|
||||
|
||||
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 = {
|
||||
name: site.name,
|
||||
description: site.description ?? "A link aggregator for the fediverse",
|
||||
start_url: url,
|
||||
scope: url,
|
||||
display: "standalone",
|
||||
id: "/",
|
||||
background_color: "#222222",
|
||||
theme_color: "#222222",
|
||||
icons: await Promise.all(
|
||||
iconSizes.map(async size => {
|
||||
let src = await readFile(
|
||||
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`)
|
||||
).then(buf => buf.toString("base64"));
|
||||
|
||||
if (icon) {
|
||||
src = await sharp(icon)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"));
|
||||
}
|
||||
|
||||
return {
|
||||
sizes: `${size}x${size}`,
|
||||
type: "image/png",
|
||||
src: `data:image/png;base64,${src}`,
|
||||
purpose: "any maskable",
|
||||
};
|
||||
})
|
||||
),
|
||||
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");
|
||||
}
|
20
src/server/utils/get-error-page-data.ts
Normal file
20
src/server/utils/get-error-page-data.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
import { ErrorPageData } from "../../shared/utils";
|
||||
|
||||
export function getErrorPageData(error: Error, site?: GetSiteResponse) {
|
||||
const errorPageData: ErrorPageData = {};
|
||||
|
||||
if (site) {
|
||||
errorPageData.error = error.message;
|
||||
}
|
||||
|
||||
const adminMatrixIds = site?.admins
|
||||
.map(({ person: { matrix_user_id } }) => matrix_user_id)
|
||||
.filter(id => id) as string[] | undefined;
|
||||
|
||||
if (adminMatrixIds && adminMatrixIds.length > 0) {
|
||||
errorPageData.adminMatrixIds = adminMatrixIds;
|
||||
}
|
||||
|
||||
return errorPageData;
|
||||
}
|
25
src/server/utils/set-forwarded-headers.ts
Normal file
25
src/server/utils/set-forwarded-headers.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { IncomingHttpHeaders } from "http";
|
||||
|
||||
export function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
||||
[key: string]: string;
|
||||
} {
|
||||
const out: { [key: string]: string } = {};
|
||||
|
||||
if (headers.host) {
|
||||
out.host = headers.host;
|
||||
}
|
||||
|
||||
const realIp = headers["x-real-ip"];
|
||||
|
||||
if (realIp) {
|
||||
out["x-real-ip"] = realIp as string;
|
||||
}
|
||||
|
||||
const forwardedFor = headers["x-forwarded-for"];
|
||||
|
||||
if (forwardedFor) {
|
||||
out["x-forwarded-for"] = forwardedFor as string;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
|
@ -15,17 +15,19 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
|
|||
const banner = this.props.banner;
|
||||
const icon = this.props.icon;
|
||||
return (
|
||||
<div className="position-relative mb-2">
|
||||
{banner && <PictrsImage src={banner} banner alt="" />}
|
||||
{icon && (
|
||||
<PictrsImage
|
||||
src={icon}
|
||||
iconOverlay
|
||||
pushup={!!this.props.banner}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
(banner || icon) && (
|
||||
<div className="position-relative mb-2">
|
||||
{banner && <PictrsImage src={banner} banner alt="" />}
|
||||
{icon && (
|
||||
<PictrsImage
|
||||
src={icon}
|
||||
iconOverlay
|
||||
pushup={!!this.props.banner}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -443,16 +443,17 @@ export class Home extends Component<any, HomeState> {
|
|||
admins={admins}
|
||||
counts={counts}
|
||||
showLocal={showLocal(this.isoData)}
|
||||
isMobile={true}
|
||||
/>
|
||||
)}
|
||||
{showTrendingMobile && (
|
||||
<div className="col-12 card border-secondary mb-3">
|
||||
<div className="card-body">{this.trendingCommunities(true)}</div>
|
||||
<div className="card border-secondary mb-3">
|
||||
{this.trendingCommunities()}
|
||||
</div>
|
||||
)}
|
||||
{showSubscribedMobile && (
|
||||
<div className="col-12 card border-secondary mb-3">
|
||||
<div className="card-body">{this.subscribedCommunities}</div>
|
||||
<div className="card border-secondary mb-3">
|
||||
{this.subscribedCommunities(true)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -471,19 +472,7 @@ export class Home extends Component<any, HomeState> {
|
|||
return (
|
||||
<div id="sidebarContainer">
|
||||
<section id="sidebarMain" className="card border-secondary mb-3">
|
||||
<div className="card-body">
|
||||
{this.trendingCommunities()}
|
||||
{canCreateCommunity(this.state.siteRes) && (
|
||||
<LinkButton
|
||||
path="/create_community"
|
||||
translationKey="create_a_community"
|
||||
/>
|
||||
)}
|
||||
<LinkButton
|
||||
path="/communities"
|
||||
translationKey="explore_communities"
|
||||
/>
|
||||
</div>
|
||||
{this.trendingCommunities()}
|
||||
</section>
|
||||
<SiteSidebar
|
||||
site={site}
|
||||
|
@ -492,18 +481,20 @@ export class Home extends Component<any, HomeState> {
|
|||
showLocal={showLocal(this.isoData)}
|
||||
/>
|
||||
{this.hasFollows && (
|
||||
<section
|
||||
id="sidebarSubscribed"
|
||||
className="card border-secondary mb-3"
|
||||
>
|
||||
<div className="card-body">{this.subscribedCommunities}</div>
|
||||
</section>
|
||||
<div className="accordion">
|
||||
<section
|
||||
id="sidebarSubscribed"
|
||||
className="card border-secondary mb-3"
|
||||
>
|
||||
{this.subscribedCommunities(false)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
trendingCommunities(isMobile = false) {
|
||||
trendingCommunities() {
|
||||
switch (this.state.trendingCommunitiesRes?.state) {
|
||||
case "loading":
|
||||
return (
|
||||
|
@ -514,68 +505,103 @@ export class Home extends Component<any, HomeState> {
|
|||
case "success": {
|
||||
const trending = this.state.trendingCommunitiesRes.data.communities;
|
||||
return (
|
||||
<div className={!isMobile ? "mb-2" : ""}>
|
||||
<h5>
|
||||
<T i18nKey="trending_communities">
|
||||
#
|
||||
<Link className="text-body" to="/communities">
|
||||
<>
|
||||
<header className="card-header d-flex align-items-center">
|
||||
<h5 className="mb-0">
|
||||
<T i18nKey="trending_communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
</h5>
|
||||
<ul className="list-inline mb-0">
|
||||
{trending.map(cv => (
|
||||
<li
|
||||
key={cv.community.id}
|
||||
className="list-inline-item d-inline-block"
|
||||
>
|
||||
<CommunityLink community={cv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Link className="text-body" to="/communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
</h5>
|
||||
</header>
|
||||
<div className="card-body">
|
||||
{trending.length > 0 && (
|
||||
<ul className="list-inline">
|
||||
{trending.map(cv => (
|
||||
<li key={cv.community.id} className="list-inline-item">
|
||||
<CommunityLink community={cv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{canCreateCommunity(this.state.siteRes) && (
|
||||
<LinkButton
|
||||
path="/create_community"
|
||||
translationKey="create_a_community"
|
||||
/>
|
||||
)}
|
||||
<LinkButton
|
||||
path="/communities"
|
||||
translationKey="explore_communities"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get subscribedCommunities() {
|
||||
subscribedCommunities(isMobile = false) {
|
||||
const { subscribedCollapsed } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>
|
||||
<T class="d-inline" i18nKey="subscribed_to_communities">
|
||||
#
|
||||
<Link className="text-body" to="/communities">
|
||||
<>
|
||||
<header
|
||||
className="card-header d-flex align-items-center"
|
||||
id="sidebarSubscribedHeader"
|
||||
>
|
||||
<h5 className="mb-0 d-inline">
|
||||
<T class="d-inline" i18nKey="subscribed_to_communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
onClick={linkEvent(this, this.handleCollapseSubscribe)}
|
||||
aria-label={i18n.t("collapse")}
|
||||
data-tippy-content={i18n.t("collapse")}
|
||||
>
|
||||
<Icon
|
||||
icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
|
||||
classes="icon-inline"
|
||||
/>
|
||||
</button>
|
||||
</h5>
|
||||
{!subscribedCollapsed && (
|
||||
<ul className="list-inline mb-0">
|
||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||
<li
|
||||
key={cfv.community.id}
|
||||
className="list-inline-item d-inline-block"
|
||||
>
|
||||
<CommunityLink community={cfv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<Link className="text-body" to="/communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
</h5>
|
||||
{!isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm text-muted"
|
||||
onClick={linkEvent(this, this.handleCollapseSubscribe)}
|
||||
aria-label={
|
||||
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||
}
|
||||
data-tippy-content={
|
||||
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||
}
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarSubscribedBody"
|
||||
aria-expanded="true"
|
||||
aria-controls="sidebarSubscribedBody"
|
||||
>
|
||||
<Icon
|
||||
icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
|
||||
classes="icon-inline"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
id="sidebarSubscribedBody"
|
||||
className="collapse show"
|
||||
aria-labelledby="sidebarSubscribedHeader"
|
||||
>
|
||||
<div className="card-body">
|
||||
<ul className="list-inline mb-0">
|
||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||
<li
|
||||
key={cfv.community.id}
|
||||
className="list-inline-item d-inline-block"
|
||||
>
|
||||
<CommunityLink community={cfv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -298,27 +298,22 @@ export class Signup extends Component<any, State> {
|
|||
</>
|
||||
)}
|
||||
{this.renderCaptcha()}
|
||||
{siteView.local_site.enable_nsfw && (
|
||||
<div className="form-group row">
|
||||
<div className="col-sm-10">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
id="register-show-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.form.show_nsfw}
|
||||
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="register-show-nsfw"
|
||||
>
|
||||
{i18n.t("show_nsfw")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group row">
|
||||
<div className="col-sm-10">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
id="register-show-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.form.show_nsfw}
|
||||
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="register-show-nsfw">
|
||||
{i18n.t("show_nsfw")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
autoComplete="false"
|
||||
|
|
|
@ -12,6 +12,7 @@ interface SiteSidebarProps {
|
|||
showLocal: boolean;
|
||||
counts?: SiteAggregates;
|
||||
admins?: PersonView[];
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
interface SiteSidebarState {
|
||||
|
@ -29,39 +30,58 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<section id="sidebarInfo" className="card border-secondary mb-3">
|
||||
<div className="card-body">
|
||||
<div>
|
||||
<div className="mb-2">{this.siteName()}</div>
|
||||
<div className="accordion">
|
||||
<section id="sidebarInfo" className="card border-secondary mb-3">
|
||||
<header
|
||||
className="card-header d-flex align-items-center"
|
||||
id="sidebarInfoHeader"
|
||||
>
|
||||
{this.siteName()}
|
||||
{!this.state.collapsed && (
|
||||
<>
|
||||
<BannerIconHeader banner={this.props.site.banner} />
|
||||
{this.siteInfo()}
|
||||
</>
|
||||
<BannerIconHeader banner={this.props.site.banner} />
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div
|
||||
id="sidebarInfoBody"
|
||||
className="collapse show"
|
||||
aria-labelledby="sidebarInfoHeader"
|
||||
>
|
||||
<div className="card-body">{this.siteInfo()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
siteName() {
|
||||
return (
|
||||
<h5 className="mb-0 d-inline">
|
||||
{this.props.site.name}
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
onClick={linkEvent(this, this.handleCollapseSidebar)}
|
||||
aria-label={i18n.t("collapse")}
|
||||
data-tippy-content={i18n.t("collapse")}
|
||||
>
|
||||
{this.state.collapsed ? (
|
||||
<Icon icon="plus-square" classes="icon-inline" />
|
||||
) : (
|
||||
<Icon icon="minus-square" classes="icon-inline" />
|
||||
)}
|
||||
</button>
|
||||
</h5>
|
||||
<>
|
||||
<h5 className="mb-0 d-inline">{this.props.site.name}</h5>
|
||||
{!this.props.isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={linkEvent(this, this.handleCollapseSidebar)}
|
||||
aria-label={
|
||||
this.state.collapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||
}
|
||||
data-tippy-content={
|
||||
this.state.collapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||
}
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarInfoBody"
|
||||
aria-expanded="true"
|
||||
aria-controls="sidebarInfoBody"
|
||||
>
|
||||
{this.state.collapsed ? (
|
||||
<Icon icon="plus-square" classes="icon-inline" />
|
||||
) : (
|
||||
<Icon icon="minus-square" classes="icon-inline" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from "classnames";
|
||||
import { Component } from "inferno";
|
||||
import { Link } from "inferno-router";
|
||||
import { Person } from "lemmy-js-client";
|
||||
|
@ -48,7 +49,10 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
|||
{!this.props.realLink ? (
|
||||
<Link
|
||||
title={apubName}
|
||||
className={this.props.muted ? "text-muted" : "text-info"}
|
||||
className={classNames("d-inline-flex align-items-baseline", {
|
||||
"text-muted": this.props.muted,
|
||||
"text-info": !this.props.muted,
|
||||
})}
|
||||
to={link}
|
||||
>
|
||||
{this.avatarAndName(displayName)}
|
||||
|
@ -56,7 +60,9 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
|||
) : (
|
||||
<a
|
||||
title={apubName}
|
||||
className={this.props.muted ? "text-muted" : "text-info"}
|
||||
className={`d-inline-flex align-items-baseline ${
|
||||
this.props.muted ? "text-muted" : "text-info"
|
||||
}`}
|
||||
href={link}
|
||||
rel={relTags}
|
||||
>
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
communityToChoice,
|
||||
elementUrl,
|
||||
emDash,
|
||||
enableNsfw,
|
||||
fetchCommunities,
|
||||
fetchThemeList,
|
||||
fetchUsers,
|
||||
|
@ -642,22 +641,20 @@ export class Settings extends Component<any, SettingsState> {
|
|||
/>
|
||||
</div>
|
||||
</form>
|
||||
{enableNsfw(this.state.siteRes) && (
|
||||
<div className="form-group">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
id="user-show-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.saveUserSettingsForm.show_nsfw}
|
||||
onChange={linkEvent(this, this.handleShowNsfwChange)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="user-show-nsfw">
|
||||
{i18n.t("show_nsfw")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
id="user-show-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.saveUserSettingsForm.show_nsfw}
|
||||
onChange={linkEvent(this, this.handleShowNsfwChange)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="user-show-nsfw">
|
||||
{i18n.t("show_nsfw")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="form-check">
|
||||
<input
|
||||
|
|
|
@ -201,7 +201,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const post = this.postView.post;
|
||||
|
||||
return (
|
||||
<div className="post-listing">
|
||||
<div className="post-listing mt-2">
|
||||
{!this.state.showEdit ? (
|
||||
<>
|
||||
{this.listing()}
|
||||
|
@ -386,10 +386,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</span>
|
||||
)}
|
||||
{this.props.showCommunity && (
|
||||
<span>
|
||||
<span className="mx-1"> {i18n.t("to")} </span>
|
||||
<CommunityLink community={post_view.community} />
|
||||
</span>
|
||||
<>
|
||||
{" "}
|
||||
{i18n.t("to")} <CommunityLink community={post_view.community} />
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
{post_view.post.language_id !== 0 && (
|
||||
|
@ -497,7 +497,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const post = this.postView.post;
|
||||
return (
|
||||
<Link
|
||||
className={`d-inline-block ${
|
||||
className={`d-inline ${
|
||||
!post.featured_community && !post.featured_local
|
||||
? "text-body"
|
||||
: "text-primary"
|
||||
|
@ -505,8 +505,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
to={`/post/${post.id}`}
|
||||
title={i18n.t("comments")}
|
||||
>
|
||||
<div
|
||||
className="d-inline-block"
|
||||
<span
|
||||
className="d-inline"
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||
/>
|
||||
</Link>
|
||||
|
@ -519,88 +519,78 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
return (
|
||||
<div className="post-title overflow-hidden">
|
||||
<h5>
|
||||
{url ? (
|
||||
this.props.showBody ? (
|
||||
<a
|
||||
className={`d-inline-block ${
|
||||
!post.featured_community && !post.featured_local
|
||||
? "text-body"
|
||||
: "text-primary"
|
||||
}`}
|
||||
href={url}
|
||||
title={url}
|
||||
rel={relTags}
|
||||
>
|
||||
<div
|
||||
className="d-inline-block"
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
this.postLink
|
||||
)
|
||||
<h5 className="d-inline">
|
||||
{url && this.props.showBody ? (
|
||||
<a
|
||||
className={
|
||||
!post.featured_community && !post.featured_local
|
||||
? "text-body"
|
||||
: "text-primary"
|
||||
}
|
||||
href={url}
|
||||
title={url}
|
||||
rel={relTags}
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||
></a>
|
||||
) : (
|
||||
this.postLink
|
||||
)}
|
||||
{(url && isImage(url)) ||
|
||||
(post.thumbnail_url && (
|
||||
<button
|
||||
className="btn btn-link text-monospace text-muted small d-inline-block"
|
||||
data-tippy-content={i18n.t("expand_here")}
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
!this.state.imageExpanded ? "plus-square" : "minus-square"
|
||||
}
|
||||
classes="icon-inline"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{post.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t("removed")}
|
||||
</small>
|
||||
)}
|
||||
{post.deleted && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("deleted")}
|
||||
>
|
||||
<Icon icon="trash" classes="icon-inline text-danger" />
|
||||
</small>
|
||||
)}
|
||||
{post.locked && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("locked")}
|
||||
>
|
||||
<Icon icon="lock" classes="icon-inline text-danger" />
|
||||
</small>
|
||||
)}
|
||||
{post.featured_community && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("featured")}
|
||||
>
|
||||
<Icon icon="pin" classes="icon-inline text-primary" />
|
||||
</small>
|
||||
)}
|
||||
{post.featured_local && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("featured")}
|
||||
>
|
||||
<Icon icon="pin" classes="icon-inline text-secondary" />
|
||||
</small>
|
||||
)}
|
||||
{post.nsfw && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t("nsfw")}
|
||||
</small>
|
||||
)}
|
||||
</h5>
|
||||
{(url && isImage(url)) ||
|
||||
(post.thumbnail_url && (
|
||||
<button
|
||||
className="btn btn-link text-monospace text-muted small d-inline-block"
|
||||
data-tippy-content={i18n.t("expand_here")}
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
!this.state.imageExpanded ? "plus-square" : "minus-square"
|
||||
}
|
||||
classes="icon-inline"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{post.removed && (
|
||||
<small className="ml-2 badge text-bg-secondary">
|
||||
{i18n.t("removed")}
|
||||
</small>
|
||||
)}
|
||||
{post.deleted && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("deleted")}
|
||||
>
|
||||
<Icon icon="trash" classes="icon-inline text-danger" />
|
||||
</small>
|
||||
)}
|
||||
{post.locked && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("locked")}
|
||||
>
|
||||
<Icon icon="lock" classes="icon-inline text-danger" />
|
||||
</small>
|
||||
)}
|
||||
{post.featured_community && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("featured")}
|
||||
>
|
||||
<Icon icon="pin" classes="icon-inline text-primary" />
|
||||
</small>
|
||||
)}
|
||||
{post.featured_local && (
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t("featured")}
|
||||
>
|
||||
<Icon icon="pin" classes="icon-inline text-secondary" />
|
||||
</small>
|
||||
)}
|
||||
{post.nsfw && (
|
||||
<small className="ml-2 badge text-bg-danger">{i18n.t("nsfw")}</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -631,11 +621,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const post = this.postView.post;
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
|
||||
<div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold">
|
||||
{this.commentsButton}
|
||||
{canShare() && (
|
||||
<button
|
||||
className="btn btn-link"
|
||||
className="btn btn-sm btn-link"
|
||||
onClick={linkEvent(this, this.handleShare)}
|
||||
type="button"
|
||||
>
|
||||
|
@ -654,12 +644,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{mobile && !this.props.viewOnly && this.mobileVotes}
|
||||
{UserService.Instance.myUserInfo &&
|
||||
!this.props.viewOnly &&
|
||||
this.postActions(mobile)}
|
||||
this.postActions()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
postActions(mobile = false) {
|
||||
get hasAdvancedButtons() {
|
||||
return (
|
||||
this.myPost ||
|
||||
(this.showBody && this.postView.post.body) ||
|
||||
amMod(this.props.moderators) ||
|
||||
amAdmin() ||
|
||||
this.canMod_ ||
|
||||
this.canAdmin_
|
||||
);
|
||||
}
|
||||
|
||||
postActions() {
|
||||
// Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
|
||||
// Possible enhancement: Make each button a component.
|
||||
const post_view = this.postView;
|
||||
|
@ -667,37 +668,53 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<>
|
||||
{this.saveButton}
|
||||
{this.crossPostButton}
|
||||
{mobile && this.showMoreButton}
|
||||
{(!mobile || this.state.showAdvanced) && (
|
||||
<>
|
||||
{!this.myPost && (
|
||||
<>
|
||||
{this.reportButton}
|
||||
{this.blockButton}
|
||||
</>
|
||||
)}
|
||||
{this.myPost && (this.showBody || this.state.showAdvanced) && (
|
||||
<>
|
||||
{this.editButton}
|
||||
{this.deleteButton}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
{this.showBody && post_view.post.body && this.viewSourceButton}
|
||||
|
||||
{this.hasAdvancedButtons && (
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0 dropdown-toggle"
|
||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||
data-tippy-content={i18n.t("more")}
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-controls="advancedButtonsDropdown"
|
||||
aria-label={i18n.t("more")}
|
||||
>
|
||||
<Icon icon="more-vertical" inline />
|
||||
</button>
|
||||
|
||||
<ul className="dropdown-menu" id="advancedButtonsDropdown">
|
||||
{!this.myPost ? (
|
||||
<>
|
||||
<li>{this.reportButton}</li>
|
||||
<li>{this.blockButton}</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>{this.editButton}</li>
|
||||
<li>{this.deleteButton}</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Any mod can do these, not limited to hierarchy*/}
|
||||
{(amMod(this.props.moderators) || amAdmin()) && (
|
||||
<>
|
||||
<li>
|
||||
<hr className="dropdown-divider" />
|
||||
</li>
|
||||
<li>{this.lockButton}</li>
|
||||
{this.featureButtons}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(this.canMod_ || this.canAdmin_) && (
|
||||
<li>{this.modRemoveButton}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{this.state.showAdvanced && (
|
||||
<>
|
||||
{this.showBody && post_view.post.body && this.viewSourceButton}
|
||||
{/* Any mod can do these, not limited to hierarchy*/}
|
||||
{(amMod(this.props.moderators) || amAdmin()) && (
|
||||
<>
|
||||
{this.lockButton}
|
||||
{this.featureButton}
|
||||
</>
|
||||
)}
|
||||
{(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
|
||||
</>
|
||||
)}
|
||||
{!mobile && this.showMoreButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -706,7 +723,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const post_view = this.postView;
|
||||
return (
|
||||
<Link
|
||||
className="btn btn-link text-muted py-0 pl-0 text-muted"
|
||||
className="btn btn-link text-muted pl-0 text-muted"
|
||||
title={i18n.t("number_of_comments", {
|
||||
count: Number(post_view.counts.comments),
|
||||
formattedCount: Number(post_view.counts.comments),
|
||||
|
@ -846,12 +863,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
get reportButton() {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleShowReportDialog)}
|
||||
data-tippy-content={i18n.t("show_report_dialog")}
|
||||
aria-label={i18n.t("show_report_dialog")}
|
||||
>
|
||||
<Icon icon="flag" inline />
|
||||
<Icon classes="mr-1" icon="flag" inline />
|
||||
{i18n.t("create_report")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -859,12 +876,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
get blockButton() {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleBlockPersonClick)}
|
||||
data-tippy-content={i18n.t("block_user")}
|
||||
aria-label={i18n.t("block_user")}
|
||||
>
|
||||
{this.state.blockLoading ? <Spinner /> : <Icon icon="slash" inline />}
|
||||
{this.state.blockLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon classes="mr-1" icon="slash" inline />
|
||||
)}
|
||||
{i18n.t("block_user")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -872,12 +893,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
get editButton() {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t("edit")}
|
||||
aria-label={i18n.t("edit")}
|
||||
>
|
||||
<Icon icon="edit" inline />
|
||||
<Icon classes="mr-1" icon="edit" inline />
|
||||
{i18n.t("edit")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -887,37 +908,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const label = !deleted ? i18n.t("delete") : i18n.t("restore");
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
data-tippy-content={label}
|
||||
aria-label={label}
|
||||
>
|
||||
{this.state.deleteLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon="trash"
|
||||
classes={classNames({ "text-danger": deleted })}
|
||||
inline
|
||||
/>
|
||||
<>
|
||||
<Icon
|
||||
icon="trash"
|
||||
classes={classNames("mr-1", { "text-danger": deleted })}
|
||||
inline
|
||||
/>
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
get showMoreButton() {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||
data-tippy-content={i18n.t("more")}
|
||||
aria-label={i18n.t("more")}
|
||||
>
|
||||
<Icon icon="more-vertical" inline />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
get viewSourceButton() {
|
||||
return (
|
||||
<button
|
||||
|
@ -940,25 +950,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const label = locked ? i18n.t("unlock") : i18n.t("lock");
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleModLock)}
|
||||
data-tippy-content={label}
|
||||
aria-label={label}
|
||||
>
|
||||
{this.state.lockLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon="lock"
|
||||
classes={classNames({ "text-danger": locked })}
|
||||
inline
|
||||
/>
|
||||
<>
|
||||
<Icon
|
||||
icon="lock"
|
||||
classes={classNames("mr-1", { "text-danger": locked })}
|
||||
inline
|
||||
/>
|
||||
{label}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
get featureButton() {
|
||||
get featureButtons() {
|
||||
const featuredCommunity = this.postView.post.featured_community;
|
||||
const labelCommunity = featuredCommunity
|
||||
? i18n.t("unfeature_from_community")
|
||||
|
@ -969,48 +981,56 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
? i18n.t("unfeature_from_local")
|
||||
: i18n.t("feature_in_local");
|
||||
return (
|
||||
<span>
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0 pl-0"
|
||||
onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
|
||||
data-tippy-content={labelCommunity}
|
||||
aria-label={labelCommunity}
|
||||
>
|
||||
{this.state.featureCommunityLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<span>
|
||||
<Icon
|
||||
icon="pin"
|
||||
classes={classNames({ "text-success": featuredCommunity })}
|
||||
inline
|
||||
/>
|
||||
{i18n.t("community")}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{amAdmin() && (
|
||||
<>
|
||||
<li>
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
onClick={linkEvent(this, this.handleModFeaturePostLocal)}
|
||||
data-tippy-content={labelLocal}
|
||||
aria-label={labelLocal}
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
|
||||
data-tippy-content={labelCommunity}
|
||||
aria-label={labelCommunity}
|
||||
>
|
||||
{this.state.featureLocalLoading ? (
|
||||
{this.state.featureCommunityLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<span>
|
||||
<>
|
||||
<Icon
|
||||
icon="pin"
|
||||
classes={classNames({ "text-success": featuredLocal })}
|
||||
classes={classNames("mr-1", {
|
||||
"text-success": featuredCommunity,
|
||||
})}
|
||||
inline
|
||||
/>
|
||||
{i18n.t("local")}
|
||||
</span>
|
||||
{i18n.t("community")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{amAdmin() && (
|
||||
<button
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(this, this.handleModFeaturePostLocal)}
|
||||
data-tippy-content={labelLocal}
|
||||
aria-label={labelLocal}
|
||||
>
|
||||
{this.state.featureLocalLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Icon
|
||||
icon="pin"
|
||||
classes={classNames("mr-1", {
|
||||
"text-success": featuredLocal,
|
||||
})}
|
||||
inline
|
||||
/>
|
||||
{i18n.t("local")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1018,7 +1038,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const removed = this.postView.post.removed;
|
||||
return (
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
!removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
|
||||
|
|
|
@ -96,9 +96,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
|||
onAddAdmin={this.props.onAddAdmin}
|
||||
onTransferCommunity={this.props.onTransferCommunity}
|
||||
/>
|
||||
{idx + 1 !== this.posts.length && (
|
||||
<hr className="my-3 border border-primary" />
|
||||
)}
|
||||
{idx + 1 !== this.posts.length && <hr className="my-3" />}
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -1202,6 +1202,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.20.7":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
|
||||
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.21.9":
|
||||
version "7.21.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb"
|
||||
|
@ -2159,6 +2166,13 @@ argparse@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-query@^5.1.3:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.2.1.tgz#bc285d9d654d1df121bcd0c134880d415ca67c15"
|
||||
integrity sha512-7uFg4b+lETFgdaJyETnILsXgnnzVnkHcgRbwbPwevm5x/LmUlt3MjczMRe1zg824iBgXZNRPTBftNYyRSKLp2g==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
array-buffer-byte-length@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead"
|
||||
|
@ -2177,7 +2191,7 @@ array-flatten@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
|
||||
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
|
||||
|
||||
array-includes@^3.1.5:
|
||||
array-includes@^3.1.5, array-includes@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
|
||||
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
|
||||
|
@ -2205,6 +2219,16 @@ array-uniq@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
||||
integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==
|
||||
|
||||
array.prototype.flatmap@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
|
||||
integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
define-properties "^1.1.4"
|
||||
es-abstract "^1.20.4"
|
||||
es-shim-unscopables "^1.0.0"
|
||||
|
||||
array.prototype.reduce@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac"
|
||||
|
@ -2244,6 +2268,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
|
||||
|
||||
ast-types-flow@^0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
|
||||
integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==
|
||||
|
||||
astral-regex@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
|
@ -2284,6 +2313,18 @@ aws4@^1.8.0:
|
|||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
||||
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
||||
|
||||
axe-core@^4.6.2:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0"
|
||||
integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==
|
||||
|
||||
axobject-query@^3.1.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
|
||||
integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
babel-loader@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c"
|
||||
|
@ -2446,16 +2487,16 @@ bonjour-service@^1.0.11:
|
|||
fast-deep-equal "^3.1.3"
|
||||
multicast-dns "^7.2.5"
|
||||
|
||||
"bootstrap-v4@npm:bootstrap@^4.6.2":
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479"
|
||||
integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==
|
||||
|
||||
bootstrap@^5.2.3:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0.tgz#0718a7cc29040ee8dbf1bd652b896f3436a87c29"
|
||||
integrity sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==
|
||||
|
||||
bootswatch@^5.2.3:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-5.3.0.tgz#7c7dd50bbe8519b0c6dbe01f4f9c3100b60228bd"
|
||||
integrity sha512-ga2hHognDrh5h3+CaBBug6ktx3MTlnDzH57s+Mvjt9ZcNxqwpK+m3sE3YIUSr8zf2iG05elOb1mnqqcdbce2ow==
|
||||
|
||||
boxen@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||
|
@ -3152,6 +3193,11 @@ cyclist@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3"
|
||||
integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
|
@ -3319,6 +3365,11 @@ depd@~1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
|
||||
|
||||
dequal@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
destroy@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
|
@ -3696,6 +3747,28 @@ eslint-plugin-inferno@^7.32.2:
|
|||
resolve "^2.0.0-next.4"
|
||||
semver "^7.3.8"
|
||||
|
||||
eslint-plugin-jsx-a11y@^6.7.1:
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976"
|
||||
integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
aria-query "^5.1.3"
|
||||
array-includes "^3.1.6"
|
||||
array.prototype.flatmap "^1.3.1"
|
||||
ast-types-flow "^0.0.7"
|
||||
axe-core "^4.6.2"
|
||||
axobject-query "^3.1.1"
|
||||
damerau-levenshtein "^1.0.8"
|
||||
emoji-regex "^9.2.2"
|
||||
has "^1.0.3"
|
||||
jsx-ast-utils "^3.3.3"
|
||||
language-tags "=1.0.5"
|
||||
minimatch "^3.1.2"
|
||||
object.entries "^1.1.6"
|
||||
object.fromentries "^2.0.6"
|
||||
semver "^6.3.0"
|
||||
|
||||
eslint-plugin-prettier@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
|
||||
|
@ -5590,6 +5663,18 @@ klona@^2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
|
||||
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
|
||||
|
||||
language-subtag-registry@~0.3.2:
|
||||
version "0.3.22"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
|
||||
integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==
|
||||
|
||||
language-tags@=1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a"
|
||||
integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==
|
||||
dependencies:
|
||||
language-subtag-registry "~0.3.2"
|
||||
|
||||
latest-version@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||
|
|
Loading…
Reference in a new issue