mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge branch 'main' into feat/create-post-file-upload-a11y
This commit is contained in:
commit
0bd0a49730
64 changed files with 26776 additions and 3360 deletions
|
@ -2,3 +2,4 @@ src/shared/translations
|
||||||
lemmy-translations
|
lemmy-translations
|
||||||
src/assets/css/themes/*.css
|
src/assets/css/themes/*.css
|
||||||
stats.json
|
stats.json
|
||||||
|
dist
|
||||||
|
|
|
@ -32,3 +32,14 @@ pipeline:
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
when:
|
when:
|
||||||
event: tag
|
event: tag
|
||||||
|
|
||||||
|
nightly_build:
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
secrets: [docker_username, docker_password]
|
||||||
|
settings:
|
||||||
|
repo: dessalines/lemmy-ui
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
tag: dev
|
||||||
|
when:
|
||||||
|
event: cron
|
||||||
|
|
|
@ -27,7 +27,7 @@ COPY .git .git
|
||||||
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
|
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
|
||||||
|
|
||||||
RUN yarn --production --prefer-offline
|
RUN yarn --production --prefer-offline
|
||||||
RUN yarn build:prod
|
RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod
|
||||||
|
|
||||||
# Prune the image
|
# Prune the image
|
||||||
RUN node-prune /usr/src/app/node_modules
|
RUN node-prune /usr/src/app/node_modules
|
||||||
|
|
|
@ -20,6 +20,7 @@ COPY generate_translations.js \
|
||||||
|
|
||||||
COPY lemmy-translations lemmy-translations
|
COPY lemmy-translations lemmy-translations
|
||||||
COPY src src
|
COPY src src
|
||||||
|
COPY .git .git
|
||||||
|
|
||||||
# Set UI version
|
# Set UI version
|
||||||
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
|
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.18.0",
|
"version": "0.18.1-rc.7",
|
||||||
"description": "An isomorphic UI for lemmy",
|
"description": "An isomorphic UI for lemmy",
|
||||||
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
|
@ -8,9 +8,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "webpack --mode=none",
|
"analyze": "webpack --mode=none",
|
||||||
"prebuild:dev": "yarn clean && node generate_translations.js",
|
"prebuild:dev": "yarn clean && node generate_translations.js",
|
||||||
"build:dev": "webpack --mode=development",
|
"build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development",
|
||||||
"prebuild:prod": "yarn clean && node generate_translations.js",
|
"prebuild:prod": "yarn clean && node generate_translations.js",
|
||||||
"build:prod": "webpack --mode=production",
|
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
|
||||||
"clean": "yarn run rimraf dist",
|
"clean": "yarn run rimraf dist",
|
||||||
"dev": "yarn build:dev --watch",
|
"dev": "yarn build:dev --watch",
|
||||||
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
|
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
|
||||||
|
@ -48,6 +48,7 @@
|
||||||
"check-password-strength": "^2.0.7",
|
"check-password-strength": "^2.0.7",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"cross-fetch": "^3.1.5",
|
"cross-fetch": "^3.1.5",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
|
@ -65,7 +66,6 @@
|
||||||
"inferno-i18next-dess": "0.0.2",
|
"inferno-i18next-dess": "0.0.2",
|
||||||
"inferno-router": "^8.1.1",
|
"inferno-router": "^8.1.1",
|
||||||
"inferno-server": "^8.1.1",
|
"inferno-server": "^8.1.1",
|
||||||
"isomorphic-cookie": "^1.2.4",
|
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lemmy-js-client": "0.18.0-rc.2",
|
"lemmy-js-client": "0.18.0-rc.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
@ -97,6 +97,7 @@
|
||||||
"@babel/core": "^7.21.8",
|
"@babel/core": "^7.21.8",
|
||||||
"@types/autosize": "^4.0.0",
|
"@types/autosize": "^4.0.0",
|
||||||
"@types/bootstrap": "^5.2.6",
|
"@types/bootstrap": "^5.2.6",
|
||||||
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/html-to-text": "^9.0.0",
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
"style-loader": "^3.3.2",
|
"style-loader": "^3.3.2",
|
||||||
"terser": "^5.17.3",
|
"terser": "^5.17.3",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
"typescript-language-server": "^3.3.2",
|
||||||
"webpack-bundle-analyzer": "^4.9.0",
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
"webpack-dev-server": "4.15.0"
|
"webpack-dev-server": "4.15.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vote-bar {
|
.vote-bar {
|
||||||
|
min-width: 5ch;
|
||||||
margin-top: -6.5px;
|
margin-top: -6.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +199,9 @@ blockquote {
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
min-height: 60px;
|
aspect-ratio: 1/1;
|
||||||
max-height: 80px;
|
width: 5rem;
|
||||||
width: 100%;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail svg {
|
.thumbnail svg {
|
||||||
|
@ -360,8 +361,9 @@ br.big {
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-icon {
|
.img-icon {
|
||||||
width: 2rem;
|
width: calc(var(--bs-body-line-height) * 1em);
|
||||||
height: 2rem;
|
height: calc(var(--bs-body-line-height) * 1em);
|
||||||
|
border-radius: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container ul {
|
.tribute-container ul {
|
||||||
|
|
117
src/assets/css/themes/_variables.darkly-pureblack.scss
Normal file
117
src/assets/css/themes/_variables.darkly-pureblack.scss
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
@import "./variables";
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
$white: #f3f3f3;
|
||||||
|
$gray-200: #ebebeb;
|
||||||
|
$gray-300: #dee2e6;
|
||||||
|
$gray-500: #adb5bd;
|
||||||
|
$gray-600: #666;
|
||||||
|
$gray-700: #333;
|
||||||
|
$gray-800: #202020;
|
||||||
|
$gray-900: #111;
|
||||||
|
$black: #000;
|
||||||
|
|
||||||
|
$blue: #375a7f;
|
||||||
|
$red: #e74c3c;
|
||||||
|
$yellow: #f39c12;
|
||||||
|
$green: #00bc8c;
|
||||||
|
$cyan: #3498db;
|
||||||
|
|
||||||
|
$primary: $green;
|
||||||
|
$secondary: $gray-700;
|
||||||
|
$success: $green;
|
||||||
|
$dark: $gray-300;
|
||||||
|
|
||||||
|
$body-color: $gray-200;
|
||||||
|
$body-bg: $black;
|
||||||
|
$link-color: $success;
|
||||||
|
$border-color: rgba($body-color, 0.25);
|
||||||
|
$mark-bg: $gray-900;
|
||||||
|
$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";
|
||||||
|
$font-size-base: 0.9375rem;
|
||||||
|
$h1-font-size: 3rem;
|
||||||
|
$h2-font-size: 2.5rem;
|
||||||
|
$h3-font-size: 2rem;
|
||||||
|
|
||||||
|
$card-cap-bg: $gray-900;
|
||||||
|
$card-bg: $gray-900;
|
||||||
|
$card-color: $gray-300;
|
||||||
|
|
||||||
|
$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);
|
||||||
|
$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-900;
|
||||||
|
$input-color: $white;
|
||||||
|
$input-disabled-bg: darken($gray-900, 20%);
|
||||||
|
$input-border-color: $gray-800;
|
||||||
|
$input-group-addon-color: $gray-800;
|
||||||
|
$input-group-addon-bg: $gray-800;
|
||||||
|
|
||||||
|
$hr-border-color: rgba($body-color, 0.25);
|
||||||
|
|
||||||
|
$table-border-color: $gray-700;
|
||||||
|
|
||||||
|
$custom-file-color: $gray-500;
|
||||||
|
$custom-file-border-color: $body-bg;
|
||||||
|
|
||||||
|
$dropdown-bg: $gray-900;
|
||||||
|
$dropdown-border-color: $gray-800;
|
||||||
|
$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;
|
||||||
|
$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-900;
|
||||||
|
$popover-bg: $gray-900;
|
||||||
|
$popover-header-bg: $gray-900;
|
||||||
|
$toast-background-color: $gray-800;
|
||||||
|
$toast-header-background-color: $gray-900;
|
||||||
|
$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;
|
||||||
|
$custom-select-bg: $gray-700;
|
||||||
|
$custom-select-color: $white;
|
||||||
|
$light: $gray-900;
|
58
src/assets/css/themes/_variables.i386.scss
Normal file
58
src/assets/css/themes/_variables.i386.scss
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
@import "./variables";
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
$white: #fff;
|
||||||
|
$gray-100: #f8f9fa;
|
||||||
|
$gray-200: #ebebeb;
|
||||||
|
$gray-300: #bbb;
|
||||||
|
$gray-500: #adb5bd;
|
||||||
|
$gray-800: #303030;
|
||||||
|
$gray-900: #222;
|
||||||
|
|
||||||
|
$blue: #5555ff;
|
||||||
|
$cyan: #55ffff;
|
||||||
|
$green: #55ff55;
|
||||||
|
$indigo: #ff55ff;
|
||||||
|
$red: #ff5555;
|
||||||
|
$yellow: #fefe54;
|
||||||
|
$orange: #a85400;
|
||||||
|
$pink: #fe54fe;
|
||||||
|
$purple: #fe5454;
|
||||||
|
|
||||||
|
$primary: #fefe54;
|
||||||
|
$secondary: $gray-900;
|
||||||
|
$success: #00aa00;
|
||||||
|
$danger: #aa0000;
|
||||||
|
$info: #00aaaa;
|
||||||
|
$warning: #aa00aa;
|
||||||
|
$light: $gray-800;
|
||||||
|
$dark: black;
|
||||||
|
|
||||||
|
$body-bg: #000084;
|
||||||
|
$body-color: $gray-300;
|
||||||
|
|
||||||
|
$link-hover-color: $white;
|
||||||
|
|
||||||
|
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||||
|
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||||
|
|
||||||
|
$navbar-dark-color: $gray-300;
|
||||||
|
$navbar-light-brand-color: $gray-300;
|
||||||
|
$navbar-dark-active-color: $gray-100;
|
||||||
|
$nav-tabs-link-active-color: $gray-100;
|
||||||
|
$navbar-dark-hover-color: rgba($gray-300, 0.75);
|
||||||
|
$navbar-light-disabled-color: $gray-800;
|
||||||
|
$navbar-light-active-color: $gray-100;
|
||||||
|
$navbar-light-hover-color: $gray-200;
|
||||||
|
$navbar-light-color: $gray-300;
|
||||||
|
|
||||||
|
$enable-rounded: false;
|
||||||
|
|
||||||
|
$input-color: $white;
|
||||||
|
$input-bg: rgb(102, 102, 102);
|
||||||
|
$input-placeholder-color: $gray-500;
|
||||||
|
$input-disabled-bg: $gray-800;
|
||||||
|
|
||||||
|
$card-bg: $gray-800;
|
||||||
|
$card-border-color: $white;
|
||||||
|
$mark-bg: #463b00;
|
|
@ -1,7 +1,3 @@
|
||||||
$link-decoration: none;
|
$link-decoration: none;
|
||||||
$min-contrast-ratio: 3;
|
$min-contrast-ratio: 3;
|
||||||
$font-size-base: 0.875rem;
|
$font-size-base: 0.875rem;
|
||||||
|
|
||||||
$container-max-widths: (
|
|
||||||
lg: 1140px,
|
|
||||||
);
|
|
||||||
|
|
11829
src/assets/css/themes/darkly-pureblack.css
Normal file
11829
src/assets/css/themes/darkly-pureblack.css
Normal file
File diff suppressed because it is too large
Load diff
2
src/assets/css/themes/darkly-pureblack.scss
Normal file
2
src/assets/css/themes/darkly-pureblack.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import "variables.darkly-pureblack";
|
||||||
|
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
|
@ -726,7 +726,11 @@ progress {
|
||||||
|
|
||||||
.container,
|
.container,
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
.container-lg {
|
.container-xxl,
|
||||||
|
.container-xl,
|
||||||
|
.container-lg,
|
||||||
|
.container-md,
|
||||||
|
.container-sm {
|
||||||
--bs-gutter-x: 1.5rem;
|
--bs-gutter-x: 1.5rem;
|
||||||
--bs-gutter-y: 0;
|
--bs-gutter-y: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -736,11 +740,31 @@ progress {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container-sm, .container {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container-md, .container-sm, .container {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container-lg, .container-md, .container-sm, .container {
|
.container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 1320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--bs-breakpoint-xs: 0;
|
--bs-breakpoint-xs: 0;
|
||||||
--bs-breakpoint-sm: 576px;
|
--bs-breakpoint-sm: 576px;
|
||||||
|
@ -3867,7 +3891,11 @@ textarea.form-control-lg {
|
||||||
}
|
}
|
||||||
.navbar > .container,
|
.navbar > .container,
|
||||||
.navbar > .container-fluid,
|
.navbar > .container-fluid,
|
||||||
.navbar > .container-lg {
|
.navbar > .container-sm,
|
||||||
|
.navbar > .container-md,
|
||||||
|
.navbar > .container-lg,
|
||||||
|
.navbar > .container-xl,
|
||||||
|
.navbar > .container-xxl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: inherit;
|
flex-wrap: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -726,7 +726,11 @@ progress {
|
||||||
|
|
||||||
.container,
|
.container,
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
.container-lg {
|
.container-xxl,
|
||||||
|
.container-xl,
|
||||||
|
.container-lg,
|
||||||
|
.container-md,
|
||||||
|
.container-sm {
|
||||||
--bs-gutter-x: 1.5rem;
|
--bs-gutter-x: 1.5rem;
|
||||||
--bs-gutter-y: 0;
|
--bs-gutter-y: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -736,11 +740,31 @@ progress {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container-sm, .container {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container-md, .container-sm, .container {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container-lg, .container-md, .container-sm, .container {
|
.container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 1320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--bs-breakpoint-xs: 0;
|
--bs-breakpoint-xs: 0;
|
||||||
--bs-breakpoint-sm: 576px;
|
--bs-breakpoint-sm: 576px;
|
||||||
|
@ -3867,7 +3891,11 @@ textarea.form-control-lg {
|
||||||
}
|
}
|
||||||
.navbar > .container,
|
.navbar > .container,
|
||||||
.navbar > .container-fluid,
|
.navbar > .container-fluid,
|
||||||
.navbar > .container-lg {
|
.navbar > .container-sm,
|
||||||
|
.navbar > .container-md,
|
||||||
|
.navbar > .container-lg,
|
||||||
|
.navbar > .container-xl,
|
||||||
|
.navbar > .container-xxl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: inherit;
|
flex-wrap: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
11594
src/assets/css/themes/i386.css
Normal file
11594
src/assets/css/themes/i386.css
Normal file
File diff suppressed because it is too large
Load diff
16
src/assets/css/themes/i386.scss
Normal file
16
src/assets/css/themes/i386.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import "variables.i386";
|
||||||
|
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.active,
|
||||||
|
.dropdown-item:hover,
|
||||||
|
option:disabled {
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background: $gray-500;
|
||||||
|
}
|
|
@ -725,7 +725,11 @@ progress {
|
||||||
|
|
||||||
.container,
|
.container,
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
.container-lg {
|
.container-xxl,
|
||||||
|
.container-xl,
|
||||||
|
.container-lg,
|
||||||
|
.container-md,
|
||||||
|
.container-sm {
|
||||||
--bs-gutter-x: 1.5rem;
|
--bs-gutter-x: 1.5rem;
|
||||||
--bs-gutter-y: 0;
|
--bs-gutter-y: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -735,11 +739,31 @@ progress {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container-sm, .container {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container-md, .container-sm, .container {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container-lg, .container-md, .container-sm, .container {
|
.container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 1320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--bs-breakpoint-xs: 0;
|
--bs-breakpoint-xs: 0;
|
||||||
--bs-breakpoint-sm: 576px;
|
--bs-breakpoint-sm: 576px;
|
||||||
|
@ -3866,7 +3890,11 @@ textarea.form-control-lg {
|
||||||
}
|
}
|
||||||
.navbar > .container,
|
.navbar > .container,
|
||||||
.navbar > .container-fluid,
|
.navbar > .container-fluid,
|
||||||
.navbar > .container-lg {
|
.navbar > .container-sm,
|
||||||
|
.navbar > .container-md,
|
||||||
|
.navbar > .container-lg,
|
||||||
|
.navbar > .container-xl,
|
||||||
|
.navbar > .container-xxl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: inherit;
|
flex-wrap: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -725,7 +725,11 @@ progress {
|
||||||
|
|
||||||
.container,
|
.container,
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
.container-lg {
|
.container-xxl,
|
||||||
|
.container-xl,
|
||||||
|
.container-lg,
|
||||||
|
.container-md,
|
||||||
|
.container-sm {
|
||||||
--bs-gutter-x: 1.5rem;
|
--bs-gutter-x: 1.5rem;
|
||||||
--bs-gutter-y: 0;
|
--bs-gutter-y: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -735,11 +739,31 @@ progress {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container-sm, .container {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container-md, .container-sm, .container {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container-lg, .container-md, .container-sm, .container {
|
.container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
|
||||||
|
max-width: 1320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--bs-breakpoint-xs: 0;
|
--bs-breakpoint-xs: 0;
|
||||||
--bs-breakpoint-sm: 576px;
|
--bs-breakpoint-sm: 576px;
|
||||||
|
@ -3866,7 +3890,11 @@ textarea.form-control-lg {
|
||||||
}
|
}
|
||||||
.navbar > .container,
|
.navbar > .container,
|
||||||
.navbar > .container-fluid,
|
.navbar > .container-fluid,
|
||||||
.navbar > .container-lg {
|
.navbar > .container-sm,
|
||||||
|
.navbar > .container-md,
|
||||||
|
.navbar > .container-lg,
|
||||||
|
.navbar > .container-xl,
|
||||||
|
.navbar > .container-xxl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: inherit;
|
flex-wrap: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { initializeSite, isAuthPath } from "@utils/app";
|
import { initializeSite, isAuthPath } from "@utils/app";
|
||||||
import { getHttpBaseInternal } from "@utils/env";
|
import { getHttpBaseInternal } from "@utils/env";
|
||||||
import { ErrorPageData } from "@utils/types";
|
import { ErrorPageData } from "@utils/types";
|
||||||
|
import * as cookie from "cookie";
|
||||||
import fetch from "cross-fetch";
|
import fetch from "cross-fetch";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { StaticRouter, matchPath } from "inferno-router";
|
import { StaticRouter, matchPath } from "inferno-router";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
|
||||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
import { App } from "../../shared/components/app/app";
|
import { App } from "../../shared/components/app/app";
|
||||||
import {
|
import {
|
||||||
|
@ -25,11 +25,15 @@ import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||||
export default async (req: Request, res: Response) => {
|
export default async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
|
||||||
|
let auth = req.headers.cookie
|
||||||
|
? cookie.parse(req.headers.cookie).jwt
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const getSiteForm: GetSite = { auth };
|
const getSiteForm: GetSite = { auth };
|
||||||
|
|
||||||
const headers = setForwardedHeaders(req.headers);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
|
|
||||||
const client = wrapClient(
|
const client = wrapClient(
|
||||||
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
||||||
);
|
);
|
||||||
|
@ -43,6 +47,7 @@ export default async (req: Request, res: Response) => {
|
||||||
let routeData: RouteData = {};
|
let routeData: RouteData = {};
|
||||||
let errorPageData: ErrorPageData | undefined = undefined;
|
let errorPageData: ErrorPageData | undefined = undefined;
|
||||||
let try_site = await client.getSite(getSiteForm);
|
let try_site = await client.getSite(getSiteForm);
|
||||||
|
|
||||||
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
||||||
console.error(
|
console.error(
|
||||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||||
|
@ -75,7 +80,12 @@ export default async (req: Request, res: Response) => {
|
||||||
|
|
||||||
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!activeRoute) {
|
||||||
|
res.status(404);
|
||||||
|
}
|
||||||
} else if (try_site.state === "failed") {
|
} else if (try_site.state === "failed") {
|
||||||
|
res.status(500);
|
||||||
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,9 +96,11 @@ export default async (req: Request, res: Response) => {
|
||||||
// Redirect to the 404 if there's an API error
|
// Redirect to the 404 if there's an API error
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error.msg);
|
console.error(error.msg);
|
||||||
|
|
||||||
if (error.msg === "instance_is_private") {
|
if (error.msg === "instance_is_private") {
|
||||||
return res.redirect(`/signup`);
|
return res.redirect(`/signup`);
|
||||||
} else {
|
} else {
|
||||||
|
res.status(500);
|
||||||
errorPageData = getErrorPageData(new Error(error.msg), site);
|
errorPageData = getErrorPageData(new Error(error.msg), site);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +125,7 @@ export default async (req: Request, res: Response) => {
|
||||||
// If an error is caught here, the error page couldn't even be rendered
|
// If an error is caught here, the error page couldn't even be rendered
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
||||||
);
|
);
|
||||||
|
|
17
src/server/handlers/security-handler.ts
Normal file
17
src/server/handlers/security-handler.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Response } from "express";
|
||||||
|
|
||||||
|
export default async ({ res }: { res: Response }) => {
|
||||||
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
`Contact: mailto:security@lemmy.ml
|
||||||
|
Contact: mailto:admin@` +
|
||||||
|
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||||
|
`
|
||||||
|
Contact: mailto:security@` +
|
||||||
|
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||||
|
`
|
||||||
|
Expires: 2024-01-01T04:59:00.000Z
|
||||||
|
`
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,16 @@
|
||||||
import { setupDateFns } from "@utils/app";
|
import { setupDateFns } from "@utils/app";
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import CatchAllHandler from "./handlers/catch-all-handler";
|
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||||
import ManifestHandler from "./handlers/manifest-handler";
|
import ManifestHandler from "./handlers/manifest-handler";
|
||||||
import RobotsHandler from "./handlers/robots-handler";
|
import RobotsHandler from "./handlers/robots-handler";
|
||||||
|
import SecurityHandler from "./handlers/security-handler";
|
||||||
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
||||||
import ThemeHandler from "./handlers/theme-handler";
|
import ThemeHandler from "./handlers/theme-handler";
|
||||||
import ThemesListHandler from "./handlers/themes-list-handler";
|
import ThemesListHandler from "./handlers/themes-list-handler";
|
||||||
import setDefaultCsp from "./middleware/set-default-csp";
|
import { setCacheControl, setDefaultCsp } from "./middleware";
|
||||||
|
|
||||||
const server = express();
|
const server = express();
|
||||||
|
|
||||||
|
@ -18,12 +20,20 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||||
|
|
||||||
server.use(express.json());
|
server.use(express.json());
|
||||||
server.use(express.urlencoded({ extended: false }));
|
server.use(express.urlencoded({ extended: false }));
|
||||||
server.use("/static", express.static(path.resolve("./dist")));
|
server.use(
|
||||||
|
getStaticDir(),
|
||||||
|
express.static(path.resolve("./dist"), {
|
||||||
|
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||||
|
immutable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
server.use(setCacheControl);
|
||||||
|
|
||||||
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||||
server.use(setDefaultCsp);
|
server.use(setDefaultCsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.get("/.well-known/security.txt", SecurityHandler);
|
||||||
server.get("/robots.txt", RobotsHandler);
|
server.get("/robots.txt", RobotsHandler);
|
||||||
server.get("/service-worker.js", ServiceWorkerHandler);
|
server.get("/service-worker.js", ServiceWorkerHandler);
|
||||||
server.get("/manifest.webmanifest", ManifestHandler);
|
server.get("/manifest.webmanifest", ManifestHandler);
|
||||||
|
|
53
src/server/middleware.ts
Normal file
53
src/server/middleware.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
||||||
|
|
||||||
|
export function setDefaultCsp({
|
||||||
|
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 * data:`
|
||||||
|
);
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
|
||||||
|
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
|
||||||
|
// all responses for 5 seconds to reduce load on backend and database. The specific cache
|
||||||
|
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
|
||||||
|
//
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||||
|
export function setCacheControl(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let caching: string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) ||
|
||||||
|
req.path.includes("/css/themelist")
|
||||||
|
) {
|
||||||
|
// Static content gets cached publicly for a day
|
||||||
|
caching = "public, max-age=86400";
|
||||||
|
} else {
|
||||||
|
if (hasJwtCookie(req)) {
|
||||||
|
caching = "private";
|
||||||
|
} else {
|
||||||
|
caching = "public, max-age=5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Cache-Control", caching);
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
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 * data:`
|
|
||||||
);
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
|
@ -8,9 +8,11 @@ const themes: ReadonlyArray<string> = [
|
||||||
"darkly",
|
"darkly",
|
||||||
"darkly-red",
|
"darkly-red",
|
||||||
"darkly-compact",
|
"darkly-compact",
|
||||||
|
"darkly-pureblack",
|
||||||
"litely",
|
"litely",
|
||||||
"litely-red",
|
"litely-red",
|
||||||
"litely-compact",
|
"litely-compact",
|
||||||
|
"i386",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function buildThemeList(): Promise<ReadonlyArray<string>> {
|
export async function buildThemeList(): Promise<ReadonlyArray<string>> {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import { Helmet } from "inferno-helmet";
|
import { Helmet } from "inferno-helmet";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import serialize from "serialize-javascript";
|
import serialize from "serialize-javascript";
|
||||||
|
@ -23,7 +24,7 @@ export async function createSsrHtml(
|
||||||
|
|
||||||
if (!appleTouchIcon) {
|
if (!appleTouchIcon) {
|
||||||
appleTouchIcon = site?.site_view.site.icon
|
appleTouchIcon = site?.site_view.site.icon
|
||||||
? `data:image/png;base64,${sharp(
|
? `data:image/png;base64,${await sharp(
|
||||||
await fetchIconPng(site.site_view.site.icon)
|
await fetchIconPng(site.site_view.site.icon)
|
||||||
)
|
)
|
||||||
.resize(180, 180)
|
.resize(180, 180)
|
||||||
|
@ -87,7 +88,7 @@ export async function createSsrHtml(
|
||||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
<link rel="stylesheet" type="text/css" href="${getStaticDir()}/styles/styles.css" />
|
||||||
|
|
||||||
<!-- Current theme and more -->
|
<!-- Current theme and more -->
|
||||||
${helmet.link.toString() || fallbackTheme}
|
${helmet.link.toString() || fallbackTheme}
|
||||||
|
@ -102,7 +103,7 @@ export async function createSsrHtml(
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<div id='root'>${root}</div>
|
<div id='root'>${root}</div>
|
||||||
<script defer src='/static/js/client.js'></script>
|
<script defer src='${getStaticDir()}/js/client.js'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
6
src/server/utils/has-jwt-cookie.ts
Normal file
6
src/server/utils/has-jwt-cookie.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import * as cookie from "cookie";
|
||||||
|
import type { Request } from "express";
|
||||||
|
|
||||||
|
export function hasJwtCookie(req: Request): boolean {
|
||||||
|
return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length);
|
||||||
|
}
|
|
@ -79,256 +79,246 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
const siteView = this.props.siteRes?.site_view;
|
const siteView = this.props.siteRes?.site_view;
|
||||||
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
||||||
return (
|
return (
|
||||||
<nav
|
<div className="shadow-sm">
|
||||||
className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
|
<nav
|
||||||
id="navbar"
|
className="navbar navbar-expand-md navbar-light p-0 px-3 container-lg"
|
||||||
>
|
id="navbar"
|
||||||
<NavLink
|
|
||||||
id="navTitle"
|
|
||||||
to="/"
|
|
||||||
title={siteView?.site.description ?? siteView?.site.name}
|
|
||||||
className="d-flex align-items-center navbar-brand me-md-3"
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
>
|
||||||
{siteView?.site.icon && showAvatars() && (
|
<NavLink
|
||||||
<PictrsImage src={siteView.site.icon} icon />
|
id="navTitle"
|
||||||
)}
|
to="/"
|
||||||
{siteView?.site.name}
|
title={siteView?.site.description ?? siteView?.site.name}
|
||||||
</NavLink>
|
className="d-flex align-items-center navbar-brand me-md-3"
|
||||||
{person && (
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
<ul className="navbar-nav d-flex flex-row ms-auto d-md-none">
|
>
|
||||||
<li id="navMessages" className="nav-item nav-item-icon">
|
{siteView?.site.icon && showAvatars() && (
|
||||||
<NavLink
|
<PictrsImage src={siteView.site.icon} icon />
|
||||||
to="/inbox"
|
)}
|
||||||
className="p-1 nav-link border-0 nav-messages"
|
{siteView?.site.name}
|
||||||
title={I18NextService.i18n.t("unread_messages", {
|
</NavLink>
|
||||||
count: Number(this.state.unreadApplicationCountRes.state),
|
{person && (
|
||||||
formattedCount: numToSI(this.unreadInboxCount),
|
<ul className="navbar-nav d-flex flex-row ms-auto d-md-none">
|
||||||
})}
|
<li id="navMessages" className="nav-item nav-item-icon">
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="bell" />
|
|
||||||
{this.unreadInboxCount > 0 && (
|
|
||||||
<span className="mx-1 badge text-bg-light">
|
|
||||||
{numToSI(this.unreadInboxCount)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
{this.moderatesSomething && (
|
|
||||||
<li className="nav-item nav-item-icon">
|
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/reports"
|
to="/inbox"
|
||||||
className="p-1 nav-link border-0"
|
className="p-1 nav-link border-0 nav-messages"
|
||||||
title={I18NextService.i18n.t("unread_reports", {
|
title={I18NextService.i18n.t("unread_messages", {
|
||||||
count: Number(this.unreadReportCount),
|
count: Number(this.state.unreadApplicationCountRes.state),
|
||||||
formattedCount: numToSI(this.unreadReportCount),
|
formattedCount: numToSI(this.unreadInboxCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="shield" />
|
<Icon icon="bell" />
|
||||||
{this.unreadReportCount > 0 && (
|
{this.unreadInboxCount > 0 && (
|
||||||
<span className="mx-1 badge text-bg-light">
|
<span className="mx-1 badge text-bg-light">
|
||||||
{numToSI(this.unreadReportCount)}
|
{numToSI(this.unreadInboxCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
)}
|
{this.moderatesSomething && (
|
||||||
{amAdmin() && (
|
<li className="nav-item nav-item-icon">
|
||||||
<li className="nav-item nav-item-icon">
|
|
||||||
<NavLink
|
|
||||||
to="/registration_applications"
|
|
||||||
className="p-1 nav-link border-0"
|
|
||||||
title={I18NextService.i18n.t(
|
|
||||||
"unread_registration_applications",
|
|
||||||
{
|
|
||||||
count: Number(this.unreadApplicationCount),
|
|
||||||
formattedCount: numToSI(this.unreadApplicationCount),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="clipboard" />
|
|
||||||
{this.unreadApplicationCount > 0 && (
|
|
||||||
<span className="mx-1 badge text-bg-light">
|
|
||||||
{numToSI(this.unreadApplicationCount)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="navbar-toggler border-0 p-1"
|
|
||||||
type="button"
|
|
||||||
aria-label="menu"
|
|
||||||
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#navbarDropdown"
|
|
||||||
aria-controls="navbarDropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
ref={this.collapseButtonRef}
|
|
||||||
>
|
|
||||||
<Icon icon="menu" />
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className="collapse navbar-collapse my-2"
|
|
||||||
id="navbarDropdown"
|
|
||||||
ref={this.mobileMenuRef}
|
|
||||||
>
|
|
||||||
<ul id="navbarLinks" className="me-auto navbar-nav">
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
to="/communities"
|
|
||||||
className="nav-link"
|
|
||||||
title={I18NextService.i18n.t("communities")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("communities")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
{/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
|
|
||||||
<NavLink
|
|
||||||
to={{
|
|
||||||
pathname: "/create_post",
|
|
||||||
search: "",
|
|
||||||
hash: "",
|
|
||||||
key: "",
|
|
||||||
state: { prevPath: this.currentLocation },
|
|
||||||
}}
|
|
||||||
className="nav-link"
|
|
||||||
title={I18NextService.i18n.t("create_post")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("create_post")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
{this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
to="/create_community"
|
|
||||||
className="nav-link"
|
|
||||||
title={I18NextService.i18n.t("create_community")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("create_community")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<li className="nav-item">
|
|
||||||
<a
|
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
|
||||||
title={I18NextService.i18n.t("support_lemmy")}
|
|
||||||
href={donateLemmyUrl}
|
|
||||||
>
|
|
||||||
<Icon icon="heart" classes="small" />
|
|
||||||
<span className="d-inline ms-1 d-md-none ms-md-0">
|
|
||||||
{I18NextService.i18n.t("support_lemmy")}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul id="navbarIcons" className="navbar-nav">
|
|
||||||
<li id="navSearch" className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
to="/search"
|
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
|
||||||
title={I18NextService.i18n.t("search")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="search" />
|
|
||||||
<span className="d-inline ms-1 d-md-none ms-md-0">
|
|
||||||
{I18NextService.i18n.t("search")}
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
{amAdmin() && (
|
|
||||||
<li id="navAdmin" className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
to="/admin"
|
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
|
||||||
title={I18NextService.i18n.t("admin_settings")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="settings" />
|
|
||||||
<span className="d-inline ms-1 d-md-none ms-md-0">
|
|
||||||
{I18NextService.i18n.t("admin_settings")}
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{person ? (
|
|
||||||
<>
|
|
||||||
<li id="navMessages" className="nav-item">
|
|
||||||
<NavLink
|
<NavLink
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
to="/reports"
|
||||||
to="/inbox"
|
className="p-1 nav-link border-0"
|
||||||
title={I18NextService.i18n.t("unread_messages", {
|
title={I18NextService.i18n.t("unread_reports", {
|
||||||
count: Number(this.unreadInboxCount),
|
count: Number(this.unreadReportCount),
|
||||||
formattedCount: numToSI(this.unreadInboxCount),
|
formattedCount: numToSI(this.unreadReportCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="bell" />
|
<Icon icon="shield" />
|
||||||
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
{this.unreadReportCount > 0 && (
|
||||||
{I18NextService.i18n.t("unread_messages", {
|
|
||||||
count: Number(this.unreadInboxCount),
|
|
||||||
formattedCount: numToSI(this.unreadInboxCount),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{this.unreadInboxCount > 0 && (
|
|
||||||
<span className="mx-1 badge text-bg-light">
|
<span className="mx-1 badge text-bg-light">
|
||||||
{numToSI(this.unreadInboxCount)}
|
{numToSI(this.unreadReportCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
{this.moderatesSomething && (
|
)}
|
||||||
<li id="navModeration" className="nav-item">
|
{amAdmin() && (
|
||||||
|
<li className="nav-item nav-item-icon">
|
||||||
|
<NavLink
|
||||||
|
to="/registration_applications"
|
||||||
|
className="p-1 nav-link border-0"
|
||||||
|
title={I18NextService.i18n.t(
|
||||||
|
"unread_registration_applications",
|
||||||
|
{
|
||||||
|
count: Number(this.unreadApplicationCount),
|
||||||
|
formattedCount: numToSI(this.unreadApplicationCount),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
<Icon icon="clipboard" />
|
||||||
|
{this.unreadApplicationCount > 0 && (
|
||||||
|
<span className="mx-1 badge text-bg-light">
|
||||||
|
{numToSI(this.unreadApplicationCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="navbar-toggler border-0 p-1"
|
||||||
|
type="button"
|
||||||
|
aria-label="menu"
|
||||||
|
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarDropdown"
|
||||||
|
aria-controls="navbarDropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
ref={this.collapseButtonRef}
|
||||||
|
>
|
||||||
|
<Icon icon="menu" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="collapse navbar-collapse my-2"
|
||||||
|
id="navbarDropdown"
|
||||||
|
ref={this.mobileMenuRef}
|
||||||
|
>
|
||||||
|
<ul id="navbarLinks" className="me-auto navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/communities"
|
||||||
|
className="nav-link"
|
||||||
|
title={I18NextService.i18n.t("communities")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("communities")}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
{/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
|
||||||
|
<NavLink
|
||||||
|
to={{
|
||||||
|
pathname: "/create_post",
|
||||||
|
search: "",
|
||||||
|
hash: "",
|
||||||
|
key: "",
|
||||||
|
state: { prevPath: this.currentLocation },
|
||||||
|
}}
|
||||||
|
className="nav-link"
|
||||||
|
title={I18NextService.i18n.t("create_post")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("create_post")}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
{this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/create_community"
|
||||||
|
className="nav-link"
|
||||||
|
title={I18NextService.i18n.t("create_community")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("create_community")}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li className="nav-item">
|
||||||
|
<a
|
||||||
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
|
title={I18NextService.i18n.t("support_lemmy")}
|
||||||
|
href={donateLemmyUrl}
|
||||||
|
>
|
||||||
|
<Icon icon="heart" classes="small" />
|
||||||
|
<span className="d-inline ms-1 d-md-none ms-md-0">
|
||||||
|
{I18NextService.i18n.t("support_lemmy")}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul id="navbarIcons" className="navbar-nav">
|
||||||
|
<li id="navSearch" className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/search"
|
||||||
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
|
title={I18NextService.i18n.t("search")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
<Icon icon="search" />
|
||||||
|
<span className="d-inline ms-1 d-md-none ms-md-0">
|
||||||
|
{I18NextService.i18n.t("search")}
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
{amAdmin() && (
|
||||||
|
<li id="navAdmin" className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/admin"
|
||||||
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
|
title={I18NextService.i18n.t("admin_settings")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
<Icon icon="settings" />
|
||||||
|
<span className="d-inline ms-1 d-md-none ms-md-0">
|
||||||
|
{I18NextService.i18n.t("admin_settings")}
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{person ? (
|
||||||
|
<>
|
||||||
|
<li id="navMessages" className="nav-item">
|
||||||
<NavLink
|
<NavLink
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
to="/reports"
|
to="/inbox"
|
||||||
title={I18NextService.i18n.t("unread_reports", {
|
title={I18NextService.i18n.t("unread_messages", {
|
||||||
count: Number(this.unreadReportCount),
|
count: Number(this.unreadInboxCount),
|
||||||
formattedCount: numToSI(this.unreadReportCount),
|
formattedCount: numToSI(this.unreadInboxCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="shield" />
|
<Icon icon="bell" />
|
||||||
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
||||||
{I18NextService.i18n.t("unread_reports", {
|
{I18NextService.i18n.t("unread_messages", {
|
||||||
count: Number(this.unreadReportCount),
|
count: Number(this.unreadInboxCount),
|
||||||
formattedCount: numToSI(this.unreadReportCount),
|
formattedCount: numToSI(this.unreadInboxCount),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
{this.unreadReportCount > 0 && (
|
{this.unreadInboxCount > 0 && (
|
||||||
<span className="mx-1 badge text-bg-light">
|
<span className="mx-1 badge text-bg-light">
|
||||||
{numToSI(this.unreadReportCount)}
|
{numToSI(this.unreadInboxCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
)}
|
{this.moderatesSomething && (
|
||||||
{amAdmin() && (
|
<li id="navModeration" className="nav-item">
|
||||||
<li id="navApplications" className="nav-item">
|
<NavLink
|
||||||
<NavLink
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
to="/registration_applications"
|
to="/reports"
|
||||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
title={I18NextService.i18n.t("unread_reports", {
|
||||||
title={I18NextService.i18n.t(
|
count: Number(this.unreadReportCount),
|
||||||
"unread_registration_applications",
|
formattedCount: numToSI(this.unreadReportCount),
|
||||||
{
|
})}
|
||||||
count: Number(this.unreadApplicationCount),
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
formattedCount: numToSI(this.unreadApplicationCount),
|
>
|
||||||
}
|
<Icon icon="shield" />
|
||||||
)}
|
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
{I18NextService.i18n.t("unread_reports", {
|
||||||
>
|
count: Number(this.unreadReportCount),
|
||||||
<Icon icon="clipboard" />
|
formattedCount: numToSI(this.unreadReportCount),
|
||||||
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
})}
|
||||||
{I18NextService.i18n.t(
|
</span>
|
||||||
|
{this.unreadReportCount > 0 && (
|
||||||
|
<span className="mx-1 badge text-bg-light">
|
||||||
|
{numToSI(this.unreadReportCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{amAdmin() && (
|
||||||
|
<li id="navApplications" className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/registration_applications"
|
||||||
|
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||||
|
title={I18NextService.i18n.t(
|
||||||
"unread_registration_applications",
|
"unread_registration_applications",
|
||||||
{
|
{
|
||||||
count: Number(this.unreadApplicationCount),
|
count: Number(this.unreadApplicationCount),
|
||||||
|
@ -337,97 +327,111 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</span>
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
{this.unreadApplicationCount > 0 && (
|
>
|
||||||
<span className="mx-1 badge text-bg-light">
|
<Icon icon="clipboard" />
|
||||||
{numToSI(this.unreadApplicationCount)}
|
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
|
||||||
|
{I18NextService.i18n.t(
|
||||||
|
"unread_registration_applications",
|
||||||
|
{
|
||||||
|
count: Number(this.unreadApplicationCount),
|
||||||
|
formattedCount: numToSI(
|
||||||
|
this.unreadApplicationCount
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
{this.unreadApplicationCount > 0 && (
|
||||||
|
<span className="mx-1 badge text-bg-light">
|
||||||
|
{numToSI(this.unreadApplicationCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{person && (
|
||||||
|
<li id="dropdownUser" className="dropdown">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn dropdown-toggle"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
>
|
||||||
|
{showAvatars() && person.avatar && (
|
||||||
|
<PictrsImage src={person.avatar} icon />
|
||||||
|
)}
|
||||||
|
{person.display_name ?? person.name}
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu"
|
||||||
|
style={{ "min-width": "fit-content" }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to={`/u/${person.name}`}
|
||||||
|
className="dropdown-item px-2"
|
||||||
|
title={I18NextService.i18n.t("profile")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
<Icon icon="user" classes="me-1" />
|
||||||
|
{I18NextService.i18n.t("profile")}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/settings"
|
||||||
|
className="dropdown-item px-2"
|
||||||
|
title={I18NextService.i18n.t("settings")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
<Icon icon="settings" classes="me-1" />
|
||||||
|
{I18NextService.i18n.t("settings")}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="dropdown-item btn btn-link px-2"
|
||||||
|
onClick={linkEvent(this, handleLogOut)}
|
||||||
|
>
|
||||||
|
<Icon icon="log-out" classes="me-1" />
|
||||||
|
{I18NextService.i18n.t("logout")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
to="/login"
|
||||||
|
className="nav-link"
|
||||||
|
title={I18NextService.i18n.t("login")}
|
||||||
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("login")}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
)}
|
<li className="nav-item">
|
||||||
{person && (
|
<NavLink
|
||||||
<li id="dropdownUser" className="dropdown">
|
to="/signup"
|
||||||
<button
|
className="nav-link"
|
||||||
type="button"
|
title={I18NextService.i18n.t("sign_up")}
|
||||||
className="btn dropdown-toggle"
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
aria-expanded="false"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
>
|
||||||
{showAvatars() && person.avatar && (
|
{I18NextService.i18n.t("sign_up")}
|
||||||
<PictrsImage src={person.avatar} icon />
|
</NavLink>
|
||||||
)}
|
|
||||||
{person.display_name ?? person.name}
|
|
||||||
</button>
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu"
|
|
||||||
style={{ "min-width": "fit-content" }}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<NavLink
|
|
||||||
to={`/u/${person.name}`}
|
|
||||||
className="dropdown-item px-2"
|
|
||||||
title={I18NextService.i18n.t("profile")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="user" classes="me-1" />
|
|
||||||
{I18NextService.i18n.t("profile")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NavLink
|
|
||||||
to="/settings"
|
|
||||||
className="dropdown-item px-2"
|
|
||||||
title={I18NextService.i18n.t("settings")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
<Icon icon="settings" classes="me-1" />
|
|
||||||
{I18NextService.i18n.t("settings")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<hr className="dropdown-divider" />
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className="dropdown-item btn btn-link px-2"
|
|
||||||
onClick={linkEvent(this, handleLogOut)}
|
|
||||||
>
|
|
||||||
<Icon icon="log-out" classes="me-1" />
|
|
||||||
{I18NextService.i18n.t("logout")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
</li>
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
) : (
|
</ul>
|
||||||
<>
|
</div>
|
||||||
<li className="nav-item">
|
</nav>
|
||||||
<NavLink
|
</div>
|
||||||
to="/login"
|
|
||||||
className="nav-link"
|
|
||||||
title={I18NextService.i18n.t("login")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("login")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
to="/signup"
|
|
||||||
className="nav-link"
|
|
||||||
title={I18NextService.i18n.t("sign_up")}
|
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("sign_up")}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { myAuthRequired } from "@utils/app";
|
import { myAuthRequired } from "@utils/app";
|
||||||
import getUserInterfaceLangId from "@utils/app/user-interface-language";
|
|
||||||
import { capitalizeFirstLetter } from "@utils/helpers";
|
import { capitalizeFirstLetter } from "@utils/helpers";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
|
@ -41,8 +40,6 @@ export class CommentForm extends Component<CommentFormProps, any> {
|
||||||
: undefined
|
: undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const userInterfaceLangId = getUserInterfaceLangId(this.props.allLanguages);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={["comment-form", "mb-3", this.props.containerClass].join(
|
className={["comment-form", "mb-3", this.props.containerClass].join(
|
||||||
|
@ -52,7 +49,6 @@ export class CommentForm extends Component<CommentFormProps, any> {
|
||||||
{UserService.Instance.myUserInfo ? (
|
{UserService.Instance.myUserInfo ? (
|
||||||
<MarkdownTextArea
|
<MarkdownTextArea
|
||||||
initialContent={initialContent}
|
initialContent={initialContent}
|
||||||
initialLanguageId={userInterfaceLangId}
|
|
||||||
showLanguage
|
showLanguage
|
||||||
buttonTitle={this.buttonTitle}
|
buttonTitle={this.buttonTitle}
|
||||||
finished={this.props.finished}
|
finished={this.props.finished}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
colorList,
|
colorList,
|
||||||
getCommentParentId,
|
getCommentParentId,
|
||||||
|
getRoleLabelPill,
|
||||||
myAuth,
|
myAuth,
|
||||||
myAuthRequired,
|
myAuthRequired,
|
||||||
showScores,
|
showScores,
|
||||||
|
@ -308,32 +309,43 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
classes="icon-inline"
|
classes="icon-inline"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="me-2">
|
<span className="me-2">
|
||||||
<PersonListing person={cv.creator} />
|
<PersonListing person={cv.creator} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{cv.comment.distinguished && (
|
{cv.comment.distinguished && (
|
||||||
<Icon icon="shield" inline classes="text-danger me-2" />
|
<Icon icon="shield" inline classes="text-danger me-2" />
|
||||||
)}
|
)}
|
||||||
{this.isPostCreator && (
|
|
||||||
<div className="badge text-bg-light d-none d-sm-inline me-2">
|
{this.isPostCreator &&
|
||||||
{I18NextService.i18n.t("creator")}
|
getRoleLabelPill({
|
||||||
</div>
|
label: I18NextService.i18n.t("op").toUpperCase(),
|
||||||
)}
|
tooltip: I18NextService.i18n.t("creator"),
|
||||||
{isMod_ && (
|
classes: "text-bg-info",
|
||||||
<div className="badge text-bg-light d-none d-sm-inline me-2">
|
shrink: false,
|
||||||
{I18NextService.i18n.t("mod")}
|
})}
|
||||||
</div>
|
|
||||||
)}
|
{isMod_ &&
|
||||||
{isAdmin_ && (
|
getRoleLabelPill({
|
||||||
<div className="badge text-bg-light d-none d-sm-inline me-2">
|
label: I18NextService.i18n.t("mod"),
|
||||||
{I18NextService.i18n.t("admin")}
|
tooltip: I18NextService.i18n.t("mod"),
|
||||||
</div>
|
classes: "text-bg-primary",
|
||||||
)}
|
})}
|
||||||
{cv.creator.bot_account && (
|
|
||||||
<div className="badge text-bg-light d-none d-sm-inline me-2">
|
{isAdmin_ &&
|
||||||
{I18NextService.i18n.t("bot_account").toLowerCase()}
|
getRoleLabelPill({
|
||||||
</div>
|
label: I18NextService.i18n.t("admin"),
|
||||||
)}
|
tooltip: I18NextService.i18n.t("admin"),
|
||||||
|
classes: "text-bg-danger",
|
||||||
|
})}
|
||||||
|
|
||||||
|
{cv.creator.bot_account &&
|
||||||
|
getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n.t("bot_account").toLowerCase(),
|
||||||
|
tooltip: I18NextService.i18n.t("bot_account"),
|
||||||
|
})}
|
||||||
|
|
||||||
{this.props.showCommunity && (
|
{this.props.showCommunity && (
|
||||||
<>
|
<>
|
||||||
<span className="mx-1">{I18NextService.i18n.t("to")}</span>
|
<span className="mx-1">{I18NextService.i18n.t("to")}</span>
|
||||||
|
@ -344,7 +356,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{this.linkBtn(true)}
|
|
||||||
|
{this.getLinkButton(true)}
|
||||||
|
|
||||||
{cv.comment.language_id !== 0 && (
|
{cv.comment.language_id !== 0 && (
|
||||||
<span className="badge text-bg-light d-none d-sm-inline me-2">
|
<span className="badge text-bg-light d-none d-sm-inline me-2">
|
||||||
{
|
{
|
||||||
|
@ -410,7 +424,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
|
<div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
|
||||||
{this.props.showContext && this.linkBtn()}
|
{this.props.showContext && this.getLinkButton()}
|
||||||
{this.props.markable && (
|
{this.props.markable && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted"
|
className="btn btn-link btn-animate text-muted"
|
||||||
|
@ -1186,7 +1200,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
linkBtn(small = false) {
|
getLinkButton(small = false) {
|
||||||
const cv = this.commentView;
|
const cv = this.commentView;
|
||||||
|
|
||||||
const classnames = classNames("btn btn-link btn-animate text-muted", {
|
const classnames = classNames("btn btn-link btn-animate text-muted", {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
@ -23,7 +24,9 @@ export class Icon extends Component<IconProps, any> {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<use
|
<use
|
||||||
xlinkHref={`/static/assets/symbols.svg#icon-${this.props.icon}`}
|
xlinkHref={`${getStaticDir()}/assets/symbols.svg#icon-${
|
||||||
|
this.props.icon
|
||||||
|
}`}
|
||||||
></use>
|
></use>
|
||||||
<div className="visually-hidden">
|
<div className="visually-hidden">
|
||||||
<title>{this.props.icon}</title>
|
<title>{this.props.icon}</title>
|
||||||
|
|
|
@ -80,6 +80,8 @@ export class ImageUploadForm extends Component<
|
||||||
if (res.state === "success") {
|
if (res.state === "success") {
|
||||||
if (res.data.msg === "ok") {
|
if (res.data.msg === "ok") {
|
||||||
i.props.onUpload(res.data.url as string);
|
i.props.onUpload(res.data.url as string);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
return this.props.iconVersion ? (
|
return this.props.iconVersion ? (
|
||||||
this.selectBtn
|
this.selectBtn
|
||||||
) : (
|
) : (
|
||||||
<div className="language-select row mb-3">
|
<div className="language-select mb-3">
|
||||||
<label
|
<label
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"col-form-label",
|
"col-form-label",
|
||||||
|
|
|
@ -159,13 +159,16 @@ export class MarkdownTextArea extends Component<
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="rounded bg-light border">
|
<div className="rounded bg-light border">
|
||||||
<div className="d-flex flex-wrap border-bottom">
|
<div
|
||||||
|
className={classNames("d-flex flex-wrap border-bottom", {
|
||||||
|
"no-click": this.isDisabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{this.getFormatButton("bold", this.handleInsertBold)}
|
{this.getFormatButton("bold", this.handleInsertBold)}
|
||||||
{this.getFormatButton("italic", this.handleInsertItalic)}
|
{this.getFormatButton("italic", this.handleInsertItalic)}
|
||||||
{this.getFormatButton("link", this.handleInsertLink)}
|
{this.getFormatButton("link", this.handleInsertLink)}
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onEmojiClick={e => this.handleEmoji(this, e)}
|
onEmojiClick={e => this.handleEmoji(this, e)}
|
||||||
disabled={this.isDisabled}
|
|
||||||
></EmojiPicker>
|
></EmojiPicker>
|
||||||
<form className="btn btn-sm text-muted fw-bold">
|
<form className="btn btn-sm text-muted fw-bold">
|
||||||
<label
|
<label
|
||||||
|
@ -191,9 +194,7 @@ export class MarkdownTextArea extends Component<
|
||||||
name="file"
|
name="file"
|
||||||
className="d-none"
|
className="d-none"
|
||||||
multiple
|
multiple
|
||||||
disabled={
|
disabled={!UserService.Instance.myUserInfo}
|
||||||
!UserService.Instance.myUserInfo || this.isDisabled
|
|
||||||
}
|
|
||||||
onChange={linkEvent(this, this.handleImageUpload)}
|
onChange={linkEvent(this, this.handleImageUpload)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@ -276,12 +277,8 @@ export class MarkdownTextArea extends Component<
|
||||||
<LanguageSelect
|
<LanguageSelect
|
||||||
iconVersion
|
iconVersion
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
// Only set the selected language ID if it exists as an option
|
|
||||||
// in the dropdown; otherwise, set it to 0 (Undetermined)
|
|
||||||
selectedLanguageIds={
|
selectedLanguageIds={
|
||||||
languageId && this.props.siteLanguages.includes(languageId)
|
languageId ? Array.of(languageId) : undefined
|
||||||
? [languageId]
|
|
||||||
: [0]
|
|
||||||
}
|
}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
onChange={this.handleLanguageChange}
|
onChange={this.handleLanguageChange}
|
||||||
|
@ -355,7 +352,6 @@ export class MarkdownTextArea extends Component<
|
||||||
data-tippy-content={I18NextService.i18n.t(type)}
|
data-tippy-content={I18NextService.i18n.t(type)}
|
||||||
aria-label={I18NextService.i18n.t(type)}
|
aria-label={I18NextService.i18n.t(type)}
|
||||||
onClick={linkEvent(this, handleClick)}
|
onClick={linkEvent(this, handleClick)}
|
||||||
disabled={this.isDisabled}
|
|
||||||
>
|
>
|
||||||
<Icon icon={iconType} classes="icon-inline" />
|
<Icon icon={iconType} classes="icon-inline" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -450,6 +446,10 @@ export class MarkdownTextArea extends Component<
|
||||||
const textarea: any = document.getElementById(i.id);
|
const textarea: any = document.getElementById(i.id);
|
||||||
autosize.update(textarea);
|
autosize.update(textarea);
|
||||||
pictrsDeleteToast(image.name, res.data.delete_url as string);
|
pictrsDeleteToast(image.name, res.data.delete_url as string);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
|
i.setState({ imageUploadStatus: undefined });
|
||||||
|
throw JSON.stringify(res.data);
|
||||||
} else {
|
} else {
|
||||||
throw JSON.stringify(res.data);
|
throw JSON.stringify(res.data);
|
||||||
}
|
}
|
||||||
|
@ -476,7 +476,7 @@ export class MarkdownTextArea extends Component<
|
||||||
// Keybind handler
|
// Keybind handler
|
||||||
// Keybinds inspired by github comment area
|
// Keybinds inspired by github comment area
|
||||||
handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
|
handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "k": {
|
case "k": {
|
||||||
i.handleInsertLink(i, event);
|
i.handleInsertLink(i, event);
|
||||||
|
@ -705,18 +705,20 @@ export class MarkdownTextArea extends Component<
|
||||||
quoteInsert() {
|
quoteInsert() {
|
||||||
const textarea: any = document.getElementById(this.id);
|
const textarea: any = document.getElementById(this.id);
|
||||||
const selectedText = window.getSelection()?.toString();
|
const selectedText = window.getSelection()?.toString();
|
||||||
const { content } = this.state;
|
let { content } = this.state;
|
||||||
if (selectedText) {
|
if (selectedText) {
|
||||||
const quotedText =
|
const quotedText =
|
||||||
selectedText
|
selectedText
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(t => `> ${t}`)
|
.map(t => `> ${t}`)
|
||||||
.join("\n") + "\n\n";
|
.join("\n") + "\n\n";
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
this.setState({ content: "" });
|
content = "";
|
||||||
} else {
|
} else {
|
||||||
this.setState({ content: `${content}\n` });
|
content = `${content}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
content: `${content}${quotedText}`,
|
content: `${content}${quotedText}`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
||||||
import format from "date-fns/format";
|
import { format } from "date-fns";
|
||||||
import parseISO from "date-fns/parseISO";
|
import parseISO from "date-fns/parseISO";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
@ -13,7 +13,8 @@ interface MomentTimeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(input: string) {
|
function formatDate(input: string) {
|
||||||
return format(parseISO(input), "PPPPpppp");
|
const parsed = parseISO(input + "Z");
|
||||||
|
return format(parsed, "PPPPpppp");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
"img-expanded slight-radius":
|
"img-expanded slight-radius":
|
||||||
!this.props.thumbnail && !this.props.icon,
|
!this.props.thumbnail && !this.props.icon,
|
||||||
"img-blur": this.props.thumbnail && this.props.nsfw,
|
"img-blur": this.props.thumbnail && this.props.nsfw,
|
||||||
"rounded-circle img-cover img-icon me-2": this.props.icon,
|
"img-cover img-icon me-1": this.props.icon,
|
||||||
"ms-2 mb-0 rounded-circle img-cover avatar-overlay":
|
"ms-2 mb-0 rounded-circle img-cover avatar-overlay":
|
||||||
this.props.iconOverlay,
|
this.props.iconOverlay,
|
||||||
"avatar-pushup": this.props.pushup,
|
"avatar-pushup": this.props.pushup,
|
||||||
|
|
|
@ -174,7 +174,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="vote-bar col-1 pe-0 small text-center">
|
<div className="vote-bar small text-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn-animate btn btn-link p-0 ${
|
className={`btn-animate btn btn-link p-0 ${
|
||||||
|
@ -193,7 +193,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
||||||
</button>
|
</button>
|
||||||
{showScores() ? (
|
{showScores() ? (
|
||||||
<div
|
<div
|
||||||
className="unselectable pointer text-muted px-1 post-score"
|
className="unselectable pointer text-muted post-score"
|
||||||
data-tippy-content={tippy(this.props.counts)}
|
data-tippy-content={tippy(this.props.counts)}
|
||||||
>
|
>
|
||||||
{numToSI(this.props.counts.score)}
|
{numToSI(this.props.counts.score)}
|
||||||
|
|
|
@ -284,7 +284,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
handleSearchSubmit(i: Communities, event: any) {
|
handleSearchSubmit(i: Communities, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
||||||
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
i.context.router.history.push(
|
||||||
|
`/search?q=${searchParamEncoded}&type=Communities`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
|
|
|
@ -317,7 +317,10 @@ export class Community extends Component<
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main className="col-12 col-md-8" ref={this.mainContentRef}>
|
<main
|
||||||
|
className="col-12 col-md-8 col-lg-9"
|
||||||
|
ref={this.mainContentRef}
|
||||||
|
>
|
||||||
{this.communityInfo(res)}
|
{this.communityInfo(res)}
|
||||||
<div className="d-block d-md-none">
|
<div className="d-block d-md-none">
|
||||||
<button
|
<button
|
||||||
|
@ -340,7 +343,7 @@ export class Community extends Component<
|
||||||
{this.listings(res)}
|
{this.listings(res)}
|
||||||
<Paginator page={page} onChange={this.handlePageChange} />
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
</main>
|
</main>
|
||||||
<aside className="d-none d-md-block col-md-4">
|
<aside className="d-none d-md-block col-md-4 col-lg-3">
|
||||||
{this.sidebar(res)}
|
{this.sidebar(res)}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -166,7 +166,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
communityTitle() {
|
communityTitle() {
|
||||||
const community = this.props.community_view.community;
|
const community = this.props.community_view.community;
|
||||||
const subscribed = this.props.community_view.subscribed;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
|
@ -176,33 +176,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<span className="me-2">
|
<span className="me-2">
|
||||||
<CommunityLink community={community} hideAvatar />
|
<CommunityLink community={community} hideAvatar />
|
||||||
</span>
|
</span>
|
||||||
{subscribed === "Subscribed" && (
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary btn-sm me-2"
|
|
||||||
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.followCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon icon="check" classes="icon-inline text-success me-1" />
|
|
||||||
{I18NextService.i18n.t("joined")}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{subscribed === "Pending" && (
|
|
||||||
<button
|
|
||||||
className="btn btn-warning me-2"
|
|
||||||
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.followCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
I18NextService.i18n.t("subscribe_pending")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{community.removed && (
|
{community.removed && (
|
||||||
<small className="me-2 text-muted fst-italic">
|
<small className="me-2 text-muted fst-italic">
|
||||||
{I18NextService.i18n.t("removed")}
|
{I18NextService.i18n.t("removed")}
|
||||||
|
@ -259,40 +232,70 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
subscribe() {
|
subscribe() {
|
||||||
const community_view = this.props.community_view;
|
const community_view = this.props.community_view;
|
||||||
return (
|
|
||||||
<>
|
if (community_view.subscribed === "NotSubscribed") {
|
||||||
{community_view.subscribed == "NotSubscribed" && (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary d-block mb-2 w-100"
|
className="btn btn-secondary d-block mb-2 w-100"
|
||||||
onClick={linkEvent(this, this.handleFollowCommunity)}
|
onClick={linkEvent(this, this.handleFollowCommunity)}
|
||||||
>
|
>
|
||||||
{this.state.followCommunityLoading ? (
|
{this.state.followCommunityLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("subscribe")
|
I18NextService.i18n.t("subscribe")
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
);
|
||||||
</>
|
}
|
||||||
);
|
|
||||||
|
if (community_view.subscribed === "Subscribed") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary d-block mb-2 w-100"
|
||||||
|
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
||||||
|
>
|
||||||
|
{this.state.followCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon="check" classes="icon-inline text-success me-1" />
|
||||||
|
{I18NextService.i18n.t("joined")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (community_view.subscribed === "Pending") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-warning d-block mb-2 w-100"
|
||||||
|
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
||||||
|
>
|
||||||
|
{this.state.followCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
I18NextService.i18n.t("subscribe_pending")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockCommunity() {
|
blockCommunity() {
|
||||||
const { subscribed, blocked } = this.props.community_view;
|
const { subscribed, blocked } = this.props.community_view;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
subscribed === "NotSubscribed" && (
|
||||||
{subscribed == "NotSubscribed" && (
|
<button
|
||||||
<button
|
className="btn btn-danger d-block mb-2 w-100"
|
||||||
className="btn btn-danger d-block mb-2 w-100"
|
onClick={linkEvent(this, this.handleBlockCommunity)}
|
||||||
onClick={linkEvent(this, this.handleBlockCommunity)}
|
>
|
||||||
>
|
{I18NextService.i18n.t(
|
||||||
{I18NextService.i18n.t(
|
blocked ? "unblock_community" : "block_community"
|
||||||
blocked ? "unblock_community" : "block_community"
|
)}
|
||||||
)}
|
</button>
|
||||||
</button>
|
)
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -512,6 +512,8 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||||
{ form: form, index: index, overrideValue: res.data.url as string },
|
{ form: form, index: index, overrideValue: res.data.url as string },
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,13 +279,15 @@ export class Home extends Component<any, HomeState> {
|
||||||
trendingCommunitiesRes,
|
trendingCommunitiesRes,
|
||||||
commentsRes,
|
commentsRes,
|
||||||
postsRes,
|
postsRes,
|
||||||
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
|
|
||||||
?.content,
|
|
||||||
isIsomorphic: true,
|
isIsomorphic: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
HomeCacheService.postsRes = postsRes;
|
HomeCacheService.postsRes = postsRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.tagline = getRandomFromList(
|
||||||
|
this.state?.siteRes?.taglines ?? []
|
||||||
|
)?.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -387,7 +389,7 @@ export class Home extends Component<any, HomeState> {
|
||||||
/>
|
/>
|
||||||
{site_setup && (
|
{site_setup && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main role="main" className="col-12 col-md-8">
|
<main role="main" className="col-12 col-md-8 col-lg-9">
|
||||||
{tagline && (
|
{tagline && (
|
||||||
<div
|
<div
|
||||||
id="tagline"
|
id="tagline"
|
||||||
|
@ -397,7 +399,7 @@ export class Home extends Component<any, HomeState> {
|
||||||
<div className="d-block d-md-none">{this.mobileView}</div>
|
<div className="d-block d-md-none">{this.mobileView}</div>
|
||||||
{this.posts}
|
{this.posts}
|
||||||
</main>
|
</main>
|
||||||
<aside className="d-none d-md-block col-md-4">
|
<aside className="d-none d-md-block col-md-4 col-lg-3">
|
||||||
{this.mySidebar}
|
{this.mySidebar}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -205,9 +205,7 @@ export class Setup extends Component<any, State> {
|
||||||
const data = i.state.registerRes.data;
|
const data = i.state.registerRes.data;
|
||||||
|
|
||||||
UserService.Instance.login(data);
|
UserService.Instance.login(data);
|
||||||
if (UserService.Instance.jwtInfo) {
|
i.setState({ doneRegisteringUser: true });
|
||||||
i.setState({ doneRegisteringUser: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
Component,
|
Component,
|
||||||
InfernoKeyboardEvent,
|
InfernoKeyboardEvent,
|
||||||
InfernoMouseEvent,
|
InfernoMouseEvent,
|
||||||
|
InfernoNode,
|
||||||
linkEvent,
|
linkEvent,
|
||||||
} from "inferno";
|
} from "inferno";
|
||||||
import {
|
import {
|
||||||
|
@ -13,6 +14,7 @@ import {
|
||||||
Instance,
|
Instance,
|
||||||
ListingType,
|
ListingType,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
|
import deepEqual from "lodash.isequal";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { ImageUploadForm } from "../common/image-upload-form";
|
import { ImageUploadForm } from "../common/image-upload-form";
|
||||||
|
@ -55,6 +57,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
initSiteForm(): EditSite {
|
initSiteForm(): EditSite {
|
||||||
const site = this.props.siteRes.site_view.site;
|
const site = this.props.siteRes.site_view.site;
|
||||||
const ls = this.props.siteRes.site_view.local_site;
|
const ls = this.props.siteRes.site_view.local_site;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
sidebar: site.sidebar,
|
sidebar: site.sidebar,
|
||||||
|
@ -623,6 +626,19 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(
|
||||||
|
prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps>
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
deepEqual(prevProps.allowedInstances, this.props.allowedInstances) ||
|
||||||
|
deepEqual(prevProps.blockedInstances, this.props.blockedInstances)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.setState({ siteForm: this.initSiteForm() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
federatedInstanceSelect(key: InstanceKey) {
|
federatedInstanceSelect(key: InstanceKey) {
|
||||||
const id = `create_site_${key}`;
|
const id = `create_site_${key}`;
|
||||||
const value = this.state.instance_select[key];
|
const value = this.state.instance_select[key];
|
||||||
|
|
|
@ -141,7 +141,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
|
|
||||||
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.state.editingRow == d.index) {
|
if (d.i.state.editingRow == d.index) {
|
||||||
d.i.setState({ editingRow: undefined });
|
d.i.setState({ editingRow: undefined });
|
||||||
} else {
|
} else {
|
||||||
d.i.setState({ editingRow: d.index });
|
d.i.setState({ editingRow: d.index });
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { showAvatars } from "@utils/app";
|
import { showAvatars } from "@utils/app";
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import { hostname, isCakeDay } from "@utils/helpers";
|
import { hostname, isCakeDay } from "@utils/helpers";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
|
@ -88,7 +89,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
!this.props.person.banned &&
|
!this.props.person.banned &&
|
||||||
showAvatars() && (
|
showAvatars() && (
|
||||||
<PictrsImage
|
<PictrsImage
|
||||||
src={avatar ?? "/static/assets/icons/icon-96x96.png"}
|
src={avatar ?? `${getStaticDir()}/assets/icons/icon-96x96.png`}
|
||||||
icon
|
icon
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
enableDownvotes,
|
enableDownvotes,
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
getCommentParentId,
|
getCommentParentId,
|
||||||
|
getRoleLabelPill,
|
||||||
myAuth,
|
myAuth,
|
||||||
myAuthRequired,
|
myAuthRequired,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
|
@ -484,23 +485,43 @@ export class Profile extends Component<
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
{isBanned(pv.person) && (
|
{isBanned(pv.person) && (
|
||||||
<li className="list-inline-item badge text-bg-danger">
|
<li className="list-inline-item">
|
||||||
{I18NextService.i18n.t("banned")}
|
{getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n.t("banned"),
|
||||||
|
tooltip: I18NextService.i18n.t("banned"),
|
||||||
|
classes: "text-bg-danger",
|
||||||
|
shrink: false,
|
||||||
|
})}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{pv.person.deleted && (
|
{pv.person.deleted && (
|
||||||
<li className="list-inline-item badge text-bg-danger">
|
<li className="list-inline-item">
|
||||||
{I18NextService.i18n.t("deleted")}
|
{getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n.t("deleted"),
|
||||||
|
tooltip: I18NextService.i18n.t("deleted"),
|
||||||
|
classes: "text-bg-danger",
|
||||||
|
shrink: false,
|
||||||
|
})}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{pv.person.admin && (
|
{pv.person.admin && (
|
||||||
<li className="list-inline-item badge text-bg-light">
|
<li className="list-inline-item">
|
||||||
{I18NextService.i18n.t("admin")}
|
{getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n.t("admin"),
|
||||||
|
tooltip: I18NextService.i18n.t("admin"),
|
||||||
|
shrink: false,
|
||||||
|
})}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{pv.person.bot_account && (
|
{pv.person.bot_account && (
|
||||||
<li className="list-inline-item badge text-bg-light">
|
<li className="list-inline-item">
|
||||||
{I18NextService.i18n.t("bot_account").toLowerCase()}
|
{getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n
|
||||||
|
.t("bot_account")
|
||||||
|
.toLowerCase(),
|
||||||
|
tooltip: I18NextService.i18n.t("bot_account"),
|
||||||
|
shrink: false,
|
||||||
|
})}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -692,6 +713,8 @@ export class Profile extends Component<
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("cancel")}
|
{I18NextService.i18n.t("cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 row">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
|
|
|
@ -8,60 +8,54 @@ interface MetadataCardProps {
|
||||||
post: Post;
|
post: Post;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetadataCardState {
|
export class MetadataCard extends Component<MetadataCardProps> {
|
||||||
expanded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MetadataCard extends Component<
|
|
||||||
MetadataCardProps,
|
|
||||||
MetadataCardState
|
|
||||||
> {
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
return (
|
|
||||||
<>
|
if (post.embed_title && post.url) {
|
||||||
{post.embed_title && post.url && (
|
return (
|
||||||
<div className="post-metadata-card card border-secondary mt-3 mb-2">
|
<div className="post-metadata-card card border-secondary mt-3 mb-2">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{post.name !== post.embed_title && (
|
{post.name !== post.embed_title && (
|
||||||
<>
|
<>
|
||||||
<h5 className="card-title d-inline">
|
<h5 className="card-title d-inline">
|
||||||
<a className="text-body" href={post.url} rel={relTags}>
|
<a className="text-body" href={post.url} rel={relTags}>
|
||||||
{post.embed_title}
|
{post.embed_title}
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
<span className="d-inline-block ms-2 mb-2 small text-muted">
|
<span className="d-inline-block ms-2 mb-2 small text-muted">
|
||||||
<a
|
<a
|
||||||
className="text-muted fst-italic"
|
className="text-muted fst-italic"
|
||||||
href={post.url}
|
href={post.url}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
>
|
>
|
||||||
{new URL(post.url).hostname}
|
{new URL(post.url).hostname}
|
||||||
<Icon icon="external-link" classes="ms-1" />
|
<Icon icon="external-link" classes="ms-1" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{post.embed_description && (
|
{post.embed_description && (
|
||||||
<div
|
<div
|
||||||
className="card-text small text-muted md-div"
|
className="card-text small text-muted md-div"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: sanitizeHtml(post.embed_description),
|
__html: sanitizeHtml(post.embed_description),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</>
|
);
|
||||||
);
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
myAuth,
|
myAuth,
|
||||||
myAuthRequired,
|
myAuthRequired,
|
||||||
} from "@utils/app";
|
} from "@utils/app";
|
||||||
import getUserInterfaceLangId from "@utils/app/user-interface-language";
|
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
debounce,
|
debounce,
|
||||||
|
@ -188,6 +187,8 @@ function handleImageUpload(i: PostForm, event: any) {
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
imageDeleteUrl: res.data.delete_url as string,
|
imageDeleteUrl: res.data.delete_url as string,
|
||||||
});
|
});
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
@ -324,9 +325,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const url = this.state.form.url;
|
const firstLang = this.state.form.language_id;
|
||||||
|
const selectedLangs = firstLang ? Array.of(firstLang) : undefined;
|
||||||
|
|
||||||
const userInterfaceLangId = getUserInterfaceLangId(this.props.allLanguages);
|
const url = this.state.form.url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}>
|
<form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}>
|
||||||
|
@ -493,8 +495,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
</div>
|
</div>
|
||||||
<LanguageSelect
|
<LanguageSelect
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
selectedLanguageIds={[userInterfaceLangId]}
|
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
selectedLanguageIds={selectedLangs}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
onChange={this.handleLanguageChange}
|
onChange={this.handleLanguageChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { myAuthRequired } from "@utils/app";
|
import { getRoleLabelPill, myAuthRequired } from "@utils/app";
|
||||||
import { canShare, share } from "@utils/browser";
|
import { canShare, share } from "@utils/browser";
|
||||||
import { getExternalHost, getHttpBase } from "@utils/env";
|
import { getExternalHost, getHttpBase } from "@utils/env";
|
||||||
import {
|
import {
|
||||||
|
@ -49,7 +49,7 @@ import {
|
||||||
PurgeType,
|
PurgeType,
|
||||||
VoteContentType,
|
VoteContentType,
|
||||||
} from "../../interfaces";
|
} from "../../interfaces";
|
||||||
import { mdNoImages, mdToHtml, mdToHtmlInline } from "../../markdown";
|
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { setupTippy } from "../../tippy";
|
import { setupTippy } from "../../tippy";
|
||||||
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
||||||
|
@ -105,6 +105,9 @@ interface PostListingProps {
|
||||||
allLanguages: Language[];
|
allLanguages: Language[];
|
||||||
siteLanguages: number[];
|
siteLanguages: number[];
|
||||||
showCommunity?: boolean;
|
showCommunity?: boolean;
|
||||||
|
/**
|
||||||
|
* Controls whether to show both the body *and* the metadata preview card
|
||||||
|
*/
|
||||||
showBody?: boolean;
|
showBody?: boolean;
|
||||||
hideImage?: boolean;
|
hideImage?: boolean;
|
||||||
enableDownvotes?: boolean;
|
enableDownvotes?: boolean;
|
||||||
|
@ -183,7 +186,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
addModLoading: false,
|
addModLoading: false,
|
||||||
addAdminLoading: false,
|
addAdminLoading: false,
|
||||||
transferLoading: false,
|
transferLoading: false,
|
||||||
imageExpanded: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +203,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
{this.listing()}
|
{this.listing()}
|
||||||
{this.state.imageExpanded && !this.props.hideImage && this.img}
|
{this.state.imageExpanded && !this.props.hideImage && this.img}
|
||||||
{post.url && this.state.showBody && post.embed_title && (
|
{this.showBody && post.url && post.embed_title && (
|
||||||
<MetadataCard post={post} />
|
<MetadataCard post={post} />
|
||||||
)}
|
)}
|
||||||
{this.showBody && this.body()}
|
{this.showBody && this.body()}
|
||||||
|
@ -329,27 +331,33 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
|
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
|
||||||
return (
|
return (
|
||||||
<a
|
<button
|
||||||
href={this.imageSrc}
|
type="button"
|
||||||
className="text-body d-inline-block position-relative mb-2"
|
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0 bg-transparent"
|
||||||
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
aria-label={I18NextService.i18n.t("expand_here")}
|
aria-label={I18NextService.i18n.t("expand_here")}
|
||||||
>
|
>
|
||||||
{this.imgThumb(this.imageSrc)}
|
{this.imgThumb(this.imageSrc)}
|
||||||
<Icon icon="image" classes="mini-overlay" />
|
<Icon
|
||||||
</a>
|
icon="image"
|
||||||
|
classes="d-block text-white position-absolute end-0 top-0 mini-overlay text-opacity-75 text-opacity-100-hover"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
} else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
|
} else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="text-body d-inline-block position-relative mb-2"
|
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0"
|
||||||
href={url}
|
href={url}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
title={url}
|
title={url}
|
||||||
>
|
>
|
||||||
{this.imgThumb(this.imageSrc)}
|
{this.imgThumb(this.imageSrc)}
|
||||||
<Icon icon="external-link" classes="mini-overlay" />
|
<Icon
|
||||||
|
icon="external-link"
|
||||||
|
classes="d-block text-white position-absolute end-0 top-0 mini-overlay text-opacity-75 text-opacity-100-hover"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
|
@ -395,24 +403,29 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
createdLine() {
|
createdLine() {
|
||||||
const post_view = this.postView;
|
const post_view = this.postView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="small">
|
<div className="small mb-1 mb-md-0">
|
||||||
<PersonListing person={post_view.creator} muted={true} />
|
<span className="me-1">
|
||||||
{this.creatorIsMod_ && (
|
<PersonListing person={post_view.creator} />
|
||||||
<span className="mx-1 badge text-bg-light">
|
</span>
|
||||||
{I18NextService.i18n.t("mod")}
|
{this.creatorIsMod_ &&
|
||||||
</span>
|
getRoleLabelPill({
|
||||||
)}
|
label: I18NextService.i18n.t("mod"),
|
||||||
{this.creatorIsAdmin_ && (
|
tooltip: I18NextService.i18n.t("mod"),
|
||||||
<span className="mx-1 badge text-bg-light">
|
classes: "text-bg-primary",
|
||||||
{I18NextService.i18n.t("admin")}
|
})}
|
||||||
</span>
|
{this.creatorIsAdmin_ &&
|
||||||
)}
|
getRoleLabelPill({
|
||||||
{post_view.creator.bot_account && (
|
label: I18NextService.i18n.t("admin"),
|
||||||
<span className="mx-1 badge text-bg-light">
|
tooltip: I18NextService.i18n.t("admin"),
|
||||||
{I18NextService.i18n.t("bot_account").toLowerCase()}
|
classes: "text-bg-danger",
|
||||||
</span>
|
})}
|
||||||
)}
|
{post_view.creator.bot_account &&
|
||||||
|
getRoleLabelPill({
|
||||||
|
label: I18NextService.i18n.t("bot_account").toLowerCase(),
|
||||||
|
tooltip: I18NextService.i18n.t("bot_account"),
|
||||||
|
})}
|
||||||
{this.props.showCommunity && (
|
{this.props.showCommunity && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
|
@ -434,7 +447,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
published={post_view.post.published}
|
published={post_view.post.published}
|
||||||
updated={post_view.post.updated}
|
updated={post_view.post.updated}
|
||||||
/>
|
/>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,6 +496,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
{/**
|
||||||
|
* If there is a URL, an embed title, and we were not told to show the
|
||||||
|
* body by the parent component, show the MetadataCard/body toggle.
|
||||||
|
*/}
|
||||||
|
{!this.props.showBody &&
|
||||||
|
post.url &&
|
||||||
|
post.embed_title &&
|
||||||
|
this.showPreviewButton()}
|
||||||
|
|
||||||
{post.removed && (
|
{post.removed && (
|
||||||
<small className="ms-2 badge text-bg-secondary">
|
<small className="ms-2 badge text-bg-secondary">
|
||||||
{I18NextService.i18n.t("removed")}
|
{I18NextService.i18n.t("removed")}
|
||||||
|
@ -625,27 +647,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showPreviewButton() {
|
|
||||||
const post_view = this.postView;
|
|
||||||
const body = post_view.post.body;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-animate text-muted py-0"
|
|
||||||
data-tippy-content={body && mdNoImages.render(body)}
|
|
||||||
data-tippy-allowHtml={true}
|
|
||||||
onClick={linkEvent(this, this.handleShowBody)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon="book-open"
|
|
||||||
classes={classNames("icon-inline me-1", {
|
|
||||||
"text-success": this.state.showBody,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
postActions() {
|
postActions() {
|
||||||
// Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
|
// Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
|
||||||
// Possible enhancement: Make each button a component.
|
// Possible enhancement: Make each button a component.
|
||||||
|
@ -657,14 +658,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.saveButton}
|
{this.saveButton}
|
||||||
{this.crossPostButton}
|
{this.crossPostButton}
|
||||||
|
|
||||||
{/**
|
{this.props.showBody && post_view.post.body && this.viewSourceButton}
|
||||||
* If there is a URL, or if the post has a body and we were told not to
|
|
||||||
* show the body, show the MetadataCard/body toggle.
|
|
||||||
*/}
|
|
||||||
{(post.url || (post.body && !this.props.showBody)) &&
|
|
||||||
this.showPreviewButton()}
|
|
||||||
|
|
||||||
{this.showBody && post_view.post.body && this.viewSourceButton}
|
|
||||||
|
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<button
|
<button
|
||||||
|
@ -709,6 +703,50 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{(this.canMod_ || this.canAdmin_) && (
|
{(this.canMod_ || this.canAdmin_) && (
|
||||||
<li>{this.modRemoveButton}</li>
|
<li>{this.modRemoveButton}</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.canMod_ && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!this.creatorIsMod_ &&
|
||||||
|
(!post_view.creator_banned_from_community ? (
|
||||||
|
<li>{this.modBanFromCommunityButton}</li>
|
||||||
|
) : (
|
||||||
|
<li>{this.modUnbanFromCommunityButton}</li>
|
||||||
|
))}
|
||||||
|
{!post_view.creator_banned_from_community && (
|
||||||
|
<li>{this.addModToCommunityButton}</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(amCommunityCreator(post_view.creator.id, this.props.moderators) ||
|
||||||
|
this.canAdmin_) &&
|
||||||
|
this.creatorIsMod_ && <li>{this.transferCommunityButton}</li>}
|
||||||
|
|
||||||
|
{/* Admins can ban from all, and appoint other admins */}
|
||||||
|
{this.canAdmin_ && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!this.creatorIsAdmin_ && (
|
||||||
|
<>
|
||||||
|
{!isBanned(post_view.creator) ? (
|
||||||
|
<li>{this.modBanButton}</li>
|
||||||
|
) : (
|
||||||
|
<li>{this.modUnbanButton}</li>
|
||||||
|
)}
|
||||||
|
<li>{this.purgePersonButton}</li>
|
||||||
|
<li>{this.purgePostButton}</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isBanned(post_view.creator) && post_view.creator.local && (
|
||||||
|
<li>{this.toggleAdminButton}</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -976,9 +1014,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modBanFromCommunityButton() {
|
get modBanFromCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleModBanFromCommunityShow)}
|
onClick={linkEvent(this, this.handleModBanFromCommunityShow)}
|
||||||
aria-label={I18NextService.i18n.t("ban_from_community")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("ban_from_community")}
|
{I18NextService.i18n.t("ban_from_community")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -988,9 +1025,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modUnbanFromCommunityButton() {
|
get modUnbanFromCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleModBanFromCommunitySubmit)}
|
onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}
|
||||||
aria-label={I18NextService.i18n.t("unban")}
|
|
||||||
>
|
>
|
||||||
{this.state.banLoading ? <Spinner /> : I18NextService.i18n.t("unban")}
|
{this.state.banLoading ? <Spinner /> : I18NextService.i18n.t("unban")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1000,20 +1036,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get addModToCommunityButton() {
|
get addModToCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleAddModToCommunity)}
|
onClick={linkEvent(this, this.handleAddModToCommunity)}
|
||||||
aria-label={
|
|
||||||
this.creatorIsMod_
|
|
||||||
? I18NextService.i18n.t("remove_as_mod")
|
|
||||||
: I18NextService.i18n.t("appoint_as_mod")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{this.state.addModLoading ? (
|
{this.state.addModLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : this.creatorIsMod_ ? (
|
) : this.creatorIsMod_ ? (
|
||||||
I18NextService.i18n.t("remove_as_mod")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_as_mod"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("appoint_as_mod")
|
capitalizeFirstLetter(I18NextService.i18n.t("appoint_as_mod"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1022,11 +1053,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modBanButton() {
|
get modBanButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleModBanShow)}
|
onClick={linkEvent(this, this.handleModBanShow)}
|
||||||
aria-label={I18NextService.i18n.t("ban_from_site")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("ban_from_site")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("ban_from_site"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1034,14 +1064,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modUnbanButton() {
|
get modUnbanButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleModBanSubmit)}
|
onClick={linkEvent(this, this.handleModBanSubmit)}
|
||||||
aria-label={I18NextService.i18n.t("unban_from_site")}
|
|
||||||
>
|
>
|
||||||
{this.state.banLoading ? (
|
{this.state.banLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("unban_from_site")
|
capitalizeFirstLetter(I18NextService.i18n.t("unban_from_site"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1050,11 +1079,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get purgePersonButton() {
|
get purgePersonButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handlePurgePersonShow)}
|
onClick={linkEvent(this, this.handlePurgePersonShow)}
|
||||||
aria-label={I18NextService.i18n.t("purge_user")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("purge_user")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("purge_user"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1062,11 +1090,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get purgePostButton() {
|
get purgePostButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handlePurgePostShow)}
|
onClick={linkEvent(this, this.handlePurgePostShow)}
|
||||||
aria-label={I18NextService.i18n.t("purge_post")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("purge_post")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("purge_post"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1074,20 +1101,31 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get toggleAdminButton() {
|
get toggleAdminButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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.handleAddAdmin)}
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
>
|
>
|
||||||
{this.state.addAdminLoading ? (
|
{this.state.addAdminLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : this.creatorIsAdmin_ ? (
|
) : this.creatorIsAdmin_ ? (
|
||||||
I18NextService.i18n.t("remove_as_admin")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_as_admin"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("appoint_as_admin")
|
capitalizeFirstLetter(I18NextService.i18n.t("appoint_as_admin"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get transferCommunityButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
|
onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}
|
||||||
|
>
|
||||||
|
{capitalizeFirstLetter(I18NextService.i18n.t("transfer_community"))}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get modRemoveButton() {
|
get modRemoveButton() {
|
||||||
const removed = this.postView.post.removed;
|
const removed = this.postView.post.removed;
|
||||||
return (
|
return (
|
||||||
|
@ -1102,102 +1140,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.state.removeLoading ? (
|
{this.state.removeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : !removed ? (
|
) : !removed ? (
|
||||||
I18NextService.i18n.t("remove")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_post"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("restore")
|
<>
|
||||||
|
{capitalizeFirstLetter(I18NextService.i18n.t("restore"))}{" "}
|
||||||
|
{I18NextService.i18n.t("post")}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mod/Admin actions to be taken against the author.
|
|
||||||
*/
|
|
||||||
userActionsLine() {
|
|
||||||
// TODO: make nicer
|
|
||||||
const post_view = this.postView;
|
|
||||||
return (
|
|
||||||
this.state.showAdvanced && (
|
|
||||||
<div className="mt-3">
|
|
||||||
{this.canMod_ && (
|
|
||||||
<>
|
|
||||||
{!this.creatorIsMod_ &&
|
|
||||||
(!post_view.creator_banned_from_community
|
|
||||||
? this.modBanFromCommunityButton
|
|
||||||
: this.modUnbanFromCommunityButton)}
|
|
||||||
{!post_view.creator_banned_from_community &&
|
|
||||||
this.addModToCommunityButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Community creators and admins can transfer community to another mod */}
|
|
||||||
{(amCommunityCreator(post_view.creator.id, this.props.moderators) ||
|
|
||||||
this.canAdmin_) &&
|
|
||||||
this.creatorIsMod_ &&
|
|
||||||
(!this.state.showConfirmTransferCommunity ? (
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
|
||||||
onClick={linkEvent(
|
|
||||||
this,
|
|
||||||
this.handleShowConfirmTransferCommunity
|
|
||||||
)}
|
|
||||||
aria-label={I18NextService.i18n.t("transfer_community")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("transfer_community")}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="d-inline-block me-1 btn btn-link btn-animate text-muted py-0"
|
|
||||||
aria-label={I18NextService.i18n.t("are_you_sure")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("are_you_sure")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0 d-inline-block me-1"
|
|
||||||
aria-label={I18NextService.i18n.t("yes")}
|
|
||||||
onClick={linkEvent(this, this.handleTransferCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.transferLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
I18NextService.i18n.t("yes")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0 d-inline-block"
|
|
||||||
onClick={linkEvent(
|
|
||||||
this,
|
|
||||||
this.handleCancelShowConfirmTransferCommunity
|
|
||||||
)}
|
|
||||||
aria-label={I18NextService.i18n.t("no")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("no")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
{/* Admins can ban from all, and appoint other admins */}
|
|
||||||
{this.canAdmin_ && (
|
|
||||||
<>
|
|
||||||
{!this.creatorIsAdmin_ && (
|
|
||||||
<>
|
|
||||||
{!isBanned(post_view.creator)
|
|
||||||
? this.modBanButton
|
|
||||||
: this.modUnbanButton}
|
|
||||||
{this.purgePersonButton}
|
|
||||||
{this.purgePostButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isBanned(post_view.creator) &&
|
|
||||||
post_view.creator.local &&
|
|
||||||
this.toggleAdminButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAndBanDialogs() {
|
removeAndBanDialogs() {
|
||||||
const post = this.postView;
|
const post = this.postView;
|
||||||
const purgeTypeText =
|
const purgeTypeText =
|
||||||
|
@ -1225,11 +1178,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
value={this.state.removeReason}
|
value={this.state.removeReason}
|
||||||
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("remove_post")}
|
|
||||||
>
|
|
||||||
{this.state.removeLoading ? (
|
{this.state.removeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1238,6 +1187,33 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
{this.state.showConfirmTransferCommunity && (
|
||||||
|
<>
|
||||||
|
<button className="d-inline-block me-1 btn btn-link btn-animate text-muted py-0">
|
||||||
|
{I18NextService.i18n.t("are_you_sure")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-animate text-muted py-0 d-inline-block me-1"
|
||||||
|
onClick={linkEvent(this, this.handleTransferCommunity)}
|
||||||
|
>
|
||||||
|
{this.state.transferLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
I18NextService.i18n.t("yes")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-animate text-muted py-0 d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferCommunity
|
||||||
|
)}
|
||||||
|
aria-label={I18NextService.i18n.t("no")}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("no")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{this.state.showBanDialog && (
|
{this.state.showBanDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||||
<div className="mb-3 row col-12">
|
<div className="mb-3 row col-12">
|
||||||
|
@ -1291,11 +1267,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
{/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("ban")}
|
|
||||||
>
|
|
||||||
{this.state.banLoading ? (
|
{this.state.banLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1324,11 +1296,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
value={this.state.reportReason}
|
value={this.state.reportReason}
|
||||||
onInput={linkEvent(this, this.handleReportReasonChange)}
|
onInput={linkEvent(this, this.handleReportReasonChange)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("create_report")}
|
|
||||||
>
|
|
||||||
{this.state.reportLoading ? (
|
{this.state.reportLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1357,11 +1325,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.state.purgeLoading ? (
|
{this.state.purgeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={purgeTypeText}
|
|
||||||
>
|
|
||||||
{this.state.purgeLoading ? <Spinner /> : { purgeTypeText }}
|
{this.state.purgeLoading ? <Spinner /> : { purgeTypeText }}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -1388,15 +1352,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showBodyPreview() {
|
showPreviewButton() {
|
||||||
const { body, id } = this.postView.post;
|
return (
|
||||||
|
<button
|
||||||
return !this.showBody && body ? (
|
type="button"
|
||||||
<Link className="text-body mt-2 d-block" to={`/post/${id}`}>
|
className="btn btn-sm btn-link link-dark link-opacity-75 link-opacity-100-hover py-0 align-baseline"
|
||||||
<div className="md-div mb-1 preview-lines">{body}</div>
|
onClick={linkEvent(this, this.handleShowBody)}
|
||||||
</Link>
|
>
|
||||||
) : (
|
<Icon
|
||||||
<></>
|
icon={!this.state.showBody ? "plus-square" : "minus-square"}
|
||||||
|
classes="icon-inline"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1412,11 +1379,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{/* If it has a thumbnail, do a right aligned thumbnail */}
|
{/* If it has a thumbnail, do a right aligned thumbnail */}
|
||||||
{this.mobileThumbnail()}
|
{this.mobileThumbnail()}
|
||||||
|
|
||||||
{/* Show a preview of the post body */}
|
|
||||||
{this.showBodyPreview()}
|
|
||||||
|
|
||||||
{this.commentsLine(true)}
|
{this.commentsLine(true)}
|
||||||
{this.userActionsLine()}
|
|
||||||
{this.duplicatesLine()}
|
{this.duplicatesLine()}
|
||||||
{this.removeAndBanDialogs()}
|
{this.removeAndBanDialogs()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1427,27 +1390,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<div className="d-none d-sm-block">
|
<div className="d-none d-sm-block">
|
||||||
<article className="row post-container">
|
<article className="row post-container">
|
||||||
{!this.props.viewOnly && (
|
{!this.props.viewOnly && (
|
||||||
<VoteButtons
|
<div className="col flex-grow-0">
|
||||||
voteContentType={VoteContentType.Post}
|
<VoteButtons
|
||||||
id={this.postView.post.id}
|
voteContentType={VoteContentType.Post}
|
||||||
onVote={this.props.onPostVote}
|
id={this.postView.post.id}
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
onVote={this.props.onPostVote}
|
||||||
counts={this.postView.counts}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
my_vote={this.postView.my_vote}
|
counts={this.postView.counts}
|
||||||
/>
|
my_vote={this.postView.my_vote}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="col-sm-2 pe-0 post-media">
|
<div className="col flex-grow-1">
|
||||||
<div className="">{this.thumbnail()}</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-9">
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col flex-grow-0 px-0">
|
||||||
|
<div className="">{this.thumbnail()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="col flex-grow-1">
|
||||||
{this.postTitleLine()}
|
{this.postTitleLine()}
|
||||||
{this.createdLine()}
|
{this.createdLine()}
|
||||||
{this.showBodyPreview()}
|
|
||||||
{this.commentsLine()}
|
{this.commentsLine()}
|
||||||
{this.duplicatesLine()}
|
{this.duplicatesLine()}
|
||||||
{this.userActionsLine()}
|
|
||||||
{this.removeAndBanDialogs()}
|
{this.removeAndBanDialogs()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -348,7 +348,7 @@ export class Post extends Component<any, PostState> {
|
||||||
const res = this.state.postRes.data;
|
const res = this.state.postRes.data;
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<main className="col-12 col-md-8 mb-3">
|
<main className="col-12 col-md-8 col-lg-9 mb-3">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
|
@ -416,7 +416,7 @@ export class Post extends Component<any, PostState> {
|
||||||
{this.state.commentViewType == CommentViewType.Flat &&
|
{this.state.commentViewType == CommentViewType.Flat &&
|
||||||
this.commentsFlat()}
|
this.commentsFlat()}
|
||||||
</main>
|
</main>
|
||||||
<aside className="d-none d-md-block col-md-4">
|
<aside className="d-none d-md-block col-md-4 col-lg-3">
|
||||||
{this.sidebar()}
|
{this.sidebar()}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -284,7 +284,6 @@ export class PrivateMessage extends Component<
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<PrivateMessageForm
|
<PrivateMessageForm
|
||||||
privateMessageView={message_view}
|
|
||||||
replyType={true}
|
replyType={true}
|
||||||
recipient={otherPerson}
|
recipient={otherPerson}
|
||||||
onCreate={this.props.onCreate}
|
onCreate={this.props.onCreate}
|
||||||
|
|
|
@ -332,9 +332,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (
|
if (!this.state.isIsomorphic) {
|
||||||
!(this.state.isIsomorphic || this.props.history.location.state?.searched)
|
|
||||||
) {
|
|
||||||
const promises = [this.fetchCommunities()];
|
const promises = [this.fetchCommunities()];
|
||||||
if (this.state.searchText) {
|
if (this.state.searchText) {
|
||||||
promises.push(this.search());
|
promises.push(this.search());
|
||||||
|
@ -432,7 +430,15 @@ export class Search extends Component<any, SearchState> {
|
||||||
q: query,
|
q: query,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
resolveObjectResponse = await client.resolveObject(resolveObjectForm);
|
resolveObjectResponse = await HttpService.silent_client.resolveObject(
|
||||||
|
resolveObjectForm
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we return this object with a state of failed, the catch-all-handler will redirect
|
||||||
|
// to an error page, so we ignore it by covering up the error with the empty state.
|
||||||
|
if (resolveObjectResponse.state === "failed") {
|
||||||
|
resolveObjectResponse = { state: "empty" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -950,7 +956,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
if (auth) {
|
if (auth) {
|
||||||
this.setState({ resolveObjectRes: { state: "loading" } });
|
this.setState({ resolveObjectRes: { state: "loading" } });
|
||||||
this.setState({
|
this.setState({
|
||||||
resolveObjectRes: await HttpService.client.resolveObject({
|
resolveObjectRes: await HttpService.silent_client.resolveObject({
|
||||||
q,
|
q,
|
||||||
auth,
|
auth,
|
||||||
}),
|
}),
|
||||||
|
@ -1097,10 +1103,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
sort: sort ?? urlSort,
|
sort: sort ?? urlSort,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.props.history.push(`/search${getQueryString(queryParams)}`, {
|
this.props.history.push(`/search${getQueryString(queryParams)}`);
|
||||||
searched: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.search();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export const favIconUrl = "/static/assets/icons/favicon.svg";
|
import { getStaticDir } from "@utils/env";
|
||||||
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
|
|
||||||
|
export const favIconUrl = `${getStaticDir()}/assets/icons/favicon.svg`;
|
||||||
|
export const favIconPngUrl = `${getStaticDir()}/assets/icons/apple-touch-icon.png`;
|
||||||
|
|
||||||
export const repoUrl = "https://github.com/LemmyNet";
|
export const repoUrl = "https://github.com/LemmyNet";
|
||||||
export const joinLemmyUrl = "https://join-lemmy.org";
|
export const joinLemmyUrl = "https://join-lemmy.org";
|
||||||
|
@ -21,7 +23,7 @@ export const markdownFieldCharacterLimit = 50000;
|
||||||
export const maxUploadImages = 20;
|
export const maxUploadImages = 20;
|
||||||
export const concurrentImageUpload = 4;
|
export const concurrentImageUpload = 4;
|
||||||
export const updateUnreadCountsInterval = 30000;
|
export const updateUnreadCountsInterval = 30000;
|
||||||
export const fetchLimit = 40;
|
export const fetchLimit = 20;
|
||||||
export const relTags = "noopener nofollow";
|
export const relTags = "noopener nofollow";
|
||||||
export const emDash = "\u2014";
|
export const emDash = "\u2014";
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { getHttpBase } from "@utils/env";
|
import { getHttpBase } from "@utils/env";
|
||||||
import { LemmyHttp } from "lemmy-js-client";
|
import { LemmyHttp } from "lemmy-js-client";
|
||||||
import { toast } from "../../shared/toast";
|
import { toast } from "../toast";
|
||||||
import { I18NextService } from "./I18NextService";
|
import { I18NextService } from "./I18NextService";
|
||||||
|
|
||||||
type EmptyRequestState = {
|
export type EmptyRequestState = {
|
||||||
state: "empty";
|
state: "empty";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export type WrappedLemmyHttp = {
|
||||||
class WrappedLemmyHttpClient {
|
class WrappedLemmyHttpClient {
|
||||||
#client: LemmyHttp;
|
#client: LemmyHttp;
|
||||||
|
|
||||||
constructor(client: LemmyHttp) {
|
constructor(client: LemmyHttp, silent = false) {
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
|
|
||||||
for (const key of Object.getOwnPropertyNames(
|
for (const key of Object.getOwnPropertyNames(
|
||||||
|
@ -61,8 +61,10 @@ class WrappedLemmyHttpClient {
|
||||||
state: !(res === undefined || res === null) ? "success" : "empty",
|
state: !(res === undefined || res === null) ? "success" : "empty",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`API error: ${error}`);
|
if (!silent) {
|
||||||
toast(I18NextService.i18n.t(error), "danger");
|
console.error(`API error: ${error}`);
|
||||||
|
toast(I18NextService.i18n.t(error), "danger");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: "failed",
|
state: "failed",
|
||||||
msg: error,
|
msg: error,
|
||||||
|
@ -74,16 +76,23 @@ class WrappedLemmyHttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapClient(client: LemmyHttp) {
|
export function wrapClient(client: LemmyHttp, silent = false) {
|
||||||
return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary
|
// unfortunately, this verbose cast is necessary
|
||||||
|
return new WrappedLemmyHttpClient(
|
||||||
|
client,
|
||||||
|
silent
|
||||||
|
) as unknown as WrappedLemmyHttp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpService {
|
export class HttpService {
|
||||||
static #_instance: HttpService;
|
static #_instance: HttpService;
|
||||||
|
#silent_client: WrappedLemmyHttp;
|
||||||
#client: WrappedLemmyHttp;
|
#client: WrappedLemmyHttp;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.#client = wrapClient(new LemmyHttp(getHttpBase()));
|
const lemmyHttp = new LemmyHttp(getHttpBase());
|
||||||
|
this.#client = wrapClient(lemmyHttp);
|
||||||
|
this.#silent_client = wrapClient(lemmyHttp, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get #Instance() {
|
static get #Instance() {
|
||||||
|
@ -93,4 +102,8 @@ export class HttpService {
|
||||||
public static get client() {
|
public static get client() {
|
||||||
return this.#Instance.#client;
|
return this.#Instance.#client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static get silent_client() {
|
||||||
|
return this.#Instance.#silent_client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { isAuthPath } from "@utils/app";
|
import { isAuthPath } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
import { isBrowser } from "@utils/browser";
|
||||||
import { isHttps } from "@utils/env";
|
import { isHttps } from "@utils/env";
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
import * as cookie from "cookie";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
||||||
import { toast } from "../toast";
|
import { toast } from "../toast";
|
||||||
|
@ -31,9 +31,15 @@ export class UserService {
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 365);
|
expires.setDate(expires.getDate() + 365);
|
||||||
if (res.jwt) {
|
if (isBrowser() && res.jwt) {
|
||||||
toast(I18NextService.i18n.t("logged_in"));
|
toast(I18NextService.i18n.t("logged_in"));
|
||||||
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps() });
|
document.cookie = cookie.serialize("jwt", res.jwt, {
|
||||||
|
expires,
|
||||||
|
secure: isHttps(),
|
||||||
|
domain: location.hostname,
|
||||||
|
sameSite: true,
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
this.#setJwtInfo();
|
this.#setJwtInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,8 +47,14 @@ export class UserService {
|
||||||
public logout() {
|
public logout() {
|
||||||
this.jwtInfo = undefined;
|
this.jwtInfo = undefined;
|
||||||
this.myUserInfo = undefined;
|
this.myUserInfo = undefined;
|
||||||
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
|
if (isBrowser()) {
|
||||||
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.hostname;
|
document.cookie = cookie.serialize("jwt", "", {
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
domain: location.hostname,
|
||||||
|
sameSite: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (isAuthPath(location.pathname)) {
|
if (isAuthPath(location.pathname)) {
|
||||||
location.replace("/");
|
location.replace("/");
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,10 +78,11 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
#setJwtInfo() {
|
#setJwtInfo() {
|
||||||
const jwt: string | undefined = IsomorphicCookie.load("jwt");
|
if (isBrowser()) {
|
||||||
|
const { jwt } = cookie.parse(document.cookie);
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/shared/utils/app/get-role-label-pill.tsx
Normal file
21
src/shared/utils/app/get-role-label-pill.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export default function getRoleLabelPill({
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
classes,
|
||||||
|
shrink = true,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
tooltip: string;
|
||||||
|
classes?: string;
|
||||||
|
shrink?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`badge me-1 ${classes ?? "text-bg-light"}`}
|
||||||
|
aria-label={tooltip}
|
||||||
|
data-tippy-content={tooltip}
|
||||||
|
>
|
||||||
|
{shrink ? label[0].toUpperCase() : label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import getDataTypeString from "./get-data-type-string";
|
||||||
import getDepthFromComment from "./get-depth-from-comment";
|
import getDepthFromComment from "./get-depth-from-comment";
|
||||||
import getIdFromProps from "./get-id-from-props";
|
import getIdFromProps from "./get-id-from-props";
|
||||||
import getRecipientIdFromProps from "./get-recipient-id-from-props";
|
import getRecipientIdFromProps from "./get-recipient-id-from-props";
|
||||||
|
import getRoleLabelPill from "./get-role-label-pill";
|
||||||
import getUpdatedSearchId from "./get-updated-search-id";
|
import getUpdatedSearchId from "./get-updated-search-id";
|
||||||
import initializeSite from "./initialize-site";
|
import initializeSite from "./initialize-site";
|
||||||
import insertCommentIntoTree from "./insert-comment-into-tree";
|
import insertCommentIntoTree from "./insert-comment-into-tree";
|
||||||
|
@ -53,7 +54,6 @@ import showScores from "./show-scores";
|
||||||
import siteBannerCss from "./site-banner-css";
|
import siteBannerCss from "./site-banner-css";
|
||||||
import updateCommunityBlock from "./update-community-block";
|
import updateCommunityBlock from "./update-community-block";
|
||||||
import updatePersonBlock from "./update-person-block";
|
import updatePersonBlock from "./update-person-block";
|
||||||
import getUserInterfaceLangId from "./user-interface-language";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildCommentsTree,
|
buildCommentsTree,
|
||||||
|
@ -87,8 +87,8 @@ export {
|
||||||
getDepthFromComment,
|
getDepthFromComment,
|
||||||
getIdFromProps,
|
getIdFromProps,
|
||||||
getRecipientIdFromProps,
|
getRecipientIdFromProps,
|
||||||
|
getRoleLabelPill,
|
||||||
getUpdatedSearchId,
|
getUpdatedSearchId,
|
||||||
getUserInterfaceLangId,
|
|
||||||
initializeSite,
|
initializeSite,
|
||||||
insertCommentIntoTree,
|
insertCommentIntoTree,
|
||||||
isAuthPath,
|
isAuthPath,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default function isAuthPath(pathname: string) {
|
export default function isAuthPath(pathname: string) {
|
||||||
return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
|
return /^\/create_.*|inbox|settings|admin|reports|registration_applications/g.test(
|
||||||
pathname
|
pathname
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,44 @@
|
||||||
import setDefaultOptions from "date-fns/setDefaultOptions";
|
import setDefaultOptions from "date-fns/setDefaultOptions";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
|
||||||
|
const EN_US = "en-US";
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
let lang = I18NextService.i18n.language;
|
let lang = I18NextService.i18n.language;
|
||||||
if (lang === "en") {
|
if (lang === "en") {
|
||||||
lang = "en-US";
|
lang = EN_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = (
|
// if lang and country are the same, then date-fns expects only the lang
|
||||||
await import(
|
// eg: instead of "fr-FR", we should import just "fr"
|
||||||
/* webpackExclude: /\.js\.flow$/ */
|
|
||||||
`date-fns/locale/${lang}`
|
if (lang.includes("-")) {
|
||||||
)
|
const parts = lang.split("-");
|
||||||
).default;
|
if (parts[0] === parts[1].toLowerCase()) {
|
||||||
|
lang = parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let locale;
|
||||||
|
|
||||||
|
try {
|
||||||
|
locale = (
|
||||||
|
await import(
|
||||||
|
/* webpackExclude: /\.js\.flow$/ */
|
||||||
|
`date-fns/locale/${lang}`
|
||||||
|
)
|
||||||
|
).default;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
`Could not load locale ${lang} from date-fns, falling back to ${EN_US}`
|
||||||
|
);
|
||||||
|
locale = (
|
||||||
|
await import(
|
||||||
|
/* webpackExclude: /\.js\.flow$/ */
|
||||||
|
`date-fns/locale/${EN_US}`
|
||||||
|
)
|
||||||
|
).default;
|
||||||
|
}
|
||||||
setDefaultOptions({
|
setDefaultOptions({
|
||||||
locale,
|
locale,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Language } from "lemmy-js-client";
|
|
||||||
import { I18NextService } from "../../services/I18NextService";
|
|
||||||
|
|
||||||
export default function getUserInterfaceLangId(
|
|
||||||
allLanguages: Language[]
|
|
||||||
): number {
|
|
||||||
// Get the string of the browser- or user-defined language, like en-US
|
|
||||||
const i18nLang = I18NextService.i18n.language;
|
|
||||||
|
|
||||||
// Find the Language object with a code that matches the initial characters of
|
|
||||||
// this string
|
|
||||||
const userLang = allLanguages.find(lang => {
|
|
||||||
return i18nLang.indexOf(lang.code) === 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the ID of that language object, or "0" for Undetermined
|
|
||||||
return userLang?.id || 0;
|
|
||||||
}
|
|
5
src/shared/utils/env/get-static-dir.ts
vendored
Normal file
5
src/shared/utils/env/get-static-dir.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Returns path to static directory, intended
|
||||||
|
// for cache-busting based on latest commit hash.
|
||||||
|
export default function getStaticDir() {
|
||||||
|
return `/static/${process.env.COMMIT_HASH}`;
|
||||||
|
}
|
2
src/shared/utils/env/index.ts
vendored
2
src/shared/utils/env/index.ts
vendored
|
@ -6,6 +6,7 @@ import getHttpBaseExternal from "./get-http-base-external";
|
||||||
import getHttpBaseInternal from "./get-http-base-internal";
|
import getHttpBaseInternal from "./get-http-base-internal";
|
||||||
import getInternalHost from "./get-internal-host";
|
import getInternalHost from "./get-internal-host";
|
||||||
import getSecure from "./get-secure";
|
import getSecure from "./get-secure";
|
||||||
|
import getStaticDir from "./get-static-dir";
|
||||||
import httpExternalPath from "./http-external-path";
|
import httpExternalPath from "./http-external-path";
|
||||||
import isHttps from "./is-https";
|
import isHttps from "./is-https";
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export {
|
||||||
getHttpBaseInternal,
|
getHttpBaseInternal,
|
||||||
getInternalHost,
|
getInternalHost,
|
||||||
getSecure,
|
getSecure,
|
||||||
|
getStaticDir,
|
||||||
httpExternalPath,
|
httpExternalPath,
|
||||||
isHttps,
|
isHttps,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,8 @@ import formatDistanceStrict from "date-fns/formatDistanceStrict";
|
||||||
import parseISO from "date-fns/parseISO";
|
import parseISO from "date-fns/parseISO";
|
||||||
|
|
||||||
export default function (dateString?: string) {
|
export default function (dateString?: string) {
|
||||||
return formatDistanceStrict(
|
const parsed = parseISO((dateString ?? Date.now().toString()) + "Z");
|
||||||
parseISO(dateString ?? Date.now().toString()),
|
return formatDistanceStrict(parsed, new Date(), {
|
||||||
new Date(),
|
addSuffix: true,
|
||||||
{
|
});
|
||||||
addSuffix: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ const CopyPlugin = require("copy-webpack-plugin");
|
||||||
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
||||||
const merge = require("lodash.merge");
|
const merge = require("lodash.merge");
|
||||||
const { ServiceWorkerPlugin } = require("service-worker-webpack");
|
const { ServiceWorkerPlugin } = require("service-worker-webpack");
|
||||||
const BundleAnalyzerPlugin =
|
|
||||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
|
||||||
const banner = `
|
const banner = `
|
||||||
hash:[contentHash], chunkhash:[chunkhash], name:[name], filebase:[base], query:[query], file:[file]
|
hash:[contentHash], chunkhash:[chunkhash], name:[name], filebase:[base], query:[query], file:[file]
|
||||||
Source code: https://github.com/LemmyNet/lemmy-ui
|
Source code: https://github.com/LemmyNet/lemmy-ui
|
||||||
|
@ -15,56 +14,63 @@ const banner = `
|
||||||
@license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
|
@license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const base = {
|
function getBase(env, mode) {
|
||||||
output: {
|
return {
|
||||||
filename: "js/server.js",
|
output: {
|
||||||
publicPath: "/",
|
filename: "js/server.js",
|
||||||
hashFunction: "xxhash64",
|
publicPath: "/",
|
||||||
},
|
hashFunction: "xxhash64",
|
||||||
resolve: {
|
|
||||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "src/"),
|
|
||||||
"@utils": path.resolve(__dirname, "src/shared/utils/"),
|
|
||||||
},
|
},
|
||||||
},
|
resolve: {
|
||||||
performance: {
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
hints: false,
|
alias: {
|
||||||
},
|
"@": path.resolve(__dirname, "src/"),
|
||||||
module: {
|
"@utils": path.resolve(__dirname, "src/shared/utils/"),
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(scss|css)$/i,
|
|
||||||
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
|
performance: {
|
||||||
exclude: /node_modules/, // ignore node_modules
|
hints: false,
|
||||||
loader: "babel-loader",
|
},
|
||||||
},
|
module: {
|
||||||
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.m?js/,
|
test: /\.(scss|css)$/i,
|
||||||
resolve: {
|
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
||||||
fullySpecified: false,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
|
||||||
|
exclude: /node_modules/, // ignore node_modules
|
||||||
|
loader: "babel-loader",
|
||||||
|
},
|
||||||
|
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
"process.env.COMMIT_HASH": `"${env.COMMIT_HASH}"`,
|
||||||
|
"process.env.NODE_ENV": `"${mode}"`,
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "styles/styles.css",
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{ from: "./src/assets", to: "./assets" }],
|
||||||
|
}),
|
||||||
|
new webpack.BannerPlugin({
|
||||||
|
banner,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
plugins: [
|
}
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: "styles/styles.css",
|
|
||||||
}),
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [{ from: "./src/assets", to: "./assets" }],
|
|
||||||
}),
|
|
||||||
new webpack.BannerPlugin({
|
|
||||||
banner,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const createServerConfig = (_env, mode) => {
|
const createServerConfig = (env, mode) => {
|
||||||
|
const base = getBase(env, mode);
|
||||||
const config = merge({}, base, {
|
const config = merge({}, base, {
|
||||||
mode,
|
mode,
|
||||||
entry: "./src/server/index.tsx",
|
entry: "./src/server/index.tsx",
|
||||||
|
@ -91,23 +97,22 @@ const createServerConfig = (_env, mode) => {
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createClientConfig = (_env, mode) => {
|
const createClientConfig = (env, mode) => {
|
||||||
|
const base = getBase(env, mode);
|
||||||
const config = merge({}, base, {
|
const config = merge({}, base, {
|
||||||
mode,
|
mode,
|
||||||
entry: "./src/client/index.tsx",
|
entry: "./src/client/index.tsx",
|
||||||
output: {
|
output: {
|
||||||
filename: "js/client.js",
|
filename: "js/client.js",
|
||||||
|
publicPath: `/static/${env.COMMIT_HASH}/`,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...base.plugins,
|
...base.plugins,
|
||||||
new ServiceWorkerPlugin({
|
new ServiceWorkerPlugin({
|
||||||
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
|
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
|
||||||
workbox: {
|
workbox: {
|
||||||
modifyURLPrefix: {
|
|
||||||
"/": "/static/",
|
|
||||||
},
|
|
||||||
cacheId: "lemmy",
|
cacheId: "lemmy",
|
||||||
include: [/(assets|styles)\/.+\..+|client\.js$/g],
|
include: [/(assets|styles|js)\/.+\..+$/g],
|
||||||
inlineWorkboxRuntime: true,
|
inlineWorkboxRuntime: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
|
@ -156,6 +161,8 @@ const createClientConfig = (_env, mode) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mode === "none") {
|
if (mode === "none") {
|
||||||
|
const BundleAnalyzerPlugin =
|
||||||
|
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
config.plugins.push(new BundleAnalyzerPlugin());
|
config.plugins.push(new BundleAnalyzerPlugin());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue