Merge branch 'main' into federation_disclaimer

This commit is contained in:
SleeplessOne1917 2023-10-25 20:26:44 +00:00 committed by GitHub
commit 7b8ce16407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
189 changed files with 6967 additions and 4756 deletions

View file

@ -20,10 +20,11 @@
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0, "@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0, "@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"arrow-body-style": 0, "arrow-body-style": 0,
"curly": 0, "curly": 0,
"eol-last": 0, "eol-last": 0,
"eqeqeq": 0, "eqeqeq": "error",
"func-style": 0, "func-style": 0,
"import/no-duplicates": 0, "import/no-duplicates": 0,
"max-statements": 0, "max-statements": 0,
@ -39,7 +40,7 @@
"no-useless-constructor": 0, "no-useless-constructor": 0,
"no-useless-escape": 0, "no-useless-escape": 0,
"no-var": 0, "no-var": 0,
"prefer-const": 1, "prefer-const": "error",
"prefer-rest-params": 0, "prefer-rest-params": 0,
"prettier/prettier": "error", "prettier/prettier": "error",
"quote-props": 0, "quote-props": 0,

View file

@ -1,5 +1,6 @@
src/shared/translations src/shared/translations
lemmy-translations lemmy-translations
src/assets/css/themes/*.css src/assets/css/themes/*.css
src/assets/css/code-themes/*.css
stats.json stats.json
dist dist

View file

@ -1,6 +1,6 @@
pipeline: steps:
fetch_git_submodules: fetch_git_submodules:
image: node:alpine image: node:20-alpine
commands: commands:
- apk add git - apk add git
- git submodule init - git submodule init
@ -8,17 +8,17 @@ pipeline:
# - git fetch --tags # - git fetch --tags
yarn: yarn:
image: node:alpine image: node:20-alpine
commands: commands:
- yarn - yarn
yarn_lint: yarn_lint:
image: node:alpine image: node:20-alpine
commands: commands:
- yarn lint - yarn lint
yarn_build_dev: yarn_build_dev:
image: node:alpine image: node:20-alpine
commands: commands:
- yarn build:dev - yarn build:dev
@ -29,7 +29,7 @@ pipeline:
repo: dessalines/lemmy-ui repo: dessalines/lemmy-ui
dockerfile: Dockerfile dockerfile: Dockerfile
platforms: linux/amd64 platforms: linux/amd64
auto_tag: true tag: ${CI_COMMIT_TAG}
when: when:
event: tag event: tag
@ -43,3 +43,19 @@ pipeline:
tag: dev tag: dev
when: when:
event: cron event: cron
notify_on_failure:
image: alpine:3
commands:
- apk add curl
- "curl -d'Lemmy-UI CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when:
status: [failure]
notify_on_tag_deploy:
image: alpine:3
commands:
- apk add curl
- "curl -d'lemmy-ui:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
when:
event: tag

View file

@ -1,4 +1,4 @@
FROM node:20.2-alpine as builder FROM node:20-alpine as builder
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
RUN curl -sf https://gobinaries.com/tj/node-prune | sh RUN curl -sf https://gobinaries.com/tj/node-prune | sh
@ -38,10 +38,14 @@ RUN rm -rf ./node_modules/npm
RUN du -sh ./node_modules/* | sort -nr | grep '\dM.*' RUN du -sh ./node_modules/* | sort -nr | grep '\dM.*'
FROM node:alpine as runner FROM node:20-alpine as runner
RUN apk update && apk add curl --no-cache
COPY --from=builder /usr/src/app/dist /app/dist COPY --from=builder /usr/src/app/dist /app/dist
COPY --from=builder /usr/src/app/node_modules /app/node_modules COPY --from=builder /usr/src/app/node_modules /app/node_modules
RUN chown -R node:node /app
USER node
EXPOSE 1234 EXPOSE 1234
WORKDIR /app WORKDIR /app
CMD node dist/js/server.js CMD node dist/js/server.js

View file

@ -1,4 +1,4 @@
FROM node:20.2-alpine as builder FROM node:20-alpine as builder
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
WORKDIR /usr/src/app WORKDIR /usr/src/app
@ -28,7 +28,7 @@ RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
RUN yarn --prefer-offline RUN yarn --prefer-offline
RUN yarn build:dev RUN yarn build:dev
FROM node:alpine as runner FROM node:20-alpine as runner
COPY --from=builder /usr/src/app/dist /app/dist COPY --from=builder /usr/src/app/dist /app/dist
COPY --from=builder /usr/src/app/node_modules /app/node_modules COPY --from=builder /usr/src/app/node_modules /app/node_modules

View file

@ -8,12 +8,12 @@ fs.readdir(translationDir, (_err, files) => {
const lang = filename.split(".")[0]; const lang = filename.split(".")[0];
try { try {
const json = JSON.parse( const json = JSON.parse(
fs.readFileSync(translationDir + filename, "utf8") fs.readFileSync(translationDir + filename, "utf8"),
); );
let data = `export const ${lang} = {\n translation: {`; let data = `export const ${lang} = {\n translation: {`;
for (const key in json) { for (const key in json) {
if (key in json) { if (key in json) {
const value = json[key].replace(/"/g, '\\"'); const value = json[key].replace(/"/g, '\\"').replace("\n", "\\n");
data += `\n ${key}: "${value}",`; data += `\n ${key}: "${value}",`;
} }
} }
@ -67,14 +67,14 @@ ${optionKeys.map(key => `${indent}| "${key}"`).join("\n")};
export type I18nKeys = NoOptionI18nKeys | OptionI18nKeys; export type I18nKeys = NoOptionI18nKeys | OptionI18nKeys;
export type TTypedOptions<TKey extends OptionI18nKeys> =${Array.from( export type TTypedOptions<TKey extends OptionI18nKeys> =${Array.from(
optionMap.entries() optionMap.entries(),
).reduce( ).reduce(
(acc, [key, options]) => (acc, [key, options]) =>
`${acc} TKey extends \"${key}\" ? ${ `${acc} TKey extends \"${key}\" ? ${
options.reduce((acc, cur) => acc + `${cur}: string | number; `, "{ ") + options.reduce((acc, cur) => acc + `${cur}: string | number; `, "{ ") +
"}" "}"
} :\n${indent}`, } :\n${indent}`,
"" "",
)} (Record<string, unknown> | string); )} (Record<string, unknown> | string);
export interface TFunctionTyped { export interface TFunctionTyped {

@ -1 +1 @@
Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce Subproject commit 6fbc86932a03c4d40829ee4a3395259b2a7660e5

View file

@ -1,6 +1,6 @@
{ {
"name": "lemmy-ui", "name": "lemmy-ui",
"version": "0.18.1-rc.11", "version": "0.19.0-rc.3",
"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",
@ -15,6 +15,7 @@
"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}\"",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "husky install",
"themes:build": "sass src/assets/css/themes/:src/assets/css/themes", "themes:build": "sass src/assets/css/themes/:src/assets/css/themes",
"themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes", "themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes",
"translations:generate": "node generate_translations.js", "translations:generate": "node generate_translations.js",
@ -34,23 +35,24 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0", "@babel/plugin-proposal-decorators": "^7.21.5",
"@babel/plugin-transform-runtime": "^7.21.4", "@babel/plugin-transform-runtime": "^7.21.5",
"@babel/plugin-transform-typescript": "^7.21.3", "@babel/plugin-transform-typescript": "^7.21.5",
"@babel/preset-env": "7.21.5", "@babel/preset-env": "7.21.5",
"@babel/preset-typescript": "^7.21.5", "@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5", "@babel/runtime": "^7.21.5",
"@emoji-mart/data": "^1.1.0", "@emoji-mart/data": "^1.1.0",
"@shortcm/qr-image": "^9.0.2",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0", "babel-plugin-inferno": "^6.6.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.3.1",
"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", "cookie": "^0.5.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cross-fetch": "^3.1.5", "cross-fetch": "^4.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"emoji-mart": "^5.4.0", "emoji-mart": "^5.4.0",
@ -58,22 +60,24 @@
"express": "~4.18.2", "express": "~4.18.2",
"history": "^5.3.0", "history": "^5.3.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"i18next": "^22.4.15", "husky": "^8.0.3",
"inferno": "^8.1.1", "i18next": "^23.3.0",
"inferno-create-element": "^8.1.1", "inferno": "^8.2.2",
"inferno-create-element": "^8.2.2",
"inferno-helmet": "^5.2.1", "inferno-helmet": "^5.2.1",
"inferno-hydrate": "^8.1.1", "inferno-hydrate": "^8.2.2",
"inferno-i18next-dess": "0.0.2", "inferno-i18next-dess": "0.0.2",
"inferno-router": "^8.1.1", "inferno-router": "^8.2.2",
"inferno-server": "^8.1.1", "inferno-server": "^8.2.2",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lemmy-js-client": "0.18.0-rc.2", "lemmy-js-client": "0.19.0-rc.14",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-bidi": "^0.1.0",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.2", "markdown-it-emoji": "^2.0.2",
"markdown-it-footnote": "^3.0.3", "markdown-it-footnote": "^3.0.3",
"markdown-it-highlightjs": "^4.0.1",
"markdown-it-html5-embed": "^1.0.0", "markdown-it-html5-embed": "^1.0.0",
"markdown-it-ruby": "^0.1.1", "markdown-it-ruby": "^0.1.1",
"markdown-it-sub": "^1.0.0", "markdown-it-sub": "^1.0.0",
@ -81,21 +85,23 @@
"mini-css-extract-plugin": "^2.7.5", "mini-css-extract-plugin": "^2.7.5",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0", "run-node-webpack-plugin": "^1.3.0",
"sanitize-html": "^2.10.0", "rxjs": "^7.8.1",
"sass": "^1.62.1", "sanitize-html": "^2.11.0",
"sass-loader": "^13.2.2", "sass": "^1.64.1",
"sass-loader": "^13.3.2",
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.1",
"service-worker-webpack": "^1.0.0", "service-worker-webpack": "^1.0.0",
"sharp": "^0.32.1", "sharp": "^0.32.4",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"tributejs": "^5.1.3", "tributejs": "^5.1.3",
"webpack": "5.82.1", "webpack": "5.88.2",
"webpack-cli": "^5.1.1", "webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.8", "@babel/core": "^7.21.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@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/cookie": "^0.5.1",
@ -103,33 +109,32 @@
"@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",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/markdown-it-container": "^2.0.5", "@types/markdown-it-container": "^2.0.6",
"@types/node": "^20.1.2", "@types/node": "^20.4.5",
"@types/path-browserify": "^1.0.0", "@types/path-browserify": "^1.0.0",
"@types/sanitize-html": "^2.9.0", "@types/sanitize-html": "^2.9.0",
"@types/serialize-javascript": "^5.0.1", "@types/serialize-javascript": "^5.0.1",
"@types/toastify-js": "^1.11.1", "@types/toastify-js": "^1.12.0",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^5.59.5", "@typescript-eslint/parser": "^6.2.0",
"eslint": "^8.40.0", "eslint": "^8.45.0",
"eslint-plugin-inferno": "^7.32.2", "eslint-plugin-inferno": "^7.32.2",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.3",
"prettier": "^2.8.8", "prettier": "^3.0.0",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^3.2.2", "prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-packagejson": "^2.4.3", "prettier-plugin-packagejson": "^2.4.5",
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"sortpack": "^2.3.4", "sortpack": "^2.3.4",
"style-loader": "^3.3.2", "style-loader": "^3.3.2",
"terser": "^5.17.3", "terser": "^5.19.2",
"typescript": "^5.0.4", "typescript": "^5.1.6",
"typescript-language-server": "^3.3.2", "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.1"
}, },
"packageManager": "yarn@1.22.19", "packageManager": "yarn@1.22.19",
"engines": { "engines": {

View file

@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View file

@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View file

@ -251,7 +251,7 @@ hr {
flex: 1; flex: 1;
} }
.img-blur { .img-blur-thumb {
filter: blur(10px); filter: blur(10px);
-webkit-filter: blur(10px); -webkit-filter: blur(10px);
-moz-filter: blur(10px); -moz-filter: blur(10px);
@ -259,6 +259,18 @@ hr {
-ms-filter: blur(10px); -ms-filter: blur(10px);
} }
.img-blur-icon {
filter: blur(3px);
-webkit-filter: blur(3px);
-moz-filter: blur(3px);
-o-filter: blur(3px);
-ms-filter: blur(3px);
}
.img-cover {
object-fit: cover;
}
.img-expanded { .img-expanded {
max-height: 90vh; max-height: 90vh;
} }
@ -436,3 +448,7 @@ br.big {
.skip-link:focus { .skip-link:focus {
top: 0; top: 0;
} }
.totp-link {
width: fit-content;
}

View file

@ -18,7 +18,7 @@ $green: #00bc8c;
$cyan: #3498db; $cyan: #3498db;
$primary: $green; $primary: $green;
$secondary: $gray-700; $secondary: $gray-600;
$success: $green; $success: $green;
$dark: $gray-300; $dark: $gray-300;
@ -30,9 +30,18 @@ $mark-bg: $gray-900;
$text-muted: $gray-600; $text-muted: $gray-600;
$yiq-contrasted-threshold: 175; $yiq-contrasted-threshold: 175;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", $font-family-sans-serif:
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Lato",
"Segoe UI Emoji", "Segoe UI Symbol"; -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; $font-size-base: 0.9375rem;
$h1-font-size: 3rem; $h1-font-size: 3rem;
$h2-font-size: 2.5rem; $h2-font-size: 2.5rem;

View file

@ -1,7 +1,6 @@
@import "variables.darkly"; @import "variables.darkly";
$primary: $blue; $primary: $blue;
$secondary: #444;
$light: $gray-800; $light: $gray-800;
$link-color: $red; $link-color: $red;

View file

@ -17,7 +17,7 @@ $green: #00bc8c;
$cyan: #3498db; $cyan: #3498db;
$primary: $green; $primary: $green;
$secondary: $gray-700; $secondary: $gray-500;
$success: $green; $success: $green;
$dark: $gray-300; $dark: $gray-300;
@ -29,9 +29,18 @@ $mark-bg: #333;
$text-muted: $gray-600; $text-muted: $gray-600;
$yiq-contrasted-threshold: 175; $yiq-contrasted-threshold: 175;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", $font-family-sans-serif:
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Lato",
"Segoe UI Emoji", "Segoe UI Symbol"; -apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
$h1-font-size: 3rem; $h1-font-size: 3rem;
$h2-font-size: 2.5rem; $h2-font-size: 2.5rem;
$h3-font-size: 2rem; $h3-font-size: 2rem;

View file

@ -26,7 +26,7 @@ $danger: #aa0000;
$info: #00aaaa; $info: #00aaaa;
$warning: #aa00aa; $warning: #aa00aa;
$light: $gray-800; $light: $gray-800;
$dark: black; $dark: $gray-300;
$body-bg: #000084; $body-bg: #000084;
$body-color: $gray-300; $body-color: $gray-300;

View file

@ -9,6 +9,7 @@ $gray-800: #303030;
$gray-900: #222; $gray-900: #222;
$light: $gray-700; $light: $gray-700;
$dark: $gray-200;
$body-bg: $gray-900; $body-bg: $gray-900;
$body-color: $gray-200; $body-color: $gray-200;

View file

@ -70,7 +70,7 @@ hr.my-3 {
--bs-gray-800: #303030; --bs-gray-800: #303030;
--bs-gray-900: #222; --bs-gray-900: #222;
--bs-primary: #00bc8c; --bs-primary: #00bc8c;
--bs-secondary: #444; --bs-secondary: #adb5bd;
--bs-success: #00bc8c; --bs-success: #00bc8c;
--bs-info: #3498db; --bs-info: #3498db;
--bs-warning: #f39c12; --bs-warning: #f39c12;
@ -78,7 +78,7 @@ hr.my-3 {
--bs-light: #303030; --bs-light: #303030;
--bs-dark: #dee2e6; --bs-dark: #dee2e6;
--bs-primary-rgb: 0, 188, 140; --bs-primary-rgb: 0, 188, 140;
--bs-secondary-rgb: 68, 68, 68; --bs-secondary-rgb: 173, 181, 189;
--bs-success-rgb: 0, 188, 140; --bs-success-rgb: 0, 188, 140;
--bs-info-rgb: 52, 152, 219; --bs-info-rgb: 52, 152, 219;
--bs-warning-rgb: 243, 156, 18; --bs-warning-rgb: 243, 156, 18;
@ -86,7 +86,7 @@ hr.my-3 {
--bs-light-rgb: 48, 48, 48; --bs-light-rgb: 48, 48, 48;
--bs-dark-rgb: 222, 226, 230; --bs-dark-rgb: 222, 226, 230;
--bs-primary-text-emphasis: #004b38; --bs-primary-text-emphasis: #004b38;
--bs-secondary-text-emphasis: #1b1b1b; --bs-secondary-text-emphasis: #45484c;
--bs-success-text-emphasis: #004b38; --bs-success-text-emphasis: #004b38;
--bs-info-text-emphasis: #153d58; --bs-info-text-emphasis: #153d58;
--bs-warning-text-emphasis: #613e07; --bs-warning-text-emphasis: #613e07;
@ -94,7 +94,7 @@ hr.my-3 {
--bs-light-text-emphasis: #444; --bs-light-text-emphasis: #444;
--bs-dark-text-emphasis: #444; --bs-dark-text-emphasis: #444;
--bs-primary-bg-subtle: #ccf2e8; --bs-primary-bg-subtle: #ccf2e8;
--bs-secondary-bg-subtle: #dadada; --bs-secondary-bg-subtle: #eff0f2;
--bs-success-bg-subtle: #ccf2e8; --bs-success-bg-subtle: #ccf2e8;
--bs-info-bg-subtle: #d6eaf8; --bs-info-bg-subtle: #d6eaf8;
--bs-warning-bg-subtle: #fdebd0; --bs-warning-bg-subtle: #fdebd0;
@ -102,7 +102,7 @@ hr.my-3 {
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #99e4d1; --bs-primary-border-subtle: #99e4d1;
--bs-secondary-border-subtle: #b4b4b4; --bs-secondary-border-subtle: #dee1e5;
--bs-success-border-subtle: #99e4d1; --bs-success-border-subtle: #99e4d1;
--bs-info-border-subtle: #aed6f1; --bs-info-border-subtle: #aed6f1;
--bs-warning-border-subtle: #fad7a0; --bs-warning-border-subtle: #fad7a0;
@ -182,7 +182,7 @@ hr.my-3 {
--bs-tertiary-bg: #292929; --bs-tertiary-bg: #292929;
--bs-tertiary-bg-rgb: 41, 41, 41; --bs-tertiary-bg-rgb: 41, 41, 41;
--bs-primary-text-emphasis: #66d7ba; --bs-primary-text-emphasis: #66d7ba;
--bs-secondary-text-emphasis: #8f8f8f; --bs-secondary-text-emphasis: #ced3d7;
--bs-success-text-emphasis: #66d7ba; --bs-success-text-emphasis: #66d7ba;
--bs-info-text-emphasis: #85c1e9; --bs-info-text-emphasis: #85c1e9;
--bs-warning-text-emphasis: #f8c471; --bs-warning-text-emphasis: #f8c471;
@ -190,7 +190,7 @@ hr.my-3 {
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #00261c; --bs-primary-bg-subtle: #00261c;
--bs-secondary-bg-subtle: #0e0e0e; --bs-secondary-bg-subtle: #232426;
--bs-success-bg-subtle: #00261c; --bs-success-bg-subtle: #00261c;
--bs-info-bg-subtle: #0a1e2c; --bs-info-bg-subtle: #0a1e2c;
--bs-warning-bg-subtle: #311f04; --bs-warning-bg-subtle: #311f04;
@ -198,7 +198,7 @@ hr.my-3 {
--bs-light-bg-subtle: #303030; --bs-light-bg-subtle: #303030;
--bs-dark-bg-subtle: #181818; --bs-dark-bg-subtle: #181818;
--bs-primary-border-subtle: #007154; --bs-primary-border-subtle: #007154;
--bs-secondary-border-subtle: #292929; --bs-secondary-border-subtle: #686d71;
--bs-success-border-subtle: #007154; --bs-success-border-subtle: #007154;
--bs-info-border-subtle: #1f5b83; --bs-info-border-subtle: #1f5b83;
--bs-warning-border-subtle: #925e0b; --bs-warning-border-subtle: #925e0b;
@ -1961,13 +1961,13 @@ progress {
.table-secondary { .table-secondary {
--bs-table-color: #000; --bs-table-color: #000;
--bs-table-bg: #dadada; --bs-table-bg: #eff0f2;
--bs-table-border-color: #c4c4c4; --bs-table-border-color: #d7d8da;
--bs-table-striped-bg: #cfcfcf; --bs-table-striped-bg: #e3e4e6;
--bs-table-striped-color: #000; --bs-table-striped-color: #000;
--bs-table-active-bg: #c4c4c4; --bs-table-active-bg: #d7d8da;
--bs-table-active-color: #000; --bs-table-active-color: #000;
--bs-table-hover-bg: #cacaca; --bs-table-hover-bg: #dddee0;
--bs-table-hover-color: #000; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
@ -2994,20 +2994,20 @@ textarea.form-control-lg {
} }
.btn-secondary { .btn-secondary {
--bs-btn-color: #fff; --bs-btn-color: #000;
--bs-btn-bg: #444; --bs-btn-bg: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #3a3a3a; --bs-btn-hover-bg: #b9c0c7;
--bs-btn-hover-border-color: #363636; --bs-btn-hover-border-color: #b5bcc4;
--bs-btn-focus-shadow-rgb: 96, 96, 96; --bs-btn-focus-shadow-rgb: 147, 154, 161;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #363636; --bs-btn-active-bg: #bdc4ca;
--bs-btn-active-border-color: #333333; --bs-btn-active-border-color: #b5bcc4;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: #444; --bs-btn-disabled-bg: #adb5bd;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
} }
.btn-success { .btn-success {
@ -3130,19 +3130,19 @@ textarea.form-control-lg {
} }
.btn-outline-secondary { .btn-outline-secondary {
--bs-btn-color: #444; --bs-btn-color: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #444; --bs-btn-hover-bg: #adb5bd;
--bs-btn-hover-border-color: #444; --bs-btn-hover-border-color: #adb5bd;
--bs-btn-focus-shadow-rgb: 68, 68, 68; --bs-btn-focus-shadow-rgb: 173, 181, 189;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #444; --bs-btn-active-bg: #adb5bd;
--bs-btn-active-border-color: #444; --bs-btn-active-border-color: #adb5bd;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #444; --bs-btn-disabled-color: #adb5bd;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
--bs-gradient: none; --bs-gradient: none;
} }
@ -6777,8 +6777,8 @@ textarea.form-control-lg {
} }
.text-bg-secondary { .text-bg-secondary {
color: #fff !important; color: #000 !important;
background-color: RGBA(68, 68, 68, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(173, 181, 189, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-success { .text-bg-success {
@ -6825,8 +6825,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-secondary:hover, .link-secondary:focus { .link-secondary:hover, .link-secondary:focus {
color: RGBA(54, 54, 54, var(--bs-link-opacity, 1)) !important; color: RGBA(189, 196, 202, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(54, 54, 54, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(189, 196, 202, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-success { .link-success {

View file

@ -30,7 +30,7 @@
--bs-gray-800: #202020; --bs-gray-800: #202020;
--bs-gray-900: #111; --bs-gray-900: #111;
--bs-primary: #00bc8c; --bs-primary: #00bc8c;
--bs-secondary: #333; --bs-secondary: #666;
--bs-success: #00bc8c; --bs-success: #00bc8c;
--bs-info: #3498db; --bs-info: #3498db;
--bs-warning: #f39c12; --bs-warning: #f39c12;
@ -38,7 +38,7 @@
--bs-light: #111; --bs-light: #111;
--bs-dark: #dee2e6; --bs-dark: #dee2e6;
--bs-primary-rgb: 0, 188, 140; --bs-primary-rgb: 0, 188, 140;
--bs-secondary-rgb: 51, 51, 51; --bs-secondary-rgb: 102, 102, 102;
--bs-success-rgb: 0, 188, 140; --bs-success-rgb: 0, 188, 140;
--bs-info-rgb: 52, 152, 219; --bs-info-rgb: 52, 152, 219;
--bs-warning-rgb: 243, 156, 18; --bs-warning-rgb: 243, 156, 18;
@ -46,7 +46,7 @@
--bs-light-rgb: 17, 17, 17; --bs-light-rgb: 17, 17, 17;
--bs-dark-rgb: 222, 226, 230; --bs-dark-rgb: 222, 226, 230;
--bs-primary-text-emphasis: #004b38; --bs-primary-text-emphasis: #004b38;
--bs-secondary-text-emphasis: #141414; --bs-secondary-text-emphasis: #292929;
--bs-success-text-emphasis: #004b38; --bs-success-text-emphasis: #004b38;
--bs-info-text-emphasis: #153d58; --bs-info-text-emphasis: #153d58;
--bs-warning-text-emphasis: #613e07; --bs-warning-text-emphasis: #613e07;
@ -54,7 +54,7 @@
--bs-light-text-emphasis: #333; --bs-light-text-emphasis: #333;
--bs-dark-text-emphasis: #333; --bs-dark-text-emphasis: #333;
--bs-primary-bg-subtle: #ccf2e8; --bs-primary-bg-subtle: #ccf2e8;
--bs-secondary-bg-subtle: #d6d6d6; --bs-secondary-bg-subtle: #e0e0e0;
--bs-success-bg-subtle: #ccf2e8; --bs-success-bg-subtle: #ccf2e8;
--bs-info-bg-subtle: #d6eaf8; --bs-info-bg-subtle: #d6eaf8;
--bs-warning-bg-subtle: #fdebd0; --bs-warning-bg-subtle: #fdebd0;
@ -62,7 +62,7 @@
--bs-light-bg-subtle: #f6f6f7; --bs-light-bg-subtle: #f6f6f7;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #99e4d1; --bs-primary-border-subtle: #99e4d1;
--bs-secondary-border-subtle: #adadad; --bs-secondary-border-subtle: #c2c2c2;
--bs-success-border-subtle: #99e4d1; --bs-success-border-subtle: #99e4d1;
--bs-info-border-subtle: #aed6f1; --bs-info-border-subtle: #aed6f1;
--bs-warning-border-subtle: #fad7a0; --bs-warning-border-subtle: #fad7a0;
@ -142,7 +142,7 @@
--bs-tertiary-bg: #191919; --bs-tertiary-bg: #191919;
--bs-tertiary-bg-rgb: 25, 25, 25; --bs-tertiary-bg-rgb: 25, 25, 25;
--bs-primary-text-emphasis: #66d7ba; --bs-primary-text-emphasis: #66d7ba;
--bs-secondary-text-emphasis: #858585; --bs-secondary-text-emphasis: #a3a3a3;
--bs-success-text-emphasis: #66d7ba; --bs-success-text-emphasis: #66d7ba;
--bs-info-text-emphasis: #85c1e9; --bs-info-text-emphasis: #85c1e9;
--bs-warning-text-emphasis: #f8c471; --bs-warning-text-emphasis: #f8c471;
@ -150,7 +150,7 @@
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #00261c; --bs-primary-bg-subtle: #00261c;
--bs-secondary-bg-subtle: #0a0a0a; --bs-secondary-bg-subtle: #141414;
--bs-success-bg-subtle: #00261c; --bs-success-bg-subtle: #00261c;
--bs-info-bg-subtle: #0a1e2c; --bs-info-bg-subtle: #0a1e2c;
--bs-warning-bg-subtle: #311f04; --bs-warning-bg-subtle: #311f04;
@ -158,7 +158,7 @@
--bs-light-bg-subtle: #202020; --bs-light-bg-subtle: #202020;
--bs-dark-bg-subtle: #101010; --bs-dark-bg-subtle: #101010;
--bs-primary-border-subtle: #007154; --bs-primary-border-subtle: #007154;
--bs-secondary-border-subtle: #1f1f1f; --bs-secondary-border-subtle: #3d3d3d;
--bs-success-border-subtle: #007154; --bs-success-border-subtle: #007154;
--bs-info-border-subtle: #1f5b83; --bs-info-border-subtle: #1f5b83;
--bs-warning-border-subtle: #925e0b; --bs-warning-border-subtle: #925e0b;
@ -1945,13 +1945,13 @@ progress {
.table-secondary { .table-secondary {
--bs-table-color: #000; --bs-table-color: #000;
--bs-table-bg: #d6d6d6; --bs-table-bg: #e0e0e0;
--bs-table-border-color: #c1c1c1; --bs-table-border-color: #cacaca;
--bs-table-striped-bg: #cbcbcb; --bs-table-striped-bg: #d5d5d5;
--bs-table-striped-color: #000; --bs-table-striped-color: #000;
--bs-table-active-bg: #c1c1c1; --bs-table-active-bg: #cacaca;
--bs-table-active-color: #000; --bs-table-active-color: #000;
--bs-table-hover-bg: #c6c6c6; --bs-table-hover-bg: #cfcfcf;
--bs-table-hover-color: #000; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
@ -2979,19 +2979,19 @@ textarea.form-control-lg {
.btn-secondary { .btn-secondary {
--bs-btn-color: #f3f3f3; --bs-btn-color: #f3f3f3;
--bs-btn-bg: #333; --bs-btn-bg: #666;
--bs-btn-border-color: #333; --bs-btn-border-color: #666;
--bs-btn-hover-color: #f3f3f3; --bs-btn-hover-color: #f3f3f3;
--bs-btn-hover-bg: #2b2b2b; --bs-btn-hover-bg: #575757;
--bs-btn-hover-border-color: #292929; --bs-btn-hover-border-color: #525252;
--bs-btn-focus-shadow-rgb: 80, 80, 80; --bs-btn-focus-shadow-rgb: 123, 123, 123;
--bs-btn-active-color: #f3f3f3; --bs-btn-active-color: #f3f3f3;
--bs-btn-active-bg: #292929; --bs-btn-active-bg: #525252;
--bs-btn-active-border-color: #262626; --bs-btn-active-border-color: #4d4d4d;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #f3f3f3; --bs-btn-disabled-color: #f3f3f3;
--bs-btn-disabled-bg: #333; --bs-btn-disabled-bg: #666;
--bs-btn-disabled-border-color: #333; --bs-btn-disabled-border-color: #666;
} }
.btn-success { .btn-success {
@ -3114,19 +3114,19 @@ textarea.form-control-lg {
} }
.btn-outline-secondary { .btn-outline-secondary {
--bs-btn-color: #333; --bs-btn-color: #666;
--bs-btn-border-color: #333; --bs-btn-border-color: #666;
--bs-btn-hover-color: #f3f3f3; --bs-btn-hover-color: #f3f3f3;
--bs-btn-hover-bg: #333; --bs-btn-hover-bg: #666;
--bs-btn-hover-border-color: #333; --bs-btn-hover-border-color: #666;
--bs-btn-focus-shadow-rgb: 51, 51, 51; --bs-btn-focus-shadow-rgb: 102, 102, 102;
--bs-btn-active-color: #f3f3f3; --bs-btn-active-color: #f3f3f3;
--bs-btn-active-bg: #333; --bs-btn-active-bg: #666;
--bs-btn-active-border-color: #333; --bs-btn-active-border-color: #666;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #333; --bs-btn-disabled-color: #666;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #333; --bs-btn-disabled-border-color: #666;
--bs-gradient: none; --bs-gradient: none;
} }
@ -6766,7 +6766,7 @@ textarea.form-control-lg {
.text-bg-secondary { .text-bg-secondary {
color: #f3f3f3 !important; color: #f3f3f3 !important;
background-color: RGBA(51, 51, 51, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(102, 102, 102, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-success { .text-bg-success {
@ -6813,8 +6813,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-secondary:hover, .link-secondary:focus { .link-secondary:hover, .link-secondary:focus {
color: RGBA(41, 41, 41, var(--bs-link-opacity, 1)) !important; color: RGBA(82, 82, 82, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(41, 41, 41, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(82, 82, 82, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-success { .link-success {

View file

@ -30,7 +30,7 @@
--bs-gray-800: #303030; --bs-gray-800: #303030;
--bs-gray-900: #222; --bs-gray-900: #222;
--bs-primary: #375a7f; --bs-primary: #375a7f;
--bs-secondary: #444; --bs-secondary: #adb5bd;
--bs-success: #00bc8c; --bs-success: #00bc8c;
--bs-info: #3498db; --bs-info: #3498db;
--bs-warning: #f39c12; --bs-warning: #f39c12;
@ -38,7 +38,7 @@
--bs-light: #303030; --bs-light: #303030;
--bs-dark: #dee2e6; --bs-dark: #dee2e6;
--bs-primary-rgb: 55, 90, 127; --bs-primary-rgb: 55, 90, 127;
--bs-secondary-rgb: 68, 68, 68; --bs-secondary-rgb: 173, 181, 189;
--bs-success-rgb: 0, 188, 140; --bs-success-rgb: 0, 188, 140;
--bs-info-rgb: 52, 152, 219; --bs-info-rgb: 52, 152, 219;
--bs-warning-rgb: 243, 156, 18; --bs-warning-rgb: 243, 156, 18;
@ -46,7 +46,7 @@
--bs-light-rgb: 48, 48, 48; --bs-light-rgb: 48, 48, 48;
--bs-dark-rgb: 222, 226, 230; --bs-dark-rgb: 222, 226, 230;
--bs-primary-text-emphasis: #162433; --bs-primary-text-emphasis: #162433;
--bs-secondary-text-emphasis: #1b1b1b; --bs-secondary-text-emphasis: #45484c;
--bs-success-text-emphasis: #004b38; --bs-success-text-emphasis: #004b38;
--bs-info-text-emphasis: #153d58; --bs-info-text-emphasis: #153d58;
--bs-warning-text-emphasis: #613e07; --bs-warning-text-emphasis: #613e07;
@ -54,7 +54,7 @@
--bs-light-text-emphasis: #444; --bs-light-text-emphasis: #444;
--bs-dark-text-emphasis: #444; --bs-dark-text-emphasis: #444;
--bs-primary-bg-subtle: #d7dee5; --bs-primary-bg-subtle: #d7dee5;
--bs-secondary-bg-subtle: #dadada; --bs-secondary-bg-subtle: #eff0f2;
--bs-success-bg-subtle: #ccf2e8; --bs-success-bg-subtle: #ccf2e8;
--bs-info-bg-subtle: #d6eaf8; --bs-info-bg-subtle: #d6eaf8;
--bs-warning-bg-subtle: #fdebd0; --bs-warning-bg-subtle: #fdebd0;
@ -62,7 +62,7 @@
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #afbdcc; --bs-primary-border-subtle: #afbdcc;
--bs-secondary-border-subtle: #b4b4b4; --bs-secondary-border-subtle: #dee1e5;
--bs-success-border-subtle: #99e4d1; --bs-success-border-subtle: #99e4d1;
--bs-info-border-subtle: #aed6f1; --bs-info-border-subtle: #aed6f1;
--bs-warning-border-subtle: #fad7a0; --bs-warning-border-subtle: #fad7a0;
@ -142,7 +142,7 @@
--bs-tertiary-bg: #292929; --bs-tertiary-bg: #292929;
--bs-tertiary-bg-rgb: 41, 41, 41; --bs-tertiary-bg-rgb: 41, 41, 41;
--bs-primary-text-emphasis: #879cb2; --bs-primary-text-emphasis: #879cb2;
--bs-secondary-text-emphasis: #8f8f8f; --bs-secondary-text-emphasis: #ced3d7;
--bs-success-text-emphasis: #66d7ba; --bs-success-text-emphasis: #66d7ba;
--bs-info-text-emphasis: #85c1e9; --bs-info-text-emphasis: #85c1e9;
--bs-warning-text-emphasis: #f8c471; --bs-warning-text-emphasis: #f8c471;
@ -150,7 +150,7 @@
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #0b1219; --bs-primary-bg-subtle: #0b1219;
--bs-secondary-bg-subtle: #0e0e0e; --bs-secondary-bg-subtle: #232426;
--bs-success-bg-subtle: #00261c; --bs-success-bg-subtle: #00261c;
--bs-info-bg-subtle: #0a1e2c; --bs-info-bg-subtle: #0a1e2c;
--bs-warning-bg-subtle: #311f04; --bs-warning-bg-subtle: #311f04;
@ -158,7 +158,7 @@
--bs-light-bg-subtle: #303030; --bs-light-bg-subtle: #303030;
--bs-dark-bg-subtle: #181818; --bs-dark-bg-subtle: #181818;
--bs-primary-border-subtle: #21364c; --bs-primary-border-subtle: #21364c;
--bs-secondary-border-subtle: #292929; --bs-secondary-border-subtle: #686d71;
--bs-success-border-subtle: #007154; --bs-success-border-subtle: #007154;
--bs-info-border-subtle: #1f5b83; --bs-info-border-subtle: #1f5b83;
--bs-warning-border-subtle: #925e0b; --bs-warning-border-subtle: #925e0b;
@ -1945,13 +1945,13 @@ progress {
.table-secondary { .table-secondary {
--bs-table-color: #000; --bs-table-color: #000;
--bs-table-bg: #dadada; --bs-table-bg: #eff0f2;
--bs-table-border-color: #c4c4c4; --bs-table-border-color: #d7d8da;
--bs-table-striped-bg: #cfcfcf; --bs-table-striped-bg: #e3e4e6;
--bs-table-striped-color: #000; --bs-table-striped-color: #000;
--bs-table-active-bg: #c4c4c4; --bs-table-active-bg: #d7d8da;
--bs-table-active-color: #000; --bs-table-active-color: #000;
--bs-table-hover-bg: #cacaca; --bs-table-hover-bg: #dddee0;
--bs-table-hover-color: #000; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
@ -2978,20 +2978,20 @@ textarea.form-control-lg {
} }
.btn-secondary { .btn-secondary {
--bs-btn-color: #fff; --bs-btn-color: #000;
--bs-btn-bg: #444; --bs-btn-bg: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #3a3a3a; --bs-btn-hover-bg: #b9c0c7;
--bs-btn-hover-border-color: #363636; --bs-btn-hover-border-color: #b5bcc4;
--bs-btn-focus-shadow-rgb: 96, 96, 96; --bs-btn-focus-shadow-rgb: 147, 154, 161;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #363636; --bs-btn-active-bg: #bdc4ca;
--bs-btn-active-border-color: #333333; --bs-btn-active-border-color: #b5bcc4;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: #444; --bs-btn-disabled-bg: #adb5bd;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
} }
.btn-success { .btn-success {
@ -3114,19 +3114,19 @@ textarea.form-control-lg {
} }
.btn-outline-secondary { .btn-outline-secondary {
--bs-btn-color: #444; --bs-btn-color: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #444; --bs-btn-hover-bg: #adb5bd;
--bs-btn-hover-border-color: #444; --bs-btn-hover-border-color: #adb5bd;
--bs-btn-focus-shadow-rgb: 68, 68, 68; --bs-btn-focus-shadow-rgb: 173, 181, 189;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #444; --bs-btn-active-bg: #adb5bd;
--bs-btn-active-border-color: #444; --bs-btn-active-border-color: #adb5bd;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #444; --bs-btn-disabled-color: #adb5bd;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
--bs-gradient: none; --bs-gradient: none;
} }
@ -6765,8 +6765,8 @@ textarea.form-control-lg {
} }
.text-bg-secondary { .text-bg-secondary {
color: #fff !important; color: #000 !important;
background-color: RGBA(68, 68, 68, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(173, 181, 189, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-success { .text-bg-success {
@ -6813,8 +6813,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-secondary:hover, .link-secondary:focus { .link-secondary:hover, .link-secondary:focus {
color: RGBA(54, 54, 54, var(--bs-link-opacity, 1)) !important; color: RGBA(189, 196, 202, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(54, 54, 54, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(189, 196, 202, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-success { .link-success {

View file

@ -30,7 +30,7 @@
--bs-gray-800: #303030; --bs-gray-800: #303030;
--bs-gray-900: #222; --bs-gray-900: #222;
--bs-primary: #00bc8c; --bs-primary: #00bc8c;
--bs-secondary: #444; --bs-secondary: #adb5bd;
--bs-success: #00bc8c; --bs-success: #00bc8c;
--bs-info: #3498db; --bs-info: #3498db;
--bs-warning: #f39c12; --bs-warning: #f39c12;
@ -38,7 +38,7 @@
--bs-light: #303030; --bs-light: #303030;
--bs-dark: #dee2e6; --bs-dark: #dee2e6;
--bs-primary-rgb: 0, 188, 140; --bs-primary-rgb: 0, 188, 140;
--bs-secondary-rgb: 68, 68, 68; --bs-secondary-rgb: 173, 181, 189;
--bs-success-rgb: 0, 188, 140; --bs-success-rgb: 0, 188, 140;
--bs-info-rgb: 52, 152, 219; --bs-info-rgb: 52, 152, 219;
--bs-warning-rgb: 243, 156, 18; --bs-warning-rgb: 243, 156, 18;
@ -46,7 +46,7 @@
--bs-light-rgb: 48, 48, 48; --bs-light-rgb: 48, 48, 48;
--bs-dark-rgb: 222, 226, 230; --bs-dark-rgb: 222, 226, 230;
--bs-primary-text-emphasis: #004b38; --bs-primary-text-emphasis: #004b38;
--bs-secondary-text-emphasis: #1b1b1b; --bs-secondary-text-emphasis: #45484c;
--bs-success-text-emphasis: #004b38; --bs-success-text-emphasis: #004b38;
--bs-info-text-emphasis: #153d58; --bs-info-text-emphasis: #153d58;
--bs-warning-text-emphasis: #613e07; --bs-warning-text-emphasis: #613e07;
@ -54,7 +54,7 @@
--bs-light-text-emphasis: #444; --bs-light-text-emphasis: #444;
--bs-dark-text-emphasis: #444; --bs-dark-text-emphasis: #444;
--bs-primary-bg-subtle: #ccf2e8; --bs-primary-bg-subtle: #ccf2e8;
--bs-secondary-bg-subtle: #dadada; --bs-secondary-bg-subtle: #eff0f2;
--bs-success-bg-subtle: #ccf2e8; --bs-success-bg-subtle: #ccf2e8;
--bs-info-bg-subtle: #d6eaf8; --bs-info-bg-subtle: #d6eaf8;
--bs-warning-bg-subtle: #fdebd0; --bs-warning-bg-subtle: #fdebd0;
@ -62,7 +62,7 @@
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #99e4d1; --bs-primary-border-subtle: #99e4d1;
--bs-secondary-border-subtle: #b4b4b4; --bs-secondary-border-subtle: #dee1e5;
--bs-success-border-subtle: #99e4d1; --bs-success-border-subtle: #99e4d1;
--bs-info-border-subtle: #aed6f1; --bs-info-border-subtle: #aed6f1;
--bs-warning-border-subtle: #fad7a0; --bs-warning-border-subtle: #fad7a0;
@ -142,7 +142,7 @@
--bs-tertiary-bg: #292929; --bs-tertiary-bg: #292929;
--bs-tertiary-bg-rgb: 41, 41, 41; --bs-tertiary-bg-rgb: 41, 41, 41;
--bs-primary-text-emphasis: #66d7ba; --bs-primary-text-emphasis: #66d7ba;
--bs-secondary-text-emphasis: #8f8f8f; --bs-secondary-text-emphasis: #ced3d7;
--bs-success-text-emphasis: #66d7ba; --bs-success-text-emphasis: #66d7ba;
--bs-info-text-emphasis: #85c1e9; --bs-info-text-emphasis: #85c1e9;
--bs-warning-text-emphasis: #f8c471; --bs-warning-text-emphasis: #f8c471;
@ -150,7 +150,7 @@
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #00261c; --bs-primary-bg-subtle: #00261c;
--bs-secondary-bg-subtle: #0e0e0e; --bs-secondary-bg-subtle: #232426;
--bs-success-bg-subtle: #00261c; --bs-success-bg-subtle: #00261c;
--bs-info-bg-subtle: #0a1e2c; --bs-info-bg-subtle: #0a1e2c;
--bs-warning-bg-subtle: #311f04; --bs-warning-bg-subtle: #311f04;
@ -158,7 +158,7 @@
--bs-light-bg-subtle: #303030; --bs-light-bg-subtle: #303030;
--bs-dark-bg-subtle: #181818; --bs-dark-bg-subtle: #181818;
--bs-primary-border-subtle: #007154; --bs-primary-border-subtle: #007154;
--bs-secondary-border-subtle: #292929; --bs-secondary-border-subtle: #686d71;
--bs-success-border-subtle: #007154; --bs-success-border-subtle: #007154;
--bs-info-border-subtle: #1f5b83; --bs-info-border-subtle: #1f5b83;
--bs-warning-border-subtle: #925e0b; --bs-warning-border-subtle: #925e0b;
@ -1945,13 +1945,13 @@ progress {
.table-secondary { .table-secondary {
--bs-table-color: #000; --bs-table-color: #000;
--bs-table-bg: #dadada; --bs-table-bg: #eff0f2;
--bs-table-border-color: #c4c4c4; --bs-table-border-color: #d7d8da;
--bs-table-striped-bg: #cfcfcf; --bs-table-striped-bg: #e3e4e6;
--bs-table-striped-color: #000; --bs-table-striped-color: #000;
--bs-table-active-bg: #c4c4c4; --bs-table-active-bg: #d7d8da;
--bs-table-active-color: #000; --bs-table-active-color: #000;
--bs-table-hover-bg: #cacaca; --bs-table-hover-bg: #dddee0;
--bs-table-hover-color: #000; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
@ -2978,20 +2978,20 @@ textarea.form-control-lg {
} }
.btn-secondary { .btn-secondary {
--bs-btn-color: #fff; --bs-btn-color: #000;
--bs-btn-bg: #444; --bs-btn-bg: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #3a3a3a; --bs-btn-hover-bg: #b9c0c7;
--bs-btn-hover-border-color: #363636; --bs-btn-hover-border-color: #b5bcc4;
--bs-btn-focus-shadow-rgb: 96, 96, 96; --bs-btn-focus-shadow-rgb: 147, 154, 161;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #363636; --bs-btn-active-bg: #bdc4ca;
--bs-btn-active-border-color: #333333; --bs-btn-active-border-color: #b5bcc4;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: #444; --bs-btn-disabled-bg: #adb5bd;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
} }
.btn-success { .btn-success {
@ -3114,19 +3114,19 @@ textarea.form-control-lg {
} }
.btn-outline-secondary { .btn-outline-secondary {
--bs-btn-color: #444; --bs-btn-color: #adb5bd;
--bs-btn-border-color: #444; --bs-btn-border-color: #adb5bd;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #444; --bs-btn-hover-bg: #adb5bd;
--bs-btn-hover-border-color: #444; --bs-btn-hover-border-color: #adb5bd;
--bs-btn-focus-shadow-rgb: 68, 68, 68; --bs-btn-focus-shadow-rgb: 173, 181, 189;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #444; --bs-btn-active-bg: #adb5bd;
--bs-btn-active-border-color: #444; --bs-btn-active-border-color: #adb5bd;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #444; --bs-btn-disabled-color: #adb5bd;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #444; --bs-btn-disabled-border-color: #adb5bd;
--bs-gradient: none; --bs-gradient: none;
} }
@ -6765,8 +6765,8 @@ textarea.form-control-lg {
} }
.text-bg-secondary { .text-bg-secondary {
color: #fff !important; color: #000 !important;
background-color: RGBA(68, 68, 68, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(173, 181, 189, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-success { .text-bg-success {
@ -6813,8 +6813,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-secondary:hover, .link-secondary:focus { .link-secondary:hover, .link-secondary:focus {
color: RGBA(54, 54, 54, var(--bs-link-opacity, 1)) !important; color: RGBA(189, 196, 202, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(54, 54, 54, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(189, 196, 202, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-success { .link-success {

View file

@ -26,35 +26,35 @@
--bs-gray-400: #ced4da; --bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd; --bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d; --bs-gray-600: #6c757d;
--bs-gray-700: #495057; --bs-gray-700: #444;
--bs-gray-800: #303030; --bs-gray-800: #303030;
--bs-gray-900: #222; --bs-gray-900: #2f2f2f;
--bs-primary: #fefe54; --bs-primary: #fefe54;
--bs-secondary: #222; --bs-secondary: #303030;
--bs-success: #00aa00; --bs-success: #00aa00;
--bs-info: #00aaaa; --bs-info: #00aaaa;
--bs-warning: #aa00aa; --bs-warning: #aa00aa;
--bs-danger: #aa0000; --bs-danger: #aa0000;
--bs-light: #303030; --bs-light: #444;
--bs-dark: black; --bs-dark: #bbb;
--bs-primary-rgb: 254, 254, 84; --bs-primary-rgb: 254, 254, 84;
--bs-secondary-rgb: 34, 34, 34; --bs-secondary-rgb: 48, 48, 48;
--bs-success-rgb: 0, 170, 0; --bs-success-rgb: 0, 170, 0;
--bs-info-rgb: 0, 170, 170; --bs-info-rgb: 0, 170, 170;
--bs-warning-rgb: 170, 0, 170; --bs-warning-rgb: 170, 0, 170;
--bs-danger-rgb: 170, 0, 0; --bs-danger-rgb: 170, 0, 0;
--bs-light-rgb: 48, 48, 48; --bs-light-rgb: 68, 68, 68;
--bs-dark-rgb: 0, 0, 0; --bs-dark-rgb: 187, 187, 187;
--bs-primary-text-emphasis: #666622; --bs-primary-text-emphasis: #666622;
--bs-secondary-text-emphasis: #0e0e0e; --bs-secondary-text-emphasis: #131313;
--bs-success-text-emphasis: #004400; --bs-success-text-emphasis: #004400;
--bs-info-text-emphasis: #004444; --bs-info-text-emphasis: #004444;
--bs-warning-text-emphasis: #440044; --bs-warning-text-emphasis: #440044;
--bs-danger-text-emphasis: #440000; --bs-danger-text-emphasis: #440000;
--bs-light-text-emphasis: #495057; --bs-light-text-emphasis: #444;
--bs-dark-text-emphasis: #495057; --bs-dark-text-emphasis: #444;
--bs-primary-bg-subtle: #ffffdd; --bs-primary-bg-subtle: #ffffdd;
--bs-secondary-bg-subtle: lightgray; --bs-secondary-bg-subtle: #d6d6d6;
--bs-success-bg-subtle: #cceecc; --bs-success-bg-subtle: #cceecc;
--bs-info-bg-subtle: #cceeee; --bs-info-bg-subtle: #cceeee;
--bs-warning-bg-subtle: #eeccee; --bs-warning-bg-subtle: #eeccee;
@ -62,7 +62,7 @@
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #ffffbb; --bs-primary-border-subtle: #ffffbb;
--bs-secondary-border-subtle: #a7a7a7; --bs-secondary-border-subtle: #acacac;
--bs-success-border-subtle: #99dd99; --bs-success-border-subtle: #99dd99;
--bs-info-border-subtle: #99dddd; --bs-info-border-subtle: #99dddd;
--bs-warning-border-subtle: #dd99dd; --bs-warning-border-subtle: #dd99dd;
@ -129,8 +129,8 @@
color-scheme: dark; color-scheme: dark;
--bs-body-color: #adb5bd; --bs-body-color: #adb5bd;
--bs-body-color-rgb: 173, 181, 189; --bs-body-color-rgb: 173, 181, 189;
--bs-body-bg: #222; --bs-body-bg: #2f2f2f;
--bs-body-bg-rgb: 34, 34, 34; --bs-body-bg-rgb: 47, 47, 47;
--bs-emphasis-color: #fff; --bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255; --bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(173, 181, 189, 0.75); --bs-secondary-color: rgba(173, 181, 189, 0.75);
@ -139,10 +139,10 @@
--bs-secondary-bg-rgb: 48, 48, 48; --bs-secondary-bg-rgb: 48, 48, 48;
--bs-tertiary-color: rgba(173, 181, 189, 0.5); --bs-tertiary-color: rgba(173, 181, 189, 0.5);
--bs-tertiary-color-rgb: 173, 181, 189; --bs-tertiary-color-rgb: 173, 181, 189;
--bs-tertiary-bg: #292929; --bs-tertiary-bg: #303030;
--bs-tertiary-bg-rgb: 41, 41, 41; --bs-tertiary-bg-rgb: 48, 48, 48;
--bs-primary-text-emphasis: #fefe98; --bs-primary-text-emphasis: #fefe98;
--bs-secondary-text-emphasis: #7a7a7a; --bs-secondary-text-emphasis: #838383;
--bs-success-text-emphasis: #66cc66; --bs-success-text-emphasis: #66cc66;
--bs-info-text-emphasis: #66cccc; --bs-info-text-emphasis: #66cccc;
--bs-warning-text-emphasis: #cc66cc; --bs-warning-text-emphasis: #cc66cc;
@ -150,7 +150,7 @@
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #bbb; --bs-dark-text-emphasis: #bbb;
--bs-primary-bg-subtle: #333311; --bs-primary-bg-subtle: #333311;
--bs-secondary-bg-subtle: #070707; --bs-secondary-bg-subtle: #0a0a0a;
--bs-success-bg-subtle: #002200; --bs-success-bg-subtle: #002200;
--bs-info-bg-subtle: #002222; --bs-info-bg-subtle: #002222;
--bs-warning-bg-subtle: #220022; --bs-warning-bg-subtle: #220022;
@ -158,12 +158,12 @@
--bs-light-bg-subtle: #303030; --bs-light-bg-subtle: #303030;
--bs-dark-bg-subtle: #181818; --bs-dark-bg-subtle: #181818;
--bs-primary-border-subtle: #989832; --bs-primary-border-subtle: #989832;
--bs-secondary-border-subtle: #141414; --bs-secondary-border-subtle: #1d1d1d;
--bs-success-border-subtle: #006600; --bs-success-border-subtle: #006600;
--bs-info-border-subtle: #006666; --bs-info-border-subtle: #006666;
--bs-warning-border-subtle: #660066; --bs-warning-border-subtle: #660066;
--bs-danger-border-subtle: #660000; --bs-danger-border-subtle: #660000;
--bs-light-border-subtle: #495057; --bs-light-border-subtle: #444;
--bs-dark-border-subtle: #303030; --bs-dark-border-subtle: #303030;
--bs-heading-color: inherit; --bs-heading-color: inherit;
--bs-link-color: #fefe98; --bs-link-color: #fefe98;
@ -171,7 +171,7 @@
--bs-link-color-rgb: 254, 254, 152; --bs-link-color-rgb: 254, 254, 152;
--bs-link-hover-color-rgb: 254, 254, 173; --bs-link-hover-color-rgb: 254, 254, 173;
--bs-code-color: #fe98fe; --bs-code-color: #fe98fe;
--bs-border-color: #495057; --bs-border-color: #444;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #99ff99; --bs-form-valid-color: #99ff99;
--bs-form-valid-border-color: #99ff99; --bs-form-valid-border-color: #99ff99;
@ -1942,13 +1942,13 @@ progress {
.table-secondary { .table-secondary {
--bs-table-color: #000; --bs-table-color: #000;
--bs-table-bg: lightgray; --bs-table-bg: #d6d6d6;
--bs-table-border-color: #bebebe; --bs-table-border-color: #c1c1c1;
--bs-table-striped-bg: #c8c8c8; --bs-table-striped-bg: #cbcbcb;
--bs-table-striped-color: #000; --bs-table-striped-color: #000;
--bs-table-active-bg: #bebebe; --bs-table-active-bg: #c1c1c1;
--bs-table-active-color: #000; --bs-table-active-color: #000;
--bs-table-hover-bg: #c3c3c3; --bs-table-hover-bg: #c6c6c6;
--bs-table-hover-color: #000; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
@ -2012,28 +2012,28 @@ progress {
.table-light { .table-light {
--bs-table-color: #fff; --bs-table-color: #fff;
--bs-table-bg: #303030; --bs-table-bg: #444;
--bs-table-border-color: #454545; --bs-table-border-color: #575757;
--bs-table-striped-bg: #3a3a3a; --bs-table-striped-bg: #4d4d4d;
--bs-table-striped-color: #fff; --bs-table-striped-color: #fff;
--bs-table-active-bg: #454545; --bs-table-active-bg: #575757;
--bs-table-active-color: #fff; --bs-table-active-color: #fff;
--bs-table-hover-bg: #404040; --bs-table-hover-bg: #525252;
--bs-table-hover-color: #fff; --bs-table-hover-color: #fff;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
} }
.table-dark { .table-dark {
--bs-table-color: #fff; --bs-table-color: #000;
--bs-table-bg: black; --bs-table-bg: #bbb;
--bs-table-border-color: #1a1a1a; --bs-table-border-color: #a8a8a8;
--bs-table-striped-bg: #0d0d0d; --bs-table-striped-bg: #b2b2b2;
--bs-table-striped-color: #fff; --bs-table-striped-color: #000;
--bs-table-active-bg: #1a1a1a; --bs-table-active-bg: #a8a8a8;
--bs-table-active-color: #fff; --bs-table-active-color: #000;
--bs-table-hover-bg: #131313; --bs-table-hover-bg: #adadad;
--bs-table-hover-color: #fff; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
} }
@ -2933,19 +2933,19 @@ textarea.form-control-lg {
.btn-secondary { .btn-secondary {
--bs-btn-color: #fff; --bs-btn-color: #fff;
--bs-btn-bg: #222; --bs-btn-bg: #303030;
--bs-btn-border-color: #222; --bs-btn-border-color: #303030;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #1d1d1d; --bs-btn-hover-bg: #292929;
--bs-btn-hover-border-color: #1b1b1b; --bs-btn-hover-border-color: #262626;
--bs-btn-focus-shadow-rgb: 67, 67, 67; --bs-btn-focus-shadow-rgb: 79, 79, 79;
--bs-btn-active-color: #fff; --bs-btn-active-color: #fff;
--bs-btn-active-bg: #1b1b1b; --bs-btn-active-bg: #262626;
--bs-btn-active-border-color: #1a1a1a; --bs-btn-active-border-color: #242424;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #222; --bs-btn-disabled-bg: #303030;
--bs-btn-disabled-border-color: #222; --bs-btn-disabled-border-color: #303030;
} }
.btn-success { .btn-success {
@ -3018,36 +3018,36 @@ textarea.form-control-lg {
.btn-light { .btn-light {
--bs-btn-color: #fff; --bs-btn-color: #fff;
--bs-btn-bg: #303030; --bs-btn-bg: #444;
--bs-btn-border-color: #303030; --bs-btn-border-color: #444;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #292929; --bs-btn-hover-bg: #3a3a3a;
--bs-btn-hover-border-color: #262626; --bs-btn-hover-border-color: #363636;
--bs-btn-focus-shadow-rgb: 79, 79, 79; --bs-btn-focus-shadow-rgb: 96, 96, 96;
--bs-btn-active-color: #fff; --bs-btn-active-color: #fff;
--bs-btn-active-bg: #262626; --bs-btn-active-bg: #363636;
--bs-btn-active-border-color: #242424; --bs-btn-active-border-color: #333333;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #303030; --bs-btn-disabled-bg: #444;
--bs-btn-disabled-border-color: #303030; --bs-btn-disabled-border-color: #444;
} }
.btn-dark { .btn-dark {
--bs-btn-color: #fff; --bs-btn-color: #000;
--bs-btn-bg: black; --bs-btn-bg: #bbb;
--bs-btn-border-color: black; --bs-btn-border-color: #bbb;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #262626; --bs-btn-hover-bg: #c5c5c5;
--bs-btn-hover-border-color: #1a1a1a; --bs-btn-hover-border-color: #c2c2c2;
--bs-btn-focus-shadow-rgb: 38, 38, 38; --bs-btn-focus-shadow-rgb: 159, 159, 159;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #333333; --bs-btn-active-bg: #c9c9c9;
--bs-btn-active-border-color: #1a1a1a; --bs-btn-active-border-color: #c2c2c2;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: black; --bs-btn-disabled-bg: #bbb;
--bs-btn-disabled-border-color: black; --bs-btn-disabled-border-color: #bbb;
} }
.btn-outline-primary { .btn-outline-primary {
@ -3068,19 +3068,19 @@ textarea.form-control-lg {
} }
.btn-outline-secondary { .btn-outline-secondary {
--bs-btn-color: #222; --bs-btn-color: #303030;
--bs-btn-border-color: #222; --bs-btn-border-color: #303030;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #222; --bs-btn-hover-bg: #303030;
--bs-btn-hover-border-color: #222; --bs-btn-hover-border-color: #303030;
--bs-btn-focus-shadow-rgb: 34, 34, 34; --bs-btn-focus-shadow-rgb: 48, 48, 48;
--bs-btn-active-color: #fff; --bs-btn-active-color: #fff;
--bs-btn-active-bg: #222; --bs-btn-active-bg: #303030;
--bs-btn-active-border-color: #222; --bs-btn-active-border-color: #303030;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #222; --bs-btn-disabled-color: #303030;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #222; --bs-btn-disabled-border-color: #303030;
--bs-gradient: none; --bs-gradient: none;
} }
@ -3153,36 +3153,36 @@ textarea.form-control-lg {
} }
.btn-outline-light { .btn-outline-light {
--bs-btn-color: #303030; --bs-btn-color: #444;
--bs-btn-border-color: #303030; --bs-btn-border-color: #444;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #303030; --bs-btn-hover-bg: #444;
--bs-btn-hover-border-color: #303030; --bs-btn-hover-border-color: #444;
--bs-btn-focus-shadow-rgb: 48, 48, 48; --bs-btn-focus-shadow-rgb: 68, 68, 68;
--bs-btn-active-color: #fff; --bs-btn-active-color: #fff;
--bs-btn-active-bg: #303030; --bs-btn-active-bg: #444;
--bs-btn-active-border-color: #303030; --bs-btn-active-border-color: #444;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #303030; --bs-btn-disabled-color: #444;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #303030; --bs-btn-disabled-border-color: #444;
--bs-gradient: none; --bs-gradient: none;
} }
.btn-outline-dark { .btn-outline-dark {
--bs-btn-color: black; --bs-btn-color: #bbb;
--bs-btn-border-color: black; --bs-btn-border-color: #bbb;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: black; --bs-btn-hover-bg: #bbb;
--bs-btn-hover-border-color: black; --bs-btn-hover-border-color: #bbb;
--bs-btn-focus-shadow-rgb: 0, 0, 0; --bs-btn-focus-shadow-rgb: 187, 187, 187;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: black; --bs-btn-active-bg: #bbb;
--bs-btn-active-border-color: black; --bs-btn-active-border-color: #bbb;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: black; --bs-btn-disabled-color: #bbb;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: black; --bs-btn-disabled-border-color: #bbb;
--bs-gradient: none; --bs-gradient: none;
} }
@ -6490,7 +6490,7 @@ textarea.form-control-lg {
.text-bg-secondary { .text-bg-secondary {
color: #fff !important; color: #fff !important;
background-color: RGBA(34, 34, 34, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(48, 48, 48, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-success { .text-bg-success {
@ -6515,12 +6515,12 @@ textarea.form-control-lg {
.text-bg-light { .text-bg-light {
color: #fff !important; color: #fff !important;
background-color: RGBA(48, 48, 48, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(68, 68, 68, var(--bs-bg-opacity, 1)) !important;
} }
.text-bg-dark { .text-bg-dark {
color: #fff !important; color: #000 !important;
background-color: RGBA(0, 0, 0, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(187, 187, 187, var(--bs-bg-opacity, 1)) !important;
} }
.link-primary { .link-primary {
@ -6537,8 +6537,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-secondary:hover, .link-secondary:focus { .link-secondary:hover, .link-secondary:focus {
color: RGBA(27, 27, 27, var(--bs-link-opacity, 1)) !important; color: RGBA(38, 38, 38, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(27, 27, 27, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(38, 38, 38, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-success { .link-success {
@ -6582,8 +6582,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-light:hover, .link-light:focus { .link-light:hover, .link-light:focus {
color: RGBA(38, 38, 38, var(--bs-link-opacity, 1)) !important; color: RGBA(54, 54, 54, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(38, 38, 38, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(54, 54, 54, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-dark { .link-dark {
@ -6591,8 +6591,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-dark:hover, .link-dark:focus { .link-dark:hover, .link-dark:focus {
color: RGBA(0, 0, 0, var(--bs-link-opacity, 1)) !important; color: RGBA(201, 201, 201, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(0, 0, 0, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(201, 201, 201, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-body-emphasis { .link-body-emphasis {
@ -11588,7 +11588,7 @@ textarea.form-control-lg {
.dropdown-item.active, .dropdown-item.active,
.dropdown-item:hover, .dropdown-item:hover,
option:disabled { option:disabled {
color: #222; color: #303030;
} }
.input-group-text { .input-group-text {

View file

@ -36,7 +36,7 @@
--bs-warning: #fffb96; --bs-warning: #fffb96;
--bs-danger: rgb(255, 95, 110); --bs-danger: rgb(255, 95, 110);
--bs-light: #444; --bs-light: #444;
--bs-dark: #222; --bs-dark: #ebebeb;
--bs-primary-rgb: 255, 64, 186; --bs-primary-rgb: 255, 64, 186;
--bs-secondary-rgb: 1, 205, 254; --bs-secondary-rgb: 1, 205, 254;
--bs-success-rgb: 5, 255, 161; --bs-success-rgb: 5, 255, 161;
@ -44,7 +44,7 @@
--bs-warning-rgb: 255, 251, 150; --bs-warning-rgb: 255, 251, 150;
--bs-danger-rgb: 255, 95, 110; --bs-danger-rgb: 255, 95, 110;
--bs-light-rgb: 68, 68, 68; --bs-light-rgb: 68, 68, 68;
--bs-dark-rgb: 34, 34, 34; --bs-dark-rgb: 235, 235, 235;
--bs-primary-text-emphasis: #661a4a; --bs-primary-text-emphasis: #661a4a;
--bs-secondary-text-emphasis: #005266; --bs-secondary-text-emphasis: #005266;
--bs-success-text-emphasis: #026640; --bs-success-text-emphasis: #026640;
@ -74,8 +74,9 @@
--bs-font-sans-serif: "Lucida Console", Monaco, monospace; --bs-font-sans-serif: "Lucida Console", Monaco, monospace;
--bs-font-monospace: Arial, "Noto Sans", sans-serif; --bs-font-monospace: Arial, "Noto Sans", sans-serif;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-root-font-size: 93.75%;
--bs-body-font-family: var(--bs-font-sans-serif); --bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.875rem; --bs-body-font-size: 1rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.5; --bs-body-line-height: 1.5;
--bs-body-color: #ebebeb; --bs-body-color: #ebebeb;
@ -184,6 +185,9 @@
box-sizing: border-box; box-sizing: border-box;
} }
:root {
font-size: var(--bs-root-font-size);
}
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
:root { :root {
scroll-behavior: smooth; scroll-behavior: smooth;
@ -220,47 +224,47 @@ h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {
} }
h1, .h1 { h1, .h1 {
font-size: calc(1.34375rem + 1.125vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1, .h1 { h1, .h1 {
font-size: 2.1875rem; font-size: 2.5rem;
} }
} }
h2, .h2 { h2, .h2 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2, .h2 { h2, .h2 {
font-size: 1.75rem; font-size: 2rem;
} }
} }
h3, .h3 { h3, .h3 {
font-size: calc(1.278125rem + 0.3375vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3, .h3 { h3, .h3 {
font-size: 1.53125rem; font-size: 1.75rem;
} }
} }
h4, .h4 { h4, .h4 {
font-size: calc(1.25625rem + 0.075vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4, .h4 { h4, .h4 {
font-size: 1.3125rem; font-size: 1.5rem;
} }
} }
h5, .h5 { h5, .h5 {
font-size: 1.09375rem; font-size: 1.25rem;
} }
h6, .h6 { h6, .h6 {
font-size: 0.875rem; font-size: 1rem;
} }
p { p {
@ -586,7 +590,7 @@ progress {
} }
.lead { .lead {
font-size: 1.09375rem; font-size: 1.25rem;
font-weight: 300; font-weight: 300;
} }
@ -680,7 +684,7 @@ progress {
.blockquote { .blockquote {
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
} }
.blockquote > :last-child { .blockquote > :last-child {
margin-bottom: 0; margin-bottom: 0;
@ -2025,15 +2029,15 @@ progress {
} }
.table-dark { .table-dark {
--bs-table-color: #fff; --bs-table-color: #000;
--bs-table-bg: #222; --bs-table-bg: #ebebeb;
--bs-table-border-color: #383838; --bs-table-border-color: #d4d4d4;
--bs-table-striped-bg: #2d2d2d; --bs-table-striped-bg: #dfdfdf;
--bs-table-striped-color: #fff; --bs-table-striped-color: #000;
--bs-table-active-bg: #383838; --bs-table-active-bg: #d4d4d4;
--bs-table-active-color: #fff; --bs-table-active-color: #000;
--bs-table-hover-bg: #333333; --bs-table-hover-bg: #d9d9d9;
--bs-table-hover-color: #fff; --bs-table-hover-color: #000;
color: var(--bs-table-color); color: var(--bs-table-color);
border-color: var(--bs-table-border-color); border-color: var(--bs-table-border-color);
} }
@ -2088,13 +2092,13 @@ progress {
.col-form-label-lg { .col-form-label-lg {
padding-top: calc(0.5rem + var(--bs-border-width)); padding-top: calc(0.5rem + var(--bs-border-width));
padding-bottom: calc(0.5rem + var(--bs-border-width)); padding-bottom: calc(0.5rem + var(--bs-border-width));
font-size: 1.09375rem; font-size: 1.25rem;
} }
.col-form-label-sm { .col-form-label-sm {
padding-top: calc(0.25rem + var(--bs-border-width)); padding-top: calc(0.25rem + var(--bs-border-width));
padding-bottom: calc(0.25rem + var(--bs-border-width)); padding-bottom: calc(0.25rem + var(--bs-border-width));
font-size: 0.765625rem; font-size: 0.875rem;
} }
.form-text { .form-text {
@ -2107,7 +2111,7 @@ progress {
display: block; display: block;
width: 100%; width: 100%;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: #fff; color: #fff;
@ -2200,7 +2204,7 @@ progress {
.form-control-sm { .form-control-sm {
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
.form-control-sm::file-selector-button { .form-control-sm::file-selector-button {
@ -2212,7 +2216,7 @@ progress {
.form-control-lg { .form-control-lg {
min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
.form-control-lg::file-selector-button { .form-control-lg::file-selector-button {
@ -2259,7 +2263,7 @@ textarea.form-control-lg {
display: block; display: block;
width: 100%; width: 100%;
padding: 0.375rem 2.25rem 0.375rem 0.75rem; padding: 0.375rem 2.25rem 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: #fff; color: #fff;
@ -2300,7 +2304,7 @@ textarea.form-control-lg {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
padding-left: 0.5rem; padding-left: 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
@ -2308,7 +2312,7 @@ textarea.form-control-lg {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
padding-left: 1rem; padding-left: 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
@ -2318,7 +2322,7 @@ textarea.form-control-lg {
.form-check { .form-check {
display: block; display: block;
min-height: 1.3125rem; min-height: 1.5rem;
padding-left: 1.5em; padding-left: 1.5em;
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
} }
@ -2654,7 +2658,7 @@ textarea.form-control-lg {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: #fff; color: #fff;
@ -2670,7 +2674,7 @@ textarea.form-control-lg {
.input-group-lg > .input-group-text, .input-group-lg > .input-group-text,
.input-group-lg > .btn { .input-group-lg > .btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
@ -2679,7 +2683,7 @@ textarea.form-control-lg {
.input-group-sm > .input-group-text, .input-group-sm > .input-group-text,
.input-group-sm > .btn { .input-group-sm > .btn {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
@ -2729,7 +2733,7 @@ textarea.form-control-lg {
max-width: 100%; max-width: 100%;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
margin-top: 0.1rem; margin-top: 0.1rem;
font-size: 0.765625rem; font-size: 0.875rem;
color: #fff; color: #fff;
background-color: var(--bs-success); background-color: var(--bs-success);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
@ -2819,7 +2823,7 @@ textarea.form-control-lg {
max-width: 100%; max-width: 100%;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
margin-top: 0.1rem; margin-top: 0.1rem;
font-size: 0.765625rem; font-size: 0.875rem;
color: #fff; color: #fff;
background-color: var(--bs-danger); background-color: var(--bs-danger);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
@ -2897,7 +2901,7 @@ textarea.form-control-lg {
--bs-btn-padding-x: 0.75rem; --bs-btn-padding-x: 0.75rem;
--bs-btn-padding-y: 0.375rem; --bs-btn-padding-y: 0.375rem;
--bs-btn-font-family: ; --bs-btn-font-family: ;
--bs-btn-font-size: 0.875rem; --bs-btn-font-size: 1rem;
--bs-btn-font-weight: 400; --bs-btn-font-weight: 400;
--bs-btn-line-height: 1.5; --bs-btn-line-height: 1.5;
--bs-btn-color: var(--bs-body-color); --bs-btn-color: var(--bs-body-color);
@ -3095,20 +3099,20 @@ textarea.form-control-lg {
} }
.btn-dark { .btn-dark {
--bs-btn-color: #fff; --bs-btn-color: #000;
--bs-btn-bg: #222; --bs-btn-bg: #ebebeb;
--bs-btn-border-color: #222; --bs-btn-border-color: #ebebeb;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #434343; --bs-btn-hover-bg: #eeeeee;
--bs-btn-hover-border-color: #383838; --bs-btn-hover-border-color: #ededed;
--bs-btn-focus-shadow-rgb: 67, 67, 67; --bs-btn-focus-shadow-rgb: 200, 200, 200;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #4e4e4e; --bs-btn-active-bg: #efefef;
--bs-btn-active-border-color: #383838; --bs-btn-active-border-color: #ededed;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff; --bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: #222; --bs-btn-disabled-bg: #ebebeb;
--bs-btn-disabled-border-color: #222; --bs-btn-disabled-border-color: #ebebeb;
} }
.btn-outline-primary { .btn-outline-primary {
@ -3231,19 +3235,19 @@ textarea.form-control-lg {
} }
.btn-outline-dark { .btn-outline-dark {
--bs-btn-color: #222; --bs-btn-color: #ebebeb;
--bs-btn-border-color: #222; --bs-btn-border-color: #ebebeb;
--bs-btn-hover-color: #fff; --bs-btn-hover-color: #000;
--bs-btn-hover-bg: #222; --bs-btn-hover-bg: #ebebeb;
--bs-btn-hover-border-color: #222; --bs-btn-hover-border-color: #ebebeb;
--bs-btn-focus-shadow-rgb: 34, 34, 34; --bs-btn-focus-shadow-rgb: 235, 235, 235;
--bs-btn-active-color: #fff; --bs-btn-active-color: #000;
--bs-btn-active-bg: #222; --bs-btn-active-bg: #ebebeb;
--bs-btn-active-border-color: #222; --bs-btn-active-border-color: #ebebeb;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #222; --bs-btn-disabled-color: #ebebeb;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #222; --bs-btn-disabled-border-color: #ebebeb;
--bs-gradient: none; --bs-gradient: none;
} }
@ -3273,14 +3277,14 @@ textarea.form-control-lg {
.btn-lg, .btn-group-lg > .btn { .btn-lg, .btn-group-lg > .btn {
--bs-btn-padding-y: 0.5rem; --bs-btn-padding-y: 0.5rem;
--bs-btn-padding-x: 1rem; --bs-btn-padding-x: 1rem;
--bs-btn-font-size: 1.09375rem; --bs-btn-font-size: 1.25rem;
--bs-btn-border-radius: var(--bs-border-radius-lg); --bs-btn-border-radius: var(--bs-border-radius-lg);
} }
.btn-sm, .btn-group-sm > .btn { .btn-sm, .btn-group-sm > .btn {
--bs-btn-padding-y: 0.25rem; --bs-btn-padding-y: 0.25rem;
--bs-btn-padding-x: 0.5rem; --bs-btn-padding-x: 0.5rem;
--bs-btn-font-size: 0.765625rem; --bs-btn-font-size: 0.875rem;
--bs-btn-border-radius: var(--bs-border-radius-sm); --bs-btn-border-radius: var(--bs-border-radius-sm);
} }
@ -3353,7 +3357,7 @@ textarea.form-control-lg {
--bs-dropdown-padding-x: 0; --bs-dropdown-padding-x: 0;
--bs-dropdown-padding-y: 0.5rem; --bs-dropdown-padding-y: 0.5rem;
--bs-dropdown-spacer: 0.125rem; --bs-dropdown-spacer: 0.125rem;
--bs-dropdown-font-size: 0.875rem; --bs-dropdown-font-size: 1rem;
--bs-dropdown-color: var(--bs-body-color); --bs-dropdown-color: var(--bs-body-color);
--bs-dropdown-bg: var(--bs-body-bg); --bs-dropdown-bg: var(--bs-body-bg);
--bs-dropdown-border-color: var(--bs-border-color-translucent); --bs-dropdown-border-color: var(--bs-border-color-translucent);
@ -3615,7 +3619,7 @@ textarea.form-control-lg {
display: block; display: block;
padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);
margin-bottom: 0; margin-bottom: 0;
font-size: 0.765625rem; font-size: 0.875rem;
color: var(--bs-dropdown-header-color); color: var(--bs-dropdown-header-color);
white-space: nowrap; white-space: nowrap;
} }
@ -3900,15 +3904,15 @@ textarea.form-control-lg {
--bs-navbar-hover-color: rgba(255, 64, 186, 0.7); --bs-navbar-hover-color: rgba(255, 64, 186, 0.7);
--bs-navbar-disabled-color: rgba(235, 235, 235, 0.3); --bs-navbar-disabled-color: rgba(235, 235, 235, 0.3);
--bs-navbar-active-color: rgba(235, 235, 235, 0.9); --bs-navbar-active-color: rgba(235, 235, 235, 0.9);
--bs-navbar-brand-padding-y: 0.3359375rem; --bs-navbar-brand-padding-y: 0.3125rem;
--bs-navbar-brand-margin-end: 1rem; --bs-navbar-brand-margin-end: 1rem;
--bs-navbar-brand-font-size: 1.09375rem; --bs-navbar-brand-font-size: 1.25rem;
--bs-navbar-brand-color: rgba(235, 235, 235, 0.9); --bs-navbar-brand-color: rgba(235, 235, 235, 0.9);
--bs-navbar-brand-hover-color: rgba(235, 235, 235, 0.9); --bs-navbar-brand-hover-color: rgba(235, 235, 235, 0.9);
--bs-navbar-nav-link-padding-x: 0.5rem; --bs-navbar-nav-link-padding-x: 0.5rem;
--bs-navbar-toggler-padding-y: 0.25rem; --bs-navbar-toggler-padding-y: 0.25rem;
--bs-navbar-toggler-padding-x: 0.75rem; --bs-navbar-toggler-padding-x: 0.75rem;
--bs-navbar-toggler-font-size: 1.09375rem; --bs-navbar-toggler-font-size: 1.25rem;
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28235, 235, 235, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28235, 235, 235, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);
--bs-navbar-toggler-border-radius: var(--bs-border-radius); --bs-navbar-toggler-border-radius: var(--bs-border-radius);
@ -4545,7 +4549,7 @@ textarea.form-control-lg {
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);
font-size: 0.875rem; font-size: 1rem;
color: var(--bs-accordion-btn-color); color: var(--bs-accordion-btn-color);
text-align: left; text-align: left;
background-color: var(--bs-accordion-btn-bg); background-color: var(--bs-accordion-btn-bg);
@ -4689,7 +4693,7 @@ textarea.form-control-lg {
.pagination { .pagination {
--bs-pagination-padding-x: 0.75rem; --bs-pagination-padding-x: 0.75rem;
--bs-pagination-padding-y: 0.375rem; --bs-pagination-padding-y: 0.375rem;
--bs-pagination-font-size: 0.875rem; --bs-pagination-font-size: 1rem;
--bs-pagination-color: var(--bs-link-color); --bs-pagination-color: var(--bs-link-color);
--bs-pagination-bg: var(--bs-body-bg); --bs-pagination-bg: var(--bs-body-bg);
--bs-pagination-border-width: var(--bs-border-width); --bs-pagination-border-width: var(--bs-border-width);
@ -4769,14 +4773,14 @@ textarea.form-control-lg {
.pagination-lg { .pagination-lg {
--bs-pagination-padding-x: 1.5rem; --bs-pagination-padding-x: 1.5rem;
--bs-pagination-padding-y: 0.75rem; --bs-pagination-padding-y: 0.75rem;
--bs-pagination-font-size: 1.09375rem; --bs-pagination-font-size: 1.25rem;
--bs-pagination-border-radius: var(--bs-border-radius-lg); --bs-pagination-border-radius: var(--bs-border-radius-lg);
} }
.pagination-sm { .pagination-sm {
--bs-pagination-padding-x: 0.5rem; --bs-pagination-padding-x: 0.5rem;
--bs-pagination-padding-y: 0.25rem; --bs-pagination-padding-y: 0.25rem;
--bs-pagination-font-size: 0.765625rem; --bs-pagination-font-size: 0.875rem;
--bs-pagination-border-radius: var(--bs-border-radius-sm); --bs-pagination-border-radius: var(--bs-border-radius-sm);
} }
@ -4911,7 +4915,7 @@ textarea.form-control-lg {
.progress, .progress,
.progress-stacked { .progress-stacked {
--bs-progress-height: 1rem; --bs-progress-height: 1rem;
--bs-progress-font-size: 0.65625rem; --bs-progress-font-size: 0.75rem;
--bs-progress-bg: var(--bs-secondary-bg); --bs-progress-bg: var(--bs-secondary-bg);
--bs-progress-border-radius: var(--bs-border-radius); --bs-progress-border-radius: var(--bs-border-radius);
--bs-progress-box-shadow: var(--bs-box-shadow-inset); --bs-progress-box-shadow: var(--bs-box-shadow-inset);
@ -5717,7 +5721,7 @@ textarea.form-control-lg {
--bs-tooltip-padding-x: 0.5rem; --bs-tooltip-padding-x: 0.5rem;
--bs-tooltip-padding-y: 0.25rem; --bs-tooltip-padding-y: 0.25rem;
--bs-tooltip-margin: ; --bs-tooltip-margin: ;
--bs-tooltip-font-size: 0.765625rem; --bs-tooltip-font-size: 0.875rem;
--bs-tooltip-color: var(--bs-body-bg); --bs-tooltip-color: var(--bs-body-bg);
--bs-tooltip-bg: var(--bs-emphasis-color); --bs-tooltip-bg: var(--bs-emphasis-color);
--bs-tooltip-border-radius: var(--bs-border-radius); --bs-tooltip-border-radius: var(--bs-border-radius);
@ -5816,7 +5820,7 @@ textarea.form-control-lg {
.popover { .popover {
--bs-popover-zindex: 1070; --bs-popover-zindex: 1070;
--bs-popover-max-width: 276px; --bs-popover-max-width: 276px;
--bs-popover-font-size: 0.765625rem; --bs-popover-font-size: 0.875rem;
--bs-popover-bg: var(--bs-body-bg); --bs-popover-bg: var(--bs-body-bg);
--bs-popover-border-width: var(--bs-border-width); --bs-popover-border-width: var(--bs-border-width);
--bs-popover-border-color: var(--bs-border-color-translucent); --bs-popover-border-color: var(--bs-border-color-translucent);
@ -5825,7 +5829,7 @@ textarea.form-control-lg {
--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); --bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-popover-header-padding-x: 1rem; --bs-popover-header-padding-x: 1rem;
--bs-popover-header-padding-y: 0.5rem; --bs-popover-header-padding-y: 0.5rem;
--bs-popover-header-font-size: 0.875rem; --bs-popover-header-font-size: 1rem;
--bs-popover-header-color: inherit; --bs-popover-header-color: inherit;
--bs-popover-header-bg: var(--bs-secondary-bg); --bs-popover-header-bg: var(--bs-secondary-bg);
--bs-popover-body-padding-x: 1rem; --bs-popover-body-padding-x: 1rem;
@ -6844,8 +6848,8 @@ textarea.form-control-lg {
} }
.text-bg-dark { .text-bg-dark {
color: #fff !important; color: #000 !important;
background-color: RGBA(34, 34, 34, var(--bs-bg-opacity, 1)) !important; background-color: RGBA(235, 235, 235, var(--bs-bg-opacity, 1)) !important;
} }
.link-primary { .link-primary {
@ -6916,8 +6920,8 @@ textarea.form-control-lg {
text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;
} }
.link-dark:hover, .link-dark:focus { .link-dark:hover, .link-dark:focus {
color: RGBA(27, 27, 27, var(--bs-link-opacity, 1)) !important; color: RGBA(239, 239, 239, var(--bs-link-opacity, 1)) !important;
text-decoration-color: RGBA(27, 27, 27, var(--bs-link-underline-opacity, 1)) !important; text-decoration-color: RGBA(239, 239, 239, var(--bs-link-underline-opacity, 1)) !important;
} }
.link-body-emphasis { .link-body-emphasis {
@ -8296,27 +8300,27 @@ textarea.form-control-lg {
} }
.fs-1 { .fs-1 {
font-size: calc(1.34375rem + 1.125vw) !important; font-size: calc(1.375rem + 1.5vw) !important;
} }
.fs-2 { .fs-2 {
font-size: calc(1.3rem + 0.6vw) !important; font-size: calc(1.325rem + 0.9vw) !important;
} }
.fs-3 { .fs-3 {
font-size: calc(1.278125rem + 0.3375vw) !important; font-size: calc(1.3rem + 0.6vw) !important;
} }
.fs-4 { .fs-4 {
font-size: calc(1.25625rem + 0.075vw) !important; font-size: calc(1.275rem + 0.3vw) !important;
} }
.fs-5 { .fs-5 {
font-size: 1.09375rem !important; font-size: 1.25rem !important;
} }
.fs-6 { .fs-6 {
font-size: 0.875rem !important; font-size: 1rem !important;
} }
.fst-italic { .fst-italic {
@ -11859,16 +11863,16 @@ textarea.form-control-lg {
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
.fs-1 { .fs-1 {
font-size: 2.1875rem !important; font-size: 2.5rem !important;
} }
.fs-2 { .fs-2 {
font-size: 1.75rem !important; font-size: 2rem !important;
} }
.fs-3 { .fs-3 {
font-size: 1.53125rem !important; font-size: 1.75rem !important;
} }
.fs-4 { .fs-4 {
font-size: 1.3125rem !important; font-size: 1.5rem !important;
} }
} }
@media print { @media print {

View file

@ -23,8 +23,11 @@ option:disabled {
} }
.form-control::placeholder { .form-control::placeholder {
text-shadow: 0.5px 0.5px 0 $secondary, 0.5px -0.5px 0 $secondary, text-shadow:
-0.5px 0.5px 0 $secondary, -0.5px -0.5px 0 $secondary; 0.5px 0.5px 0 $secondary,
0.5px -0.5px 0 $secondary,
-0.5px 0.5px 0 $secondary,
-0.5px -0.5px 0 $secondary;
} }
.input-group-text { .input-group-text {

View file

@ -74,8 +74,9 @@
--bs-font-sans-serif: "Lucida Console", Monaco, monospace; --bs-font-sans-serif: "Lucida Console", Monaco, monospace;
--bs-font-monospace: Arial, "Noto Sans", sans-serif; --bs-font-monospace: Arial, "Noto Sans", sans-serif;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-root-font-size: 93.75%;
--bs-body-font-family: var(--bs-font-sans-serif); --bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.875rem; --bs-body-font-size: 1rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.5; --bs-body-line-height: 1.5;
--bs-body-color: #495057; --bs-body-color: #495057;
@ -184,6 +185,9 @@
box-sizing: border-box; box-sizing: border-box;
} }
:root {
font-size: var(--bs-root-font-size);
}
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
:root { :root {
scroll-behavior: smooth; scroll-behavior: smooth;
@ -220,47 +224,47 @@ h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {
} }
h1, .h1 { h1, .h1 {
font-size: calc(1.34375rem + 1.125vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1, .h1 { h1, .h1 {
font-size: 2.1875rem; font-size: 2.5rem;
} }
} }
h2, .h2 { h2, .h2 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2, .h2 { h2, .h2 {
font-size: 1.75rem; font-size: 2rem;
} }
} }
h3, .h3 { h3, .h3 {
font-size: calc(1.278125rem + 0.3375vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3, .h3 { h3, .h3 {
font-size: 1.53125rem; font-size: 1.75rem;
} }
} }
h4, .h4 { h4, .h4 {
font-size: calc(1.25625rem + 0.075vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4, .h4 { h4, .h4 {
font-size: 1.3125rem; font-size: 1.5rem;
} }
} }
h5, .h5 { h5, .h5 {
font-size: 1.09375rem; font-size: 1.25rem;
} }
h6, .h6 { h6, .h6 {
font-size: 0.875rem; font-size: 1rem;
} }
p { p {
@ -585,7 +589,7 @@ progress {
} }
.lead { .lead {
font-size: 1.09375rem; font-size: 1.25rem;
font-weight: 300; font-weight: 300;
} }
@ -679,7 +683,7 @@ progress {
.blockquote { .blockquote {
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
} }
.blockquote > :last-child { .blockquote > :last-child {
margin-bottom: 0; margin-bottom: 0;
@ -2087,13 +2091,13 @@ progress {
.col-form-label-lg { .col-form-label-lg {
padding-top: calc(0.5rem + var(--bs-border-width)); padding-top: calc(0.5rem + var(--bs-border-width));
padding-bottom: calc(0.5rem + var(--bs-border-width)); padding-bottom: calc(0.5rem + var(--bs-border-width));
font-size: 1.09375rem; font-size: 1.25rem;
} }
.col-form-label-sm { .col-form-label-sm {
padding-top: calc(0.25rem + var(--bs-border-width)); padding-top: calc(0.25rem + var(--bs-border-width));
padding-bottom: calc(0.25rem + var(--bs-border-width)); padding-bottom: calc(0.25rem + var(--bs-border-width));
font-size: 0.765625rem; font-size: 0.875rem;
} }
.form-text { .form-text {
@ -2106,7 +2110,7 @@ progress {
display: block; display: block;
width: 100%; width: 100%;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: var(--bs-body-color); color: var(--bs-body-color);
@ -2199,7 +2203,7 @@ progress {
.form-control-sm { .form-control-sm {
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
.form-control-sm::file-selector-button { .form-control-sm::file-selector-button {
@ -2211,7 +2215,7 @@ progress {
.form-control-lg { .form-control-lg {
min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
.form-control-lg::file-selector-button { .form-control-lg::file-selector-button {
@ -2258,7 +2262,7 @@ textarea.form-control-lg {
display: block; display: block;
width: 100%; width: 100%;
padding: 0.375rem 2.25rem 0.375rem 0.75rem; padding: 0.375rem 2.25rem 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: var(--bs-body-color); color: var(--bs-body-color);
@ -2299,7 +2303,7 @@ textarea.form-control-lg {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
padding-left: 0.5rem; padding-left: 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
@ -2307,7 +2311,7 @@ textarea.form-control-lg {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
padding-left: 1rem; padding-left: 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
@ -2317,7 +2321,7 @@ textarea.form-control-lg {
.form-check { .form-check {
display: block; display: block;
min-height: 1.3125rem; min-height: 1.5rem;
padding-left: 1.5em; padding-left: 1.5em;
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
} }
@ -2653,7 +2657,7 @@ textarea.form-control-lg {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
color: var(--bs-body-color); color: var(--bs-body-color);
@ -2669,7 +2673,7 @@ textarea.form-control-lg {
.input-group-lg > .input-group-text, .input-group-lg > .input-group-text,
.input-group-lg > .btn { .input-group-lg > .btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1.09375rem; font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg); border-radius: var(--bs-border-radius-lg);
} }
@ -2678,7 +2682,7 @@ textarea.form-control-lg {
.input-group-sm > .input-group-text, .input-group-sm > .input-group-text,
.input-group-sm > .btn { .input-group-sm > .btn {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.765625rem; font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm); border-radius: var(--bs-border-radius-sm);
} }
@ -2728,7 +2732,7 @@ textarea.form-control-lg {
max-width: 100%; max-width: 100%;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
margin-top: 0.1rem; margin-top: 0.1rem;
font-size: 0.765625rem; font-size: 0.875rem;
color: #fff; color: #fff;
background-color: var(--bs-success); background-color: var(--bs-success);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
@ -2818,7 +2822,7 @@ textarea.form-control-lg {
max-width: 100%; max-width: 100%;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
margin-top: 0.1rem; margin-top: 0.1rem;
font-size: 0.765625rem; font-size: 0.875rem;
color: #fff; color: #fff;
background-color: var(--bs-danger); background-color: var(--bs-danger);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
@ -2896,7 +2900,7 @@ textarea.form-control-lg {
--bs-btn-padding-x: 0.75rem; --bs-btn-padding-x: 0.75rem;
--bs-btn-padding-y: 0.375rem; --bs-btn-padding-y: 0.375rem;
--bs-btn-font-family: ; --bs-btn-font-family: ;
--bs-btn-font-size: 0.875rem; --bs-btn-font-size: 1rem;
--bs-btn-font-weight: 400; --bs-btn-font-weight: 400;
--bs-btn-line-height: 1.5; --bs-btn-line-height: 1.5;
--bs-btn-color: var(--bs-body-color); --bs-btn-color: var(--bs-body-color);
@ -3272,14 +3276,14 @@ textarea.form-control-lg {
.btn-lg, .btn-group-lg > .btn { .btn-lg, .btn-group-lg > .btn {
--bs-btn-padding-y: 0.5rem; --bs-btn-padding-y: 0.5rem;
--bs-btn-padding-x: 1rem; --bs-btn-padding-x: 1rem;
--bs-btn-font-size: 1.09375rem; --bs-btn-font-size: 1.25rem;
--bs-btn-border-radius: var(--bs-border-radius-lg); --bs-btn-border-radius: var(--bs-border-radius-lg);
} }
.btn-sm, .btn-group-sm > .btn { .btn-sm, .btn-group-sm > .btn {
--bs-btn-padding-y: 0.25rem; --bs-btn-padding-y: 0.25rem;
--bs-btn-padding-x: 0.5rem; --bs-btn-padding-x: 0.5rem;
--bs-btn-font-size: 0.765625rem; --bs-btn-font-size: 0.875rem;
--bs-btn-border-radius: var(--bs-border-radius-sm); --bs-btn-border-radius: var(--bs-border-radius-sm);
} }
@ -3352,7 +3356,7 @@ textarea.form-control-lg {
--bs-dropdown-padding-x: 0; --bs-dropdown-padding-x: 0;
--bs-dropdown-padding-y: 0.5rem; --bs-dropdown-padding-y: 0.5rem;
--bs-dropdown-spacer: 0.125rem; --bs-dropdown-spacer: 0.125rem;
--bs-dropdown-font-size: 0.875rem; --bs-dropdown-font-size: 1rem;
--bs-dropdown-color: var(--bs-body-color); --bs-dropdown-color: var(--bs-body-color);
--bs-dropdown-bg: var(--bs-body-bg); --bs-dropdown-bg: var(--bs-body-bg);
--bs-dropdown-border-color: var(--bs-border-color-translucent); --bs-dropdown-border-color: var(--bs-border-color-translucent);
@ -3614,7 +3618,7 @@ textarea.form-control-lg {
display: block; display: block;
padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);
margin-bottom: 0; margin-bottom: 0;
font-size: 0.765625rem; font-size: 0.875rem;
color: var(--bs-dropdown-header-color); color: var(--bs-dropdown-header-color);
white-space: nowrap; white-space: nowrap;
} }
@ -3899,15 +3903,15 @@ textarea.form-control-lg {
--bs-navbar-hover-color: rgba(255, 64, 186, 0.7); --bs-navbar-hover-color: rgba(255, 64, 186, 0.7);
--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);
--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);
--bs-navbar-brand-padding-y: 0.3359375rem; --bs-navbar-brand-padding-y: 0.3125rem;
--bs-navbar-brand-margin-end: 1rem; --bs-navbar-brand-margin-end: 1rem;
--bs-navbar-brand-font-size: 1.09375rem; --bs-navbar-brand-font-size: 1.25rem;
--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);
--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);
--bs-navbar-nav-link-padding-x: 0.5rem; --bs-navbar-nav-link-padding-x: 0.5rem;
--bs-navbar-toggler-padding-y: 0.25rem; --bs-navbar-toggler-padding-y: 0.25rem;
--bs-navbar-toggler-padding-x: 0.75rem; --bs-navbar-toggler-padding-x: 0.75rem;
--bs-navbar-toggler-font-size: 1.09375rem; --bs-navbar-toggler-font-size: 1.25rem;
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2873, 80, 87, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2873, 80, 87, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);
--bs-navbar-toggler-border-radius: var(--bs-border-radius); --bs-navbar-toggler-border-radius: var(--bs-border-radius);
@ -4544,7 +4548,7 @@ textarea.form-control-lg {
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);
font-size: 0.875rem; font-size: 1rem;
color: var(--bs-accordion-btn-color); color: var(--bs-accordion-btn-color);
text-align: left; text-align: left;
background-color: var(--bs-accordion-btn-bg); background-color: var(--bs-accordion-btn-bg);
@ -4688,7 +4692,7 @@ textarea.form-control-lg {
.pagination { .pagination {
--bs-pagination-padding-x: 0.75rem; --bs-pagination-padding-x: 0.75rem;
--bs-pagination-padding-y: 0.375rem; --bs-pagination-padding-y: 0.375rem;
--bs-pagination-font-size: 0.875rem; --bs-pagination-font-size: 1rem;
--bs-pagination-color: var(--bs-link-color); --bs-pagination-color: var(--bs-link-color);
--bs-pagination-bg: var(--bs-body-bg); --bs-pagination-bg: var(--bs-body-bg);
--bs-pagination-border-width: var(--bs-border-width); --bs-pagination-border-width: var(--bs-border-width);
@ -4768,14 +4772,14 @@ textarea.form-control-lg {
.pagination-lg { .pagination-lg {
--bs-pagination-padding-x: 1.5rem; --bs-pagination-padding-x: 1.5rem;
--bs-pagination-padding-y: 0.75rem; --bs-pagination-padding-y: 0.75rem;
--bs-pagination-font-size: 1.09375rem; --bs-pagination-font-size: 1.25rem;
--bs-pagination-border-radius: var(--bs-border-radius-lg); --bs-pagination-border-radius: var(--bs-border-radius-lg);
} }
.pagination-sm { .pagination-sm {
--bs-pagination-padding-x: 0.5rem; --bs-pagination-padding-x: 0.5rem;
--bs-pagination-padding-y: 0.25rem; --bs-pagination-padding-y: 0.25rem;
--bs-pagination-font-size: 0.765625rem; --bs-pagination-font-size: 0.875rem;
--bs-pagination-border-radius: var(--bs-border-radius-sm); --bs-pagination-border-radius: var(--bs-border-radius-sm);
} }
@ -4910,7 +4914,7 @@ textarea.form-control-lg {
.progress, .progress,
.progress-stacked { .progress-stacked {
--bs-progress-height: 1rem; --bs-progress-height: 1rem;
--bs-progress-font-size: 0.65625rem; --bs-progress-font-size: 0.75rem;
--bs-progress-bg: var(--bs-secondary-bg); --bs-progress-bg: var(--bs-secondary-bg);
--bs-progress-border-radius: var(--bs-border-radius); --bs-progress-border-radius: var(--bs-border-radius);
--bs-progress-box-shadow: var(--bs-box-shadow-inset); --bs-progress-box-shadow: var(--bs-box-shadow-inset);
@ -5716,7 +5720,7 @@ textarea.form-control-lg {
--bs-tooltip-padding-x: 0.5rem; --bs-tooltip-padding-x: 0.5rem;
--bs-tooltip-padding-y: 0.25rem; --bs-tooltip-padding-y: 0.25rem;
--bs-tooltip-margin: ; --bs-tooltip-margin: ;
--bs-tooltip-font-size: 0.765625rem; --bs-tooltip-font-size: 0.875rem;
--bs-tooltip-color: var(--bs-body-bg); --bs-tooltip-color: var(--bs-body-bg);
--bs-tooltip-bg: var(--bs-emphasis-color); --bs-tooltip-bg: var(--bs-emphasis-color);
--bs-tooltip-border-radius: var(--bs-border-radius); --bs-tooltip-border-radius: var(--bs-border-radius);
@ -5815,7 +5819,7 @@ textarea.form-control-lg {
.popover { .popover {
--bs-popover-zindex: 1070; --bs-popover-zindex: 1070;
--bs-popover-max-width: 276px; --bs-popover-max-width: 276px;
--bs-popover-font-size: 0.765625rem; --bs-popover-font-size: 0.875rem;
--bs-popover-bg: var(--bs-body-bg); --bs-popover-bg: var(--bs-body-bg);
--bs-popover-border-width: var(--bs-border-width); --bs-popover-border-width: var(--bs-border-width);
--bs-popover-border-color: var(--bs-border-color-translucent); --bs-popover-border-color: var(--bs-border-color-translucent);
@ -5824,7 +5828,7 @@ textarea.form-control-lg {
--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); --bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-popover-header-padding-x: 1rem; --bs-popover-header-padding-x: 1rem;
--bs-popover-header-padding-y: 0.5rem; --bs-popover-header-padding-y: 0.5rem;
--bs-popover-header-font-size: 0.875rem; --bs-popover-header-font-size: 1rem;
--bs-popover-header-color: inherit; --bs-popover-header-color: inherit;
--bs-popover-header-bg: var(--bs-secondary-bg); --bs-popover-header-bg: var(--bs-secondary-bg);
--bs-popover-body-padding-x: 1rem; --bs-popover-body-padding-x: 1rem;
@ -8295,27 +8299,27 @@ textarea.form-control-lg {
} }
.fs-1 { .fs-1 {
font-size: calc(1.34375rem + 1.125vw) !important; font-size: calc(1.375rem + 1.5vw) !important;
} }
.fs-2 { .fs-2 {
font-size: calc(1.3rem + 0.6vw) !important; font-size: calc(1.325rem + 0.9vw) !important;
} }
.fs-3 { .fs-3 {
font-size: calc(1.278125rem + 0.3375vw) !important; font-size: calc(1.3rem + 0.6vw) !important;
} }
.fs-4 { .fs-4 {
font-size: calc(1.25625rem + 0.075vw) !important; font-size: calc(1.275rem + 0.3vw) !important;
} }
.fs-5 { .fs-5 {
font-size: 1.09375rem !important; font-size: 1.25rem !important;
} }
.fs-6 { .fs-6 {
font-size: 0.875rem !important; font-size: 1rem !important;
} }
.fst-italic { .fst-italic {
@ -11858,16 +11862,16 @@ textarea.form-control-lg {
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
.fs-1 { .fs-1 {
font-size: 2.1875rem !important; font-size: 2.5rem !important;
} }
.fs-2 { .fs-2 {
font-size: 1.75rem !important; font-size: 2rem !important;
} }
.fs-3 { .fs-3 {
font-size: 1.53125rem !important; font-size: 1.75rem !important;
} }
.fs-4 { .fs-4 {
font-size: 1.3125rem !important; font-size: 1.5rem !important;
} }
} }
@media print { @media print {

View file

@ -1,118 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024" width="1024"
height="1024" height="1024"
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
version="1.1" version="1.1">
id="svg8" <g transform="translate(0,-26.066658)">
inkscape:version="0.92.4 (unknown)"
sodipodi:docname="lemmy-logo-border.svg"
inkscape:export-filename="/home/andres/Pictures/References/Logos/Lemmy/lemmy-logo-border.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
enable-background="new">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="452.38625"
inkscape:cy="470.53357"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:showpageshadow="false"
inkscape:window-width="1366"
inkscape:window-height="740"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true"
inkscape:snap-midpoints="false"
inkscape:snap-smooth-nodes="false"
inkscape:object-paths="false"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-26.066658)"
style="display:inline">
<path <path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z" d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z" />
id="path817-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccssccccccscccccscccscccscccccsccscccssccscscccscc"
inkscape:label="white-border"
sodipodi:insensitive="true" />
<path
id="path1087"
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
inkscape:connector-curvature="0"
inkscape:label="ears"
sodipodi:insensitive="true" />
<path <path
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z" d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473" />
id="path969" <path
inkscape:connector-curvature="0" style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="szszs" d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z" />
inkscape:label="head"
sodipodi:insensitive="true" />
<path <path
id="path1084"
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z" d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z" />
inkscape:connector-curvature="0"
inkscape:label="eyes"
sodipodi:nodetypes="ssssssssss"
sodipodi:insensitive="true" />
<path <path
id="path1008"
style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351" d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351" />
inkscape:connector-curvature="0"
inkscape:label="whiskers"
sodipodi:nodetypes="cccccccc"
sodipodi:insensitive="true" />
<path <path
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z" d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z" />
id="path1115"
inkscape:connector-curvature="0"
inkscape:label="nose"
sodipodi:nodetypes="zszsz"
sodipodi:insensitive="true" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -142,8 +142,8 @@
<symbol id="icon-book-open" viewBox="0 0 24 24"> <symbol id="icon-book-open" viewBox="0 0 24 24">
<path d="M21 4v13h-6c-0.728 0-1.412 0.195-2 0.535v-10.535c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879zM11 17.535c-0.588-0.34-1.272-0.535-2-0.535h-6v-13h5c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121zM22 2h-6c-1.38 0-2.632 0.561-3.536 1.464-0.167 0.167-0.322 0.346-0.464 0.536-0.142-0.19-0.297-0.369-0.464-0.536-0.904-0.903-2.156-1.464-3.536-1.464h-6c-0.552 0-1 0.448-1 1v15c0 0.552 0.448 1 1 1h7c0.553 0 1.051 0.223 1.414 0.586s0.586 0.861 0.586 1.414c0 0.552 0.448 1 1 1s1-0.448 1-1c0-0.553 0.223-1.051 0.586-1.414s0.861-0.586 1.414-0.586h7c0.552 0 1-0.448 1-1v-15c0-0.552-0.448-1-1-1z"></path> <path d="M21 4v13h-6c-0.728 0-1.412 0.195-2 0.535v-10.535c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879zM11 17.535c-0.588-0.34-1.272-0.535-2-0.535h-6v-13h5c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121zM22 2h-6c-1.38 0-2.632 0.561-3.536 1.464-0.167 0.167-0.322 0.346-0.464 0.536-0.142-0.19-0.297-0.369-0.464-0.536-0.904-0.903-2.156-1.464-3.536-1.464h-6c-0.552 0-1 0.448-1 1v15c0 0.552 0.448 1 1 1h7c0.553 0 1.051 0.223 1.414 0.586s0.586 0.861 0.586 1.414c0 0.552 0.448 1 1 1s1-0.448 1-1c0-0.553 0.223-1.051 0.586-1.414s0.861-0.586 1.414-0.586h7c0.552 0 1-0.448 1-1v-15c0-0.552-0.448-1-1-1z"></path>
</symbol> </symbol>
<symbol id="icon-alert-triangle" viewBox="0 0 24 24"> <symbol id="icon-alert-triangle" viewBox="0 -960 960 960">
<path d="M11.148 4.374c0.073-0.123 0.185-0.242 0.334-0.332 0.236-0.143 0.506-0.178 0.756-0.116s0.474 0.216 0.614 0.448l8.466 14.133c0.070 0.12 0.119 0.268 0.128 0.434-0.015 0.368-0.119 0.591-0.283 0.759-0.18 0.184-0.427 0.298-0.693 0.301l-16.937-0.001c-0.152-0.001-0.321-0.041-0.481-0.134-0.239-0.138-0.399-0.359-0.466-0.607s-0.038-0.519 0.092-0.745zM9.432 3.346l-8.47 14.14c-0.422 0.731-0.506 1.55-0.308 2.29s0.68 1.408 1.398 1.822c0.464 0.268 0.976 0.4 1.475 0.402h16.943c0.839-0.009 1.587-0.354 2.123-0.902s0.864-1.303 0.855-2.131c-0.006-0.536-0.153-1.044-0.406-1.474l-8.474-14.147c-0.432-0.713-1.11-1.181-1.854-1.363s-1.561-0.081-2.269 0.349c-0.429 0.26-0.775 0.615-1.012 1.014zM11 9v4c0 0.552 0.448 1 1 1s1-0.448 1-1v-4c0-0.552-0.448-1-1-1s-1 0.448-1 1zM12 18c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path> <path d="M480-79q-16 0-30.5-6T423-102L102-423q-11-12-17-26.5T79-480q0-16 6-31t17-26l321-321q12-12 26.5-17.5T480-881q16 0 31 5.5t26 17.5l321 321q12 11 17.5 26t5.5 31q0 16-5.5 30.5T858-423L537-102q-11 11-26 17t-31 6Zm0-80 321-321-321-321-321 321 321 321Zm-40-281h80v-240h-80v240Zm40 120q17 0 28.5-11.5T520-360q0-17-11.5-28.5T480-400q-17 0-28.5 11.5T440-360q0 17 11.5 28.5T480-320Zm0-160Z"></path>
</symbol> </symbol>
<symbol id="icon-zap" viewBox="0 0 24 24"> <symbol id="icon-zap" viewBox="0 0 24 24">
<path d="M11.585 5.26l-0.577 4.616c0.033 0.716 0.465 1.124 0.992 1.124h6.865l-6.45 7.74 0.577-4.616c-0.033-0.716-0.465-1.124-0.992-1.124h-6.865zM12.232 1.36l-10 12c-0.354 0.424-0.296 1.055 0.128 1.408 0.187 0.157 0.415 0.233 0.64 0.232h7.867l-0.859 6.876c-0.069 0.548 0.32 1.048 0.868 1.116 0.349 0.044 0.678-0.098 0.892-0.352l10-12c0.354-0.424 0.296-1.055-0.128-1.408-0.187-0.157-0.415-0.233-0.64-0.232h-7.867l0.859-6.876c0.069-0.548-0.32-1.048-0.868-1.116-0.349-0.044-0.678 0.098-0.892 0.352z"></path> <path d="M11.585 5.26l-0.577 4.616c0.033 0.716 0.465 1.124 0.992 1.124h6.865l-6.45 7.74 0.577-4.616c-0.033-0.716-0.465-1.124-0.992-1.124h-6.865zM12.232 1.36l-10 12c-0.354 0.424-0.296 1.055 0.128 1.408 0.187 0.157 0.415 0.233 0.64 0.232h7.867l-0.859 6.876c-0.069 0.548 0.32 1.048 0.868 1.116 0.349 0.044 0.678-0.098 0.892-0.352l10-12c0.354-0.424 0.296-1.055-0.128-1.408-0.187-0.157-0.415-0.233-0.64-0.232h-7.867l0.859-6.876c0.069-0.548-0.32-1.048-0.868-1.116-0.349-0.044-0.678 0.098-0.892 0.352z"></path>
@ -258,5 +258,12 @@
<path d="M8.72046 10.6397L14.9999 7.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.72046 10.6397L14.9999 7.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol> </symbol>
<symbol id="icon-eye" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</symbol>
<symbol id="icon-eye-slash" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
</symbol>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,11 +1,12 @@
import { initializeSite, setupDateFns } from "@utils/app"; import { initializeSite, setupDateFns } from "@utils/app";
import { hydrate } from "inferno-hydrate"; import { hydrate } from "inferno-hydrate";
import { Router } from "inferno-router"; import { BrowserRouter } from "inferno-router";
import { App } from "../shared/components/app/app"; import { App } from "../shared/components/app/app";
import { HistoryService, UserService } from "../shared/services"; import { UserService } from "../shared/services";
import "bootstrap/js/dist/collapse"; import "bootstrap/js/dist/collapse";
import "bootstrap/js/dist/dropdown"; import "bootstrap/js/dist/dropdown";
import "bootstrap/js/dist/modal";
async function startClient() { async function startClient() {
initializeSite(window.isoData.site_res); initializeSite(window.isoData.site_res);
@ -13,9 +14,9 @@ async function startClient() {
await setupDateFns(); await setupDateFns();
const wrapper = ( const wrapper = (
<Router history={HistoryService.history}> <BrowserRouter>
<App user={UserService.Instance.myUserInfo} /> <App user={UserService.Instance.myUserInfo} />
</Router> </BrowserRouter>
); );
const root = document.getElementById("root"); const root = document.getElementById("root");

View file

@ -6,7 +6,7 @@ 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 { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import { App } from "../../shared/components/app/app"; import { App } from "../../shared/components/app/app";
import { import {
InitialFetchRequest, InitialFetchRequest,
@ -26,18 +26,19 @@ 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 = req.headers.cookie
? cookie.parse(req.headers.cookie).jwt
: undefined;
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 }),
); );
const auth = req.headers.cookie
? cookie.parse(req.headers.cookie).jwt
: undefined;
if (auth) {
client.setHeaders({ Authorization: `Bearer ${auth}` });
}
const { path, url, query } = req; const { path, url, query } = req;
// Get site data first // Get site data first
@ -46,19 +47,18 @@ export default async (req: Request, res: Response) => {
let site: GetSiteResponse | undefined = undefined; let site: GetSiteResponse | undefined = undefined;
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();
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",
); );
getSiteForm.auth = undefined; client.setHeaders({});
auth = undefined; try_site = await client.getSite();
try_site = await client.getSite(getSiteForm);
} }
if (!auth && isAuthPath(path)) { if (!auth && isAuthPath(path)) {
return res.redirect("/login"); return res.redirect(`/login?prev=${encodeURIComponent(url)}`);
} }
if (try_site.state === "success") { if (try_site.state === "success") {
@ -72,7 +72,6 @@ export default async (req: Request, res: Response) => {
if (site && activeRoute?.fetchInitialData) { if (site && activeRoute?.fetchInitialData) {
const initialFetchReq: InitialFetchRequest = { const initialFetchReq: InitialFetchRequest = {
client, client,
auth,
path, path,
query, query,
site, site,
@ -90,7 +89,7 @@ export default async (req: Request, res: Response) => {
} }
const error = Object.values(routeData).find( const error = Object.values(routeData).find(
res => res.state === "failed" && res.msg !== "couldnt_find_object" // TODO: find a better way of handling errors res => res.state === "failed" && res.msg !== "couldnt_find_object", // TODO: find a better way of handling errors
) as FailedRequestState | undefined; ) as FailedRequestState | undefined;
// Redirect to the 404 if there's an API error // Redirect to the 404 if there's an API error
@ -120,14 +119,14 @@ export default async (req: Request, res: Response) => {
const root = renderToString(wrapper); const root = renderToString(wrapper);
res.send(await createSsrHtml(root, isoData)); res.send(await createSsrHtml(root, isoData, res.locals.cspNonce));
} catch (err) { } catch (err) {
// 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",
); );
} }
}; };

View file

@ -0,0 +1,35 @@
import type { Request, Response } from "express";
import { existsSync } from "fs";
import path from "path";
const extraThemesFolder =
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
export default async (req: Request, res: Response) => {
res.contentType("text/css");
const theme = req.params.name;
if (!theme.endsWith(".css")) {
return res.status(400).send("Theme must be a css file");
}
const customTheme = path.resolve(extraThemesFolder, theme);
if (existsSync(customTheme)) {
return res.sendFile(customTheme);
} else {
const internalTheme = path.resolve(
`./dist/assets/css/code-themes/${theme}`,
);
// If the theme doesn't exist, just send atom-one-light
if (existsSync(internalTheme)) {
return res.sendFile(internalTheme);
} else {
return res.sendFile(
path.resolve("./dist/assets/css/code-themes/atom-one-light.css"),
);
}
}
};

View file

@ -13,9 +13,9 @@ export default async (req: Request, res: Response) => {
if (!manifest || manifest.start_url !== getHttpBaseExternal()) { if (!manifest || manifest.start_url !== getHttpBaseExternal()) {
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 }),
); );
const site = await client.getSite({}); const site = await client.getSite();
if (site.state === "success") { if (site.state === "success") {
manifest = await generateManifestJson(site.data); manifest = await generateManifestJson(site.data);

View file

@ -15,5 +15,6 @@ export default async ({ res }: { res: Response }) => {
Disallow: /admin Disallow: /admin
Disallow: /password_change Disallow: /password_change
Disallow: /search/ Disallow: /search/
Disallow: /modlog
`); `);
}; };

View file

@ -12,6 +12,6 @@ export default async ({ res }: { res: Response }) => {
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
` `
Expires: 2024-01-01T04:59:00.000Z Expires: 2024-01-01T04:59:00.000Z
` `,
); );
}; };

View file

@ -8,7 +8,7 @@ export default async ({ res }: { res: Response }) => {
path.resolve( path.resolve(
`./dist/service-worker${ `./dist/service-worker${
process.env.NODE_ENV === "development" ? "-development" : "" process.env.NODE_ENV === "development" ? "-development" : ""
}.js` }.js`,
) ),
); );
}; };

View file

@ -11,6 +11,7 @@ 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 { setCacheControl, setDefaultCsp } from "./middleware"; import { setCacheControl, setDefaultCsp } from "./middleware";
import CodeThemeHandler from "./handlers/code-theme-handler";
const server = express(); const server = express();
@ -20,17 +21,26 @@ 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(
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"]) { const serverPath = path.resolve("./dist");
if (
!process.env["LEMMY_UI_DISABLE_CSP"] &&
!process.env["LEMMY_UI_DEBUG"] &&
process.env["NODE_ENV"] !== "development"
) {
server.use(
getStaticDir(),
express.static(serverPath, {
maxAge: 24 * 60 * 60 * 1000, // 1 day
immutable: true,
}),
);
server.use(setDefaultCsp); server.use(setDefaultCsp);
server.use(setCacheControl);
} else {
// In debug mode, don't use the maxAge and immutable, or it breaks live reload for dev
server.use(getStaticDir(), express.static(serverPath));
} }
server.get("/.well-known/security.txt", SecurityHandler); server.get("/.well-known/security.txt", SecurityHandler);
@ -38,6 +48,7 @@ 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);
server.get("/css/themes/:name", ThemeHandler); server.get("/css/themes/:name", ThemeHandler);
server.get("/css/code-themes/:name", CodeThemeHandler);
server.get("/css/themelist", ThemesListHandler); server.get("/css/themelist", ThemesListHandler);
server.get("/*", CatchAllHandler); server.get("/*", CatchAllHandler);

View file

@ -1,3 +1,4 @@
import * as crypto from "crypto";
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { hasJwtCookie } from "./utils/has-jwt-cookie"; import { hasJwtCookie } from "./utils/has-jwt-cookie";
@ -8,9 +9,20 @@ export function setDefaultCsp({
res: Response; res: Response;
next: NextFunction; next: NextFunction;
}) { }) {
res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
res.setHeader( res.setHeader(
"Content-Security-Policy", "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:` `default-src 'self';
manifest-src *;
connect-src *;
img-src * data:;
script-src 'self' 'nonce-${res.locals.cspNonce}';
style-src 'self' 'unsafe-inline';
form-action 'self';
base-uri 'self';
frame-src *;
media-src * data:`.replace(/\s+/g, " "),
); );
next(); next();
@ -25,7 +37,7 @@ export function setDefaultCsp({
export function setCacheControl( export function setCacheControl(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction,
) { ) {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
return next(); return next();
@ -43,7 +55,7 @@ export function setCacheControl(
if (hasJwtCookie(req)) { if (hasJwtCookie(req)) {
caching = "private"; caching = "private";
} else { } else {
caching = "public, max-age=5"; caching = "public, max-age=60";
} }
} }

View file

@ -4,7 +4,7 @@ import { renderToString } from "inferno-server";
import serialize from "serialize-javascript"; import serialize from "serialize-javascript";
import sharp from "sharp"; import sharp from "sharp";
import { favIconPngUrl, favIconUrl } from "../../shared/config"; import { favIconPngUrl, favIconUrl } from "../../shared/config";
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces"; import { IsoDataOptionalSite } from "../../shared/interfaces";
import { buildThemeList } from "./build-themes-list"; import { buildThemeList } from "./build-themes-list";
import { fetchIconPng } from "./fetch-icon-png"; import { fetchIconPng } from "./fetch-icon-png";
@ -14,7 +14,8 @@ let appleTouchIcon: string | undefined = undefined;
export async function createSsrHtml( export async function createSsrHtml(
root: string, root: string,
isoData: IsoDataOptionalSite isoData: IsoDataOptionalSite,
cspNonce: string,
) { ) {
const site = isoData.site_res; const site = isoData.site_res;
@ -22,10 +23,16 @@ export async function createSsrHtml(
(await buildThemeList())[0] (await buildThemeList())[0]
}.css" />`; }.css" />`;
const customHtmlHeaderScriptTag = new RegExp("<script", "g");
const customHtmlHeaderWithNonce = customHtmlHeader.replace(
customHtmlHeaderScriptTag,
`<script nonce="${cspNonce}"`,
);
if (!appleTouchIcon) { if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${await 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)
.extend({ .extend({
@ -45,28 +52,28 @@ export async function createSsrHtml(
process.env["LEMMY_UI_DEBUG"] === "true" process.env["LEMMY_UI_DEBUG"] === "true"
? renderToString( ? renderToString(
<> <>
<script src="//cdn.jsdelivr.net/npm/eruda"></script> <script
<script>eruda.init();</script> nonce={cspNonce}
</> src="//cdn.jsdelivr.net/npm/eruda"
></script>
<script nonce={cspNonce}>eruda.init();</script>
</>,
) )
: ""; : "";
const helmet = Helmet.renderStatic(); const helmet = Helmet.renderStatic();
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}> <html ${helmet.htmlAttributes.toString()}>
<head> <head>
<script>window.isoData = ${serialize(isoData)}</script> <script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile --> <!-- A remote debugging utility for mobile -->
${erudaStr} ${erudaStr}
<!-- Custom injected script --> <!-- Custom injected script -->
${customHtmlHeader} ${customHtmlHeaderWithNonce}
${helmet.title.toString()} ${helmet.title.toString()}
${helmet.meta.toString()} ${helmet.meta.toString()}

View file

@ -1,4 +1,3 @@
import { getHttpBaseExternal } from "@utils/env";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import path from "path"; import path from "path";
@ -11,7 +10,7 @@ const defaultLogoPathDirectory = path.join(
process.cwd(), process.cwd(),
"dist", "dist",
"assets", "assets",
"icons" "icons",
); );
export default async function ({ export default async function ({
@ -21,15 +20,13 @@ export default async function ({
local_site: { community_creation_admin_only }, local_site: { community_creation_admin_only },
}, },
}: GetSiteResponse) { }: GetSiteResponse) {
const url = getHttpBaseExternal();
const icon = site.icon ? await fetchIconPng(site.icon) : null; const icon = site.icon ? await fetchIconPng(site.icon) : null;
return { return {
name: site.name, name: site.name,
description: site.description ?? "A link aggregator for the fediverse", description: site.description ?? "A link aggregator for the fediverse",
start_url: url, start_url: "/",
scope: url, scope: "/",
display: "standalone", display: "standalone",
id: "/", id: "/",
background_color: "#222222", background_color: "#222222",
@ -37,7 +34,7 @@ export default async function ({
icons: await Promise.all( icons: await Promise.all(
iconSizes.map(async size => { iconSizes.map(async size => {
let src = await readFile( let src = await readFile(
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`) path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`),
).then(buf => buf.toString("base64")); ).then(buf => buf.toString("base64"));
if (icon) { if (icon) {
@ -54,7 +51,7 @@ export default async function ({
src: `data:image/png;base64,${src}`, src: `data:image/png;base64,${src}`,
purpose: "any maskable", purpose: "any maskable",
}; };
}) }),
), ),
shortcuts: [ shortcuts: [
{ {
@ -76,7 +73,8 @@ export default async function ({
description: "Create a post.", description: "Create a post.",
}, },
].concat( ].concat(
my_user?.local_user_view.person.admin || !community_creation_admin_only my_user?.local_user_view.local_user.admin ||
!community_creation_admin_only
? [ ? [
{ {
name: "Create Community", name: "Create Community",
@ -85,7 +83,7 @@ export default async function ({
description: "Create a community", description: "Create a community",
}, },
] ]
: [] : [],
), ),
related_applications: [ related_applications: [
{ {

View file

@ -1,4 +1,4 @@
import { isAuthPath, setIsoData } from "@utils/app"; import { isAnonymousPath, isAuthPath, setIsoData } from "@utils/app";
import { dataBsTheme } from "@utils/browser"; import { dataBsTheme } from "@utils/browser";
import { Component, RefObject, createRef, linkEvent } from "inferno"; import { Component, RefObject, createRef, linkEvent } from "inferno";
import { Provider } from "inferno-i18next-dess"; import { Provider } from "inferno-i18next-dess";
@ -14,6 +14,8 @@ import { Footer } from "./footer";
import { Navbar } from "./navbar"; import { Navbar } from "./navbar";
import "./styles.scss"; import "./styles.scss";
import { Theme } from "./theme"; import { Theme } from "./theme";
import AnonymousGuard from "../common/anonymous-guard";
import { CodeTheme } from "./code-theme";
interface AppProps { interface AppProps {
user?: MyUserInfo; user?: MyUserInfo;
@ -54,7 +56,10 @@ export class App extends Component<AppProps, any> {
{I18NextService.i18n.t("jump_to_content", "Jump to content")} {I18NextService.i18n.t("jump_to_content", "Jump to content")}
</button> </button>
{siteView && ( {siteView && (
<Theme defaultTheme={siteView.local_site.default_theme} /> <>
<Theme defaultTheme={siteView.local_site.default_theme} />
<CodeTheme />
</>
)} )}
<Navbar siteRes={siteRes} /> <Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1"> <div className="mt-4 p-0 fl-1">
@ -75,9 +80,13 @@ export class App extends Component<AppProps, any> {
<div tabIndex={-1}> <div tabIndex={-1}>
{RouteComponent && {RouteComponent &&
(isAuthPath(path ?? "") ? ( (isAuthPath(path ?? "") ? (
<AuthGuard> <AuthGuard {...routeProps}>
<RouteComponent {...routeProps} /> <RouteComponent {...routeProps} />
</AuthGuard> </AuthGuard>
) : isAnonymousPath(path ?? "") ? (
<AnonymousGuard>
<RouteComponent {...routeProps} />
</AnonymousGuard>
) : ( ) : (
<RouteComponent {...routeProps} /> <RouteComponent {...routeProps} />
))} ))}
@ -86,7 +95,7 @@ export class App extends Component<AppProps, any> {
); );
}} }}
/> />
) ),
)} )}
<Route component={ErrorPage} /> <Route component={ErrorPage} />
</Switch> </Switch>

View file

@ -0,0 +1,22 @@
import { Component } from "inferno";
export class CodeTheme extends Component {
render() {
return (
<>
<link
rel="stylesheet"
type="text/css"
href={`/css/code-themes/atom-one-light.css`}
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href={`/css/code-themes/atom-one-dark.css`}
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</>
);
}
}

View file

@ -1,5 +1,4 @@
import { setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { removeAuthParam } from "@utils/helpers";
import { Component } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -56,11 +55,7 @@ export class ErrorPage extends Component<any, any> {
</> </>
)} )}
{errorPageData?.error && ( {errorPageData?.error && (
<T <T i18nKey="error_code_message" parent="p">
i18nKey="error_code_message"
parent="p"
interpolation={{ error: removeAuthParam(errorPageData.error) }}
>
#<strong className="text-danger">#</strong># #<strong className="text-danger">#</strong>#
</T> </T>
)} )}

View file

@ -1,31 +1,30 @@
import { myAuth, showAvatars } from "@utils/app"; import { showAvatars } from "@utils/app";
import { isBrowser } from "@utils/browser"; import { isBrowser } from "@utils/browser";
import { numToSI, poll } from "@utils/helpers"; import { numToSI } from "@utils/helpers";
import { amAdmin, canCreateCommunity } from "@utils/roles"; import { amAdmin, canCreateCommunity } from "@utils/roles";
import { Component, createRef, linkEvent } from "inferno"; import { Component, createRef, linkEvent } from "inferno";
import { NavLink } from "inferno-router"; import { NavLink } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { donateLemmyUrl } from "../../config";
import { import {
GetReportCountResponse, I18NextService,
GetSiteResponse, UserService,
GetUnreadCountResponse, UnreadCounterService,
GetUnreadRegistrationApplicationCountResponse, } from "../../services";
} from "lemmy-js-client";
import { donateLemmyUrl, updateUnreadCountsInterval } from "../../config";
import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image"; import { PictrsImage } from "../common/pictrs-image";
import { Subscription } from "rxjs";
interface NavbarProps { interface NavbarProps {
siteRes?: GetSiteResponse; siteRes?: GetSiteResponse;
} }
interface NavbarState { interface NavbarState {
unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
unreadReportCountRes: RequestState<GetReportCountResponse>;
unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
onSiteBanner?(url: string): any; onSiteBanner?(url: string): any;
unreadInboxCount: number;
unreadReportCount: number;
unreadApplicationCount: number;
} }
function handleCollapseClick(i: Navbar) { function handleCollapseClick(i: Navbar) {
@ -44,13 +43,17 @@ function handleLogOut(i: Navbar) {
} }
export class Navbar extends Component<NavbarProps, NavbarState> { export class Navbar extends Component<NavbarProps, NavbarState> {
state: NavbarState = {
unreadInboxCountRes: { state: "empty" },
unreadReportCountRes: { state: "empty" },
unreadApplicationCountRes: { state: "empty" },
};
collapseButtonRef = createRef<HTMLButtonElement>(); collapseButtonRef = createRef<HTMLButtonElement>();
mobileMenuRef = createRef<HTMLDivElement>(); mobileMenuRef = createRef<HTMLDivElement>();
unreadInboxCountSubscription: Subscription;
unreadReportCountSubscription: Subscription;
unreadApplicationCountSubscription: Subscription;
state: NavbarState = {
unreadInboxCount: 0,
unreadReportCount: 0,
unreadApplicationCount: 0,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -63,7 +66,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
if (isBrowser()) { if (isBrowser()) {
// On the first load, check the unreads // On the first load, check the unreads
this.requestNotificationPermission(); this.requestNotificationPermission();
this.fetchUnreads(); this.unreadInboxCountSubscription =
UnreadCounterService.Instance.unreadInboxCountSubject.subscribe(
unreadInboxCount => this.setState({ unreadInboxCount }),
);
this.unreadReportCountSubscription =
UnreadCounterService.Instance.unreadReportCountSubject.subscribe(
unreadReportCount => this.setState({ unreadReportCount }),
);
this.unreadApplicationCountSubscription =
UnreadCounterService.Instance.unreadApplicationCountSubject.subscribe(
unreadApplicationCount => this.setState({ unreadApplicationCount }),
);
this.requestNotificationPermission(); this.requestNotificationPermission();
document.addEventListener("mouseup", this.handleOutsideMenuClick); document.addEventListener("mouseup", this.handleOutsideMenuClick);
@ -72,6 +86,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener("mouseup", this.handleOutsideMenuClick); document.removeEventListener("mouseup", this.handleOutsideMenuClick);
this.unreadInboxCountSubscription.unsubscribe();
this.unreadReportCountSubscription.unsubscribe();
this.unreadApplicationCountSubscription.unsubscribe();
} }
// TODO class active corresponding to current pages // TODO class active corresponding to current pages
@ -103,34 +120,34 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
to="/inbox" to="/inbox"
className="p-1 nav-link border-0 nav-messages" className="p-1 nav-link border-0 nav-messages"
title={I18NextService.i18n.t("unread_messages", { title={I18NextService.i18n.t("unread_messages", {
count: Number(this.state.unreadApplicationCountRes.state), count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount), formattedCount: numToSI(this.state.unreadInboxCount),
})} })}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="bell" /> <Icon icon="bell" />
{this.unreadInboxCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadInboxCount)} {numToSI(this.state.unreadInboxCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
</li> </li>
{this.moderatesSomething && ( {UserService.Instance.moderatesSomething && (
<li className="nav-item nav-item-icon"> <li className="nav-item nav-item-icon">
<NavLink <NavLink
to="/reports" to="/reports"
className="p-1 nav-link border-0" className="p-1 nav-link border-0"
title={I18NextService.i18n.t("unread_reports", { title={I18NextService.i18n.t("unread_reports", {
count: Number(this.unreadReportCount), count: Number(this.state.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount), formattedCount: numToSI(this.state.unreadReportCount),
})} })}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="shield" /> <Icon icon="shield" />
{this.unreadReportCount > 0 && ( {this.state.unreadReportCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadReportCount)} {numToSI(this.state.unreadReportCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
@ -144,16 +161,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
title={I18NextService.i18n.t( title={I18NextService.i18n.t(
"unread_registration_applications", "unread_registration_applications",
{ {
count: Number(this.unreadApplicationCount), count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI(this.unreadApplicationCount), formattedCount: numToSI(
} this.state.unreadApplicationCount,
),
},
)} )}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="clipboard" /> <Icon icon="clipboard" />
{this.unreadApplicationCount > 0 && ( {this.state.unreadApplicationCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadApplicationCount)} {numToSI(this.state.unreadApplicationCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
@ -268,46 +287,48 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
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="/inbox" to="/inbox"
title={I18NextService.i18n.t("unread_messages", { title={I18NextService.i18n.t("unread_messages", {
count: Number(this.unreadInboxCount), count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount), formattedCount: numToSI(this.state.unreadInboxCount),
})} })}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="bell" /> <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_messages", { {I18NextService.i18n.t("unread_messages", {
count: Number(this.unreadInboxCount), count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount), formattedCount: numToSI(this.state.unreadInboxCount),
})} })}
</span> </span>
{this.unreadInboxCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadInboxCount)} {numToSI(this.state.unreadInboxCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
</li> </li>
{this.moderatesSomething && ( {UserService.Instance.moderatesSomething && (
<li id="navModeration" className="nav-item"> <li id="navModeration" 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="/reports"
title={I18NextService.i18n.t("unread_reports", { title={I18NextService.i18n.t("unread_reports", {
count: Number(this.unreadReportCount), count: Number(this.state.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount), formattedCount: numToSI(this.state.unreadReportCount),
})} })}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="shield" /> <Icon icon="shield" />
<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_reports", {
count: Number(this.unreadReportCount), count: Number(this.state.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount), formattedCount: numToSI(
this.state.unreadReportCount,
),
})} })}
</span> </span>
{this.unreadReportCount > 0 && ( {this.state.unreadReportCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadReportCount)} {numToSI(this.state.unreadReportCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
@ -321,11 +342,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
title={I18NextService.i18n.t( title={I18NextService.i18n.t(
"unread_registration_applications", "unread_registration_applications",
{ {
count: Number(this.unreadApplicationCount), count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI( formattedCount: numToSI(
this.unreadApplicationCount this.state.unreadApplicationCount,
), ),
} },
)} )}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
@ -334,16 +355,16 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{I18NextService.i18n.t( {I18NextService.i18n.t(
"unread_registration_applications", "unread_registration_applications",
{ {
count: Number(this.unreadApplicationCount), count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI( formattedCount: numToSI(
this.unreadApplicationCount this.state.unreadApplicationCount,
), ),
} },
)} )}
</span> </span>
{this.unreadApplicationCount > 0 && ( {this.state.unreadApplicationCount > 0 && (
<span className="mx-1 badge text-bg-light"> <span className="mx-1 badge text-bg-light">
{numToSI(this.unreadApplicationCount)} {numToSI(this.state.unreadApplicationCount)}
</span> </span>
)} )}
</NavLink> </NavLink>
@ -441,75 +462,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
} }
get moderatesSomething(): boolean {
const mods = UserService.Instance.myUserInfo?.moderates;
const moderatesS = (mods && mods.length > 0) || false;
return amAdmin() || moderatesS;
}
fetchUnreads() {
poll(async () => {
if (window.document.visibilityState !== "hidden") {
const auth = myAuth();
if (auth) {
this.setState({
unreadInboxCountRes: await HttpService.client.getUnreadCount({
auth,
}),
});
if (this.moderatesSomething) {
this.setState({
unreadReportCountRes: await HttpService.client.getReportCount({
auth,
}),
});
}
if (amAdmin()) {
this.setState({
unreadApplicationCountRes:
await HttpService.client.getUnreadRegistrationApplicationCount({
auth,
}),
});
}
}
}
}, updateUnreadCountsInterval);
}
get unreadInboxCount(): number {
if (this.state.unreadInboxCountRes.state == "success") {
const data = this.state.unreadInboxCountRes.data;
return data.replies + data.mentions + data.private_messages;
} else {
return 0;
}
}
get unreadReportCount(): number {
if (this.state.unreadReportCountRes.state == "success") {
const data = this.state.unreadReportCountRes.data;
return (
data.post_reports +
data.comment_reports +
(data.private_message_reports ?? 0)
);
} else {
return 0;
}
}
get unreadApplicationCount(): number {
if (this.state.unreadApplicationCountRes.state == "success") {
const data = this.state.unreadApplicationCountRes.data;
return data.registration_applications;
} else {
return 0;
}
}
get currentLocation() { get currentLocation() {
return this.context.router.history.location.pathname; return this.context.router.history.location.pathname;
} }

View file

@ -21,7 +21,10 @@ export class Theme extends Component<Props> {
/> />
</Helmet> </Helmet>
); );
} else if (this.props.defaultTheme != "browser") { } else if (
this.props.defaultTheme !== "browser" &&
this.props.defaultTheme !== "browser-compact"
) {
return ( return (
<Helmet> <Helmet>
<link <link
@ -31,6 +34,25 @@ export class Theme extends Component<Props> {
/> />
</Helmet> </Helmet>
); );
} else if (this.props.defaultTheme === "browser-compact") {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely-compact.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly-compact.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
);
} else { } else {
return ( return (
<Helmet> <Helmet>

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
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";
@ -43,7 +42,7 @@ export class CommentForm extends Component<CommentFormProps, any> {
return ( return (
<div <div
className={["comment-form", "mb-3", this.props.containerClass].join( className={["comment-form", "mb-3", this.props.containerClass].join(
" " " ",
)} )}
> >
{UserService.Instance.myUserInfo ? ( {UserService.Instance.myUserInfo ? (
@ -84,7 +83,7 @@ export class CommentForm extends Component<CommentFormProps, any> {
: capitalizeFirstLetter(I18NextService.i18n.t("reply")); : capitalizeFirstLetter(I18NextService.i18n.t("reply"));
} }
handleCommentSubmit(content: string, form_id: string, language_id?: number) { handleCommentSubmit(content: string, language_id?: number) {
const { node, onUpsertComment, edit } = this.props; const { node, onUpsertComment, edit } = this.props;
if (typeof node === "number") { if (typeof node === "number") {
const post_id = node; const post_id = node;
@ -92,8 +91,6 @@ export class CommentForm extends Component<CommentFormProps, any> {
content, content,
post_id, post_id,
language_id, language_id,
form_id,
auth: myAuthRequired(),
}); });
} else { } else {
if (edit) { if (edit) {
@ -101,9 +98,7 @@ export class CommentForm extends Component<CommentFormProps, any> {
onUpsertComment({ onUpsertComment({
content, content,
comment_id, comment_id,
form_id,
language_id, language_id,
auth: myAuthRequired(),
}); });
} else { } else {
const post_id = node.comment_view.post.id; const post_id = node.comment_view.post.id;
@ -112,9 +107,7 @@ export class CommentForm extends Component<CommentFormProps, any> {
content, content,
parent_id, parent_id,
post_id, post_id,
form_id,
language_id, language_id,
auth: myAuthRequired(),
}); });
} }
} }

View file

@ -1,10 +1,4 @@
import { import { colorList, getCommentParentId, showScores } from "@utils/app";
colorList,
getCommentParentId,
myAuth,
myAuthRequired,
showScores,
} from "@utils/app";
import { futureDaysToUnixTime, numToSI } from "@utils/helpers"; import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
import { import {
amCommunityCreator, amCommunityCreator,
@ -68,6 +62,7 @@ import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { CommentForm } from "./comment-form"; import { CommentForm } from "./comment-form";
import { CommentNodes } from "./comment-nodes"; import { CommentNodes } from "./comment-nodes";
import ReportForm from "../common/report-form";
interface CommentNodeState { interface CommentNodeState {
showReply: boolean; showReply: boolean;
@ -90,7 +85,6 @@ interface CommentNodeState {
viewSource: boolean; viewSource: boolean;
showAdvanced: boolean; showAdvanced: boolean;
showReportDialog: boolean; showReportDialog: boolean;
reportReason?: string;
createOrEditCommentLoading: boolean; createOrEditCommentLoading: boolean;
upvoteLoading: boolean; upvoteLoading: boolean;
downvoteLoading: boolean; downvoteLoading: boolean;
@ -105,7 +99,6 @@ interface CommentNodeState {
addAdminLoading: boolean; addAdminLoading: boolean;
transferCommunityLoading: boolean; transferCommunityLoading: boolean;
fetchChildrenLoading: boolean; fetchChildrenLoading: boolean;
reportLoading: boolean;
purgeLoading: boolean; purgeLoading: boolean;
} }
@ -114,7 +107,7 @@ interface CommentNodeProps {
moderators?: CommunityModeratorView[]; moderators?: CommunityModeratorView[];
admins?: PersonView[]; admins?: PersonView[];
noBorder?: boolean; noBorder?: boolean;
noIndent?: boolean; isTopLevel?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean; locked?: boolean;
markable?: boolean; markable?: boolean;
@ -179,7 +172,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
addAdminLoading: false, addAdminLoading: false,
transferCommunityLoading: false, transferCommunityLoading: false,
fetchChildrenLoading: false, fetchChildrenLoading: false,
reportLoading: false,
purgeLoading: false, purgeLoading: false,
}; };
@ -187,6 +179,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
super(props, context); super(props, context);
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handleReportComment = this.handleReportComment.bind(this);
} }
get commentView(): CommentView { get commentView(): CommentView {
@ -198,11 +191,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps> nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>,
): void { ): void {
if (!deepEqual(this.props, nextProps)) { if (!deepEqual(this.props, nextProps)) {
this.setState({ this.setState({
showReply: false,
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
showBanDialog: false, showBanDialog: false,
@ -232,7 +224,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
addAdminLoading: false, addAdminLoading: false,
transferCommunityLoading: false, transferCommunityLoading: false,
fetchChildrenLoading: false, fetchChildrenLoading: false,
reportLoading: false,
purgeLoading: false, purgeLoading: false,
}); });
} }
@ -243,34 +234,34 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
const cv = this.commentView; const cv = this.commentView;
const purgeTypeText = const purgeTypeText =
this.state.purgeType == PurgeType.Comment this.state.purgeType === PurgeType.Comment
? I18NextService.i18n.t("purge_comment") ? I18NextService.i18n.t("purge_comment")
: `${I18NextService.i18n.t("purge")} ${cv.creator.name}`; : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
const canMod_ = canMod( const canMod_ = canMod(
cv.creator.id, cv.creator.id,
this.props.moderators, this.props.moderators,
this.props.admins this.props.admins,
); );
const canModOnSelf = canMod( const canModOnSelf = canMod(
cv.creator.id, cv.creator.id,
this.props.moderators, this.props.moderators,
this.props.admins, this.props.admins,
UserService.Instance.myUserInfo, UserService.Instance.myUserInfo,
true true,
); );
const canAdmin_ = canAdmin(cv.creator.id, this.props.admins); const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
const canAdminOnSelf = canAdmin( const canAdminOnSelf = canAdmin(
cv.creator.id, cv.creator.id,
this.props.admins, this.props.admins,
UserService.Instance.myUserInfo, UserService.Instance.myUserInfo,
true true,
); );
const isMod_ = isMod(cv.creator.id, this.props.moderators); const isMod_ = isMod(cv.creator.id, this.props.moderators);
const isAdmin_ = isAdmin(cv.creator.id, this.props.admins); const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
const amCommunityCreator_ = amCommunityCreator( const amCommunityCreator_ = amCommunityCreator(
cv.creator.id, cv.creator.id,
this.props.moderators this.props.moderators,
); );
const moreRepliesBorderColor = this.props.node.depth const moreRepliesBorderColor = this.props.node.depth
@ -278,13 +269,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
: colorList[0]; : colorList[0];
const showMoreChildren = const showMoreChildren =
this.props.viewType == CommentViewType.Tree && this.props.viewType === CommentViewType.Tree &&
!this.state.collapsed && !this.state.collapsed &&
node.children.length == 0 && node.children.length === 0 &&
node.comment_view.counts.child_count > 0; node.comment_view.counts.child_count > 0;
return ( return (
<li className="comment"> <li className="comment list-unstyled">
<article <article
id={`comment-${cv.comment.id}`} id={`comment-${cv.comment.id}`}
className={classNames(`details comment-node py-2`, { className={classNames(`details comment-node py-2`, {
@ -292,11 +283,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
mark: this.isCommentNew || this.commentView.comment.distinguished, mark: this.isCommentNew || this.commentView.comment.distinguished,
})} })}
> >
<div <div className="ms-2">
className={classNames({
"ms-2": !this.props.noIndent,
})}
>
<div className="d-flex flex-wrap align-items-center text-muted small"> <div className="d-flex flex-wrap align-items-center text-muted small">
<button <button
className="btn btn-sm btn-link text-muted me-2" className="btn btn-sm btn-link text-muted me-2"
@ -341,7 +328,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<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">
{ {
this.props.allLanguages.find( this.props.allLanguages.find(
lang => lang.id === cv.comment.language_id lang => lang.id === cv.comment.language_id,
)?.name )?.name
} }
</span> </span>
@ -352,7 +339,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{showScores() && ( {showScores() && (
<> <>
<span <span
className="me-1 fw-bold" className={`me-1 fw-bold ${this.scoreColor}`}
aria-label={I18NextService.i18n.t("number_of_points", { aria-label={I18NextService.i18n.t("number_of_points", {
count: Number(this.commentView.counts.score), count: Number(this.commentView.counts.score),
formattedCount: numToSI(this.commentView.counts.score), formattedCount: numToSI(this.commentView.counts.score),
@ -378,7 +365,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
finished={this.props.finished.get( finished={this.props.finished.get(
this.props.node.comment_view.comment.id this.props.node.comment_view.comment.id,
)} )}
focus focus
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
@ -388,20 +375,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
/> />
)} )}
{!this.state.showEdit && !this.state.collapsed && ( {!this.state.showEdit && !this.state.collapsed && (
<div> <>
{this.state.viewSource ? ( <div className="comment-content">
<pre>{this.commentUnlessRemoved}</pre> {this.state.viewSource ? (
) : ( <pre>{this.commentUnlessRemoved}</pre>
<div ) : (
className="md-div" <div
dangerouslySetInnerHTML={ className="md-div"
this.props.hideImages dangerouslySetInnerHTML={
? mdToHtmlNoImages(this.commentUnlessRemoved) this.props.hideImages
: mdToHtml(this.commentUnlessRemoved) ? mdToHtmlNoImages(this.commentUnlessRemoved)
} : mdToHtml(this.commentUnlessRemoved)
/> }
)} />
<div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold"> )}
</div>
<div className="comment-bottom-btns d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
{this.props.showContext && this.getLinkButton()} {this.props.showContext && this.getLinkButton()}
{this.props.markable && ( {this.props.markable && (
<button <button
@ -474,13 +463,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowReportDialog this.handleShowReportDialog,
)} )}
data-tippy-content={I18NextService.i18n.t( data-tippy-content={I18NextService.i18n.t(
"show_report_dialog" "show_report_dialog",
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"show_report_dialog" "show_report_dialog",
)} )}
> >
<Icon icon="flag" /> <Icon icon="flag" />
@ -489,10 +478,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleBlockPerson this.handleBlockPerson,
)} )}
data-tippy-content={I18NextService.i18n.t( data-tippy-content={I18NextService.i18n.t(
"block_user" "block_user",
)} )}
aria-label={I18NextService.i18n.t("block_user")} aria-label={I18NextService.i18n.t("block_user")}
> >
@ -533,7 +522,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)} onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={I18NextService.i18n.t( data-tippy-content={I18NextService.i18n.t(
"view_source" "view_source",
)} )}
aria-label={I18NextService.i18n.t("view_source")} aria-label={I18NextService.i18n.t("view_source")}
> >
@ -550,7 +539,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={I18NextService.i18n.t( data-tippy-content={I18NextService.i18n.t(
"edit" "edit",
)} )}
aria-label={I18NextService.i18n.t("edit")} aria-label={I18NextService.i18n.t("edit")}
> >
@ -560,7 +549,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDeleteComment this.handleDeleteComment,
)} )}
data-tippy-content={ data-tippy-content={
!cv.comment.deleted !cv.comment.deleted
@ -590,7 +579,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDistinguishComment this.handleDistinguishComment,
)} )}
data-tippy-content={ data-tippy-content={
!cv.comment.distinguished !cv.comment.distinguished
@ -621,7 +610,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModRemoveShow this.handleModRemoveShow,
)} )}
aria-label={I18NextService.i18n.t("remove")} aria-label={I18NextService.i18n.t("remove")}
> >
@ -632,7 +621,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleRemoveComment this.handleRemoveComment,
)} )}
aria-label={I18NextService.i18n.t("restore")} aria-label={I18NextService.i18n.t("restore")}
> >
@ -654,14 +643,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanFromCommunityShow this.handleModBanFromCommunityShow,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"ban_from_community" "ban_from_community",
)} )}
> >
{I18NextService.i18n.t( {I18NextService.i18n.t(
"ban_from_community" "ban_from_community",
)} )}
</button> </button>
) : ( ) : (
@ -669,7 +658,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleBanPersonFromCommunity this.handleBanPersonFromCommunity,
)} )}
aria-label={I18NextService.i18n.t("unban")} aria-label={I18NextService.i18n.t("unban")}
> >
@ -686,13 +675,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmAppointAsMod this.handleShowConfirmAppointAsMod,
)} )}
aria-label={ aria-label={
isMod_ isMod_
? I18NextService.i18n.t("remove_as_mod") ? I18NextService.i18n.t("remove_as_mod")
: I18NextService.i18n.t( : I18NextService.i18n.t(
"appoint_as_mod" "appoint_as_mod",
) )
} }
> >
@ -705,7 +694,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<button <button
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"are_you_sure" "are_you_sure",
)} )}
> >
{I18NextService.i18n.t("are_you_sure")} {I18NextService.i18n.t("are_you_sure")}
@ -714,7 +703,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleAddModToCommunity this.handleAddModToCommunity,
)} )}
aria-label={I18NextService.i18n.t("yes")} aria-label={I18NextService.i18n.t("yes")}
> >
@ -728,7 +717,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelConfirmAppointAsMod this.handleCancelConfirmAppointAsMod,
)} )}
aria-label={I18NextService.i18n.t("no")} aria-label={I18NextService.i18n.t("no")}
> >
@ -747,10 +736,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmTransferCommunity this.handleShowConfirmTransferCommunity,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"transfer_community" "transfer_community",
)} )}
> >
{I18NextService.i18n.t("transfer_community")} {I18NextService.i18n.t("transfer_community")}
@ -760,7 +749,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<button <button
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"are_you_sure" "are_you_sure",
)} )}
> >
{I18NextService.i18n.t("are_you_sure")} {I18NextService.i18n.t("are_you_sure")}
@ -769,7 +758,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleTransferCommunity this.handleTransferCommunity,
)} )}
aria-label={I18NextService.i18n.t("yes")} aria-label={I18NextService.i18n.t("yes")}
> >
@ -784,7 +773,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
onClick={linkEvent( onClick={linkEvent(
this, this,
this this
.handleCancelShowConfirmTransferCommunity .handleCancelShowConfirmTransferCommunity,
)} )}
aria-label={I18NextService.i18n.t("no")} aria-label={I18NextService.i18n.t("no")}
> >
@ -801,10 +790,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handlePurgePersonShow this.handlePurgePersonShow,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"purge_user" "purge_user",
)} )}
> >
{I18NextService.i18n.t("purge_user")} {I18NextService.i18n.t("purge_user")}
@ -813,10 +802,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handlePurgeCommentShow this.handlePurgeCommentShow,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"purge_comment" "purge_comment",
)} )}
> >
{I18NextService.i18n.t("purge_comment")} {I18NextService.i18n.t("purge_comment")}
@ -827,10 +816,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanShow this.handleModBanShow,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"ban_from_site" "ban_from_site",
)} )}
> >
{I18NextService.i18n.t("ban_from_site")} {I18NextService.i18n.t("ban_from_site")}
@ -840,10 +829,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleBanPerson this.handleBanPerson,
)} )}
aria-label={I18NextService.i18n.t( aria-label={I18NextService.i18n.t(
"unban_from_site" "unban_from_site",
)} )}
> >
{this.state.banLoading ? ( {this.state.banLoading ? (
@ -862,22 +851,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmAppointAsAdmin this.handleShowConfirmAppointAsAdmin,
)} )}
aria-label={ aria-label={
isAdmin_ isAdmin_
? I18NextService.i18n.t( ? I18NextService.i18n.t(
"remove_as_admin" "remove_as_admin",
) )
: I18NextService.i18n.t( : I18NextService.i18n.t(
"appoint_as_admin" "appoint_as_admin",
) )
} }
> >
{isAdmin_ {isAdmin_
? I18NextService.i18n.t("remove_as_admin") ? I18NextService.i18n.t("remove_as_admin")
: I18NextService.i18n.t( : I18NextService.i18n.t(
"appoint_as_admin" "appoint_as_admin",
)} )}
</button> </button>
) : ( ) : (
@ -889,7 +878,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleAddAdmin this.handleAddAdmin,
)} )}
aria-label={I18NextService.i18n.t("yes")} aria-label={I18NextService.i18n.t("yes")}
> >
@ -903,7 +892,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelConfirmAppointAsAdmin this.handleCancelConfirmAppointAsAdmin,
)} )}
aria-label={I18NextService.i18n.t("no")} aria-label={I18NextService.i18n.t("no")}
> >
@ -919,7 +908,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
</div> </div>
{/* end of button group */} {/* end of button group */}
</div> </>
)} )}
</div> </div>
</article> </article>
@ -941,7 +930,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{I18NextService.i18n.t("x_more_replies", { {I18NextService.i18n.t("x_more_replies", {
count: node.comment_view.counts.child_count, count: node.comment_view.counts.child_count,
formattedCount: numToSI( formattedCount: numToSI(
node.comment_view.counts.child_count node.comment_view.counts.child_count,
), ),
})}{" "} })}{" "}
@ -980,33 +969,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</form> </form>
)} )}
{this.state.showReportDialog && ( {this.state.showReportDialog && (
<form <ReportForm onSubmit={this.handleReportComment} />
className="form-inline"
onSubmit={linkEvent(this, this.handleReportComment)}
>
<label
className="visually-hidden"
htmlFor={`report-reason-${cv.comment.id}`}
>
{I18NextService.i18n.t("reason")}
</label>
<input
type="text"
required
id={`report-reason-${cv.comment.id}`}
className="form-control me-2"
placeholder={I18NextService.i18n.t("reason")}
value={this.state.reportReason}
onInput={linkEvent(this, this.handleReportReasonChange)}
/>
<button
type="submit"
className="btn btn-secondary"
aria-label={I18NextService.i18n.t("create_report")}
>
{I18NextService.i18n.t("create_report")}
</button>
</form>
)} )}
{this.state.showBanDialog && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
@ -1116,7 +1079,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
finished={this.props.finished.get( finished={this.props.finished.get(
this.props.node.comment_view.comment.id this.props.node.comment_view.comment.id,
)} )}
focus focus
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
@ -1136,7 +1099,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
hideImages={this.props.hideImages} hideImages={this.props.hideImages}
isChild={!this.props.noIndent} isChild={!this.props.isTopLevel}
depth={this.props.node.depth + 1} depth={this.props.node.depth + 1}
finished={this.props.finished} finished={this.props.finished}
onCommentReplyRead={this.props.onCommentReplyRead} onCommentReplyRead={this.props.onCommentReplyRead}
@ -1212,19 +1175,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
get myComment(): boolean { get myComment(): boolean {
return ( return (
UserService.Instance.myUserInfo?.local_user_view.person.id == UserService.Instance.myUserInfo?.local_user_view.person.id ===
this.commentView.creator.id this.commentView.creator.id
); );
} }
get isPostCreator(): boolean { get isPostCreator(): boolean {
return this.commentView.creator.id == this.commentView.post.creator_id; return this.commentView.creator.id === this.commentView.post.creator_id;
} }
get scoreColor() { get scoreColor() {
if (this.commentView.my_vote == 1) { if (this.commentView.my_vote === 1) {
return "text-info"; return "text-info";
} else if (this.commentView.my_vote == -1) { } else if (this.commentView.my_vote === -1) {
return "text-danger"; return "text-danger";
} else { } else {
return "text-muted"; return "text-muted";
@ -1281,10 +1244,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState({ showReportDialog: !i.state.showReportDialog }); i.setState({ showReportDialog: !i.state.showReportDialog });
} }
handleReportReasonChange(i: CommentNode, event: any) {
i.setState({ reportReason: event.target.value });
}
handleModRemoveShow(i: CommentNode) { handleModRemoveShow(i: CommentNode) {
i.setState({ i.setState({
showRemoveDialog: !i.state.showRemoveDialog, showRemoveDialog: !i.state.showRemoveDialog,
@ -1301,13 +1260,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
isPersonMentionType( isPersonMentionType(
item: CommentView | PersonMentionView | CommentReplyView item: CommentView | PersonMentionView | CommentReplyView,
): item is PersonMentionView { ): item is PersonMentionView {
return (item as PersonMentionView).person_mention?.id !== undefined; return (item as PersonMentionView).person_mention?.id !== undefined;
} }
isCommentReplyType( isCommentReplyType(
item: CommentView | PersonMentionView | CommentReplyView item: CommentView | PersonMentionView | CommentReplyView,
): item is CommentReplyView { ): item is CommentReplyView {
return (item as CommentReplyView).comment_reply?.id !== undefined; return (item as CommentReplyView).comment_reply?.id !== undefined;
} }
@ -1414,7 +1373,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onSaveComment({ i.props.onSaveComment({
comment_id: i.commentView.comment.id, comment_id: i.commentView.comment.id,
save: !i.commentView.saved, save: !i.commentView.saved,
auth: myAuthRequired(),
}); });
} }
@ -1423,7 +1381,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onBlockPerson({ i.props.onBlockPerson({
person_id: i.commentView.creator.id, person_id: i.commentView.creator.id,
block: true, block: true,
auth: myAuthRequired(),
}); });
} }
@ -1434,13 +1391,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onPersonMentionRead({ i.props.onPersonMentionRead({
person_mention_id: cv.person_mention.id, person_mention_id: cv.person_mention.id,
read: !cv.person_mention.read, read: !cv.person_mention.read,
auth: myAuthRequired(),
}); });
} else if (i.isCommentReplyType(cv)) { } else if (i.isCommentReplyType(cv)) {
i.props.onCommentReplyRead({ i.props.onCommentReplyRead({
comment_reply_id: cv.comment_reply.id, comment_reply_id: cv.comment_reply.id,
read: !cv.comment_reply.read, read: !cv.comment_reply.read,
auth: myAuthRequired(),
}); });
} }
} }
@ -1450,7 +1405,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onDeleteComment({ i.props.onDeleteComment({
comment_id: i.commentId, comment_id: i.commentId,
deleted: !i.commentView.comment.deleted, deleted: !i.commentView.comment.deleted,
auth: myAuthRequired(),
}); });
} }
@ -1460,7 +1414,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onRemoveComment({ i.props.onRemoveComment({
comment_id: i.commentId, comment_id: i.commentId,
removed: !i.commentView.comment.removed, removed: !i.commentView.comment.removed,
auth: myAuthRequired(),
reason: i.state.removeReason, reason: i.state.removeReason,
}); });
} }
@ -1470,7 +1423,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onDistinguishComment({ i.props.onDistinguishComment({
comment_id: i.commentId, comment_id: i.commentId,
distinguished: !i.commentView.comment.distinguished, distinguished: !i.commentView.comment.distinguished,
auth: myAuthRequired(),
}); });
} }
@ -1483,7 +1435,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
reason: i.state.banReason, reason: i.state.banReason,
remove_data: i.state.removeData, remove_data: i.state.removeData,
expires: futureDaysToUnixTime(i.state.banExpireDays), expires: futureDaysToUnixTime(i.state.banExpireDays),
auth: myAuthRequired(),
}); });
} }
@ -1495,13 +1446,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
reason: i.state.banReason, reason: i.state.banReason,
remove_data: i.state.removeData, remove_data: i.state.removeData,
expires: futureDaysToUnixTime(i.state.banExpireDays), expires: futureDaysToUnixTime(i.state.banExpireDays),
auth: myAuthRequired(),
}); });
} }
handleModBanBothSubmit(i: CommentNode, event: any) { handleModBanBothSubmit(i: CommentNode, event: any) {
event.preventDefault(); event.preventDefault();
if (i.state.banType == BanType.Community) { if (i.state.banType === BanType.Community) {
i.handleBanPersonFromCommunity(i); i.handleBanPersonFromCommunity(i);
} else { } else {
i.handleBanPerson(i); i.handleBanPerson(i);
@ -1516,7 +1466,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
community_id: i.commentView.community.id, community_id: i.commentView.community.id,
person_id: i.commentView.creator.id, person_id: i.commentView.creator.id,
added, added,
auth: myAuthRequired(),
}); });
} }
@ -1527,7 +1476,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onAddAdmin({ i.props.onAddAdmin({
person_id: i.commentView.creator.id, person_id: i.commentView.creator.id,
added, added,
auth: myAuthRequired(),
}); });
} }
@ -1536,17 +1484,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.props.onTransferCommunity({ i.props.onTransferCommunity({
community_id: i.commentView.community.id, community_id: i.commentView.community.id,
person_id: i.commentView.creator.id, person_id: i.commentView.creator.id,
auth: myAuthRequired(),
}); });
} }
handleReportComment(i: CommentNode, event: any) { handleReportComment(reason: string) {
event.preventDefault(); this.props.onCommentReport({
i.setState({ reportLoading: true }); comment_id: this.commentId,
i.props.onCommentReport({ reason,
comment_id: i.commentId, });
reason: i.state.reportReason ?? "",
auth: myAuthRequired(), this.setState({
showReportDialog: false,
}); });
} }
@ -1554,17 +1502,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
event.preventDefault(); event.preventDefault();
i.setState({ purgeLoading: true }); i.setState({ purgeLoading: true });
if (i.state.purgeType == PurgeType.Person) { if (i.state.purgeType === PurgeType.Person) {
i.props.onPurgePerson({ i.props.onPurgePerson({
person_id: i.commentView.creator.id, person_id: i.commentView.creator.id,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: myAuthRequired(),
}); });
} else { } else {
i.props.onPurgeComment({ i.props.onPurgeComment({
comment_id: i.commentId, comment_id: i.commentId,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: myAuthRequired(),
}); });
} }
} }
@ -1577,7 +1523,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
limit: 999, // TODO limit: 999, // TODO
type_: "All", type_: "All",
saved_only: false, saved_only: false,
auth: myAuth(),
}); });
} }
} }

View file

@ -35,7 +35,7 @@ interface CommentNodesProps {
admins?: PersonView[]; admins?: PersonView[];
maxCommentsShown?: number; maxCommentsShown?: number;
noBorder?: boolean; noBorder?: boolean;
noIndent?: boolean; isTopLevel?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean; locked?: boolean;
markable?: boolean; markable?: boolean;
@ -86,7 +86,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
this.props.nodes.length > 0 && ( this.props.nodes.length > 0 && (
<ul <ul
className={classNames("comments", { className={classNames("comments", {
"ms-1": !!this.props.isChild, "ms-1": this.props.depth && this.props.depth > 1,
"border-top border-light": !this.props.noBorder, "border-top border-light": !this.props.noBorder,
})} })}
style={ style={
@ -100,7 +100,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
key={node.comment_view.comment.id} key={node.comment_view.comment.id}
node={node} node={node}
noBorder={this.props.noBorder} noBorder={this.props.noBorder}
noIndent={this.props.noIndent} isTopLevel={this.props.isTopLevel}
viewOnly={this.props.viewOnly} viewOnly={this.props.viewOnly}
locked={this.props.locked} locked={this.props.locked}
moderators={this.props.moderators} moderators={this.props.moderators}

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -11,6 +10,7 @@ import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node"; import { CommentNode } from "./comment-node";
import { EMPTY_REQUEST } from "../../services/HttpService";
interface CommentReportProps { interface CommentReportProps {
report: CommentReportView; report: CommentReportView;
@ -33,9 +33,9 @@ export class CommentReport extends Component<
} }
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps> nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>,
): void { ): void {
if (this.props != nextProps) { if (this.props !== nextProps) {
this.setState({ loading: false }); this.setState({ loading: false });
} }
} }
@ -44,7 +44,7 @@ export class CommentReport extends Component<
const r = this.props.report; const r = this.props.report;
const comment = r.comment; const comment = r.comment;
const tippyContent = I18NextService.i18n.t( const tippyContent = I18NextService.i18n.t(
r.comment_report.resolved ? "unresolve_report" : "resolve_report" r.comment_report.resolved ? "unresolve_report" : "resolve_report",
); );
// Set the original post data ( a troll could change it ) // Set the original post data ( a troll could change it )
@ -98,8 +98,8 @@ export class CommentReport extends Component<
onPersonMentionRead={() => {}} onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}} onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}} onBanPerson={() => {}}
onCreateComment={() => Promise.resolve({ state: "empty" })} onCreateComment={() => Promise.resolve(EMPTY_REQUEST)}
onEditComment={() => Promise.resolve({ state: "empty" })} onEditComment={() => Promise.resolve(EMPTY_REQUEST)}
/> />
<div> <div>
{I18NextService.i18n.t("reporter")}:{" "} {I18NextService.i18n.t("reporter")}:{" "}
@ -149,7 +149,6 @@ export class CommentReport extends Component<
i.props.onResolveReport({ i.props.onResolveReport({
report_id: i.props.report.comment_report.id, report_id: i.props.report.comment_report.id,
resolved: !i.props.report.comment_report.resolved, resolved: !i.props.report.comment_report.resolved,
auth: myAuthRequired(),
}); });
} }
} }

View file

@ -0,0 +1,31 @@
import { Component } from "inferno";
import { UserService } from "../../services";
import { Spinner } from "./icon";
interface AnonymousGuardState {
hasRedirected: boolean;
}
class AnonymousGuard extends Component<any, AnonymousGuardState> {
state = {
hasRedirected: false,
} as AnonymousGuardState;
constructor(props: any, context: any) {
super(props, context);
}
componentDidMount() {
if (UserService.Instance.myUserInfo) {
this.context.router.history.replace(`/`);
} else {
this.setState({ hasRedirected: true });
}
}
render() {
return this.state.hasRedirected ? this.props.children : <Spinner />;
}
}
export default AnonymousGuard;

View file

@ -1,12 +1,40 @@
import { InfernoNode } from "inferno"; import { Component } from "inferno";
import { Redirect } from "inferno-router"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { UserService } from "../../services"; import { UserService } from "../../services";
import { Spinner } from "./icon";
function AuthGuard(props: { children?: InfernoNode }) { interface AuthGuardState {
if (!UserService.Instance.myUserInfo) { hasRedirected: boolean;
return <Redirect to="/login" />; }
} else {
return props.children; class AuthGuard extends Component<
RouteComponentProps<Record<string, string>>,
AuthGuardState
> {
state = {
hasRedirected: false,
} as AuthGuardState;
constructor(
props: RouteComponentProps<Record<string, string>>,
context: any,
) {
super(props, context);
}
componentDidMount() {
if (!UserService.Instance.myUserInfo) {
const { pathname, search } = this.props.location;
this.context.router.history.replace(
`/login?prev=${encodeURIComponent(pathname + search)}`,
);
} else {
this.setState({ hasRedirected: true });
}
}
render() {
return this.state.hasRedirected ? this.props.children : <Spinner />;
} }
} }

View file

@ -13,13 +13,13 @@ interface BadgesProps {
} }
const isCommunityAggregates = ( const isCommunityAggregates = (
counts: CommunityAggregates | SiteAggregates counts: CommunityAggregates | SiteAggregates,
): counts is CommunityAggregates => { ): counts is CommunityAggregates => {
return "subscribers" in counts; return "subscribers" in counts;
}; };
const isSiteAggregates = ( const isSiteAggregates = (
counts: CommunityAggregates | SiteAggregates counts: CommunityAggregates | SiteAggregates,
): counts is SiteAggregates => { ): counts is SiteAggregates => {
return "communities" in counts; return "communities" in counts;
}; };
@ -34,7 +34,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{ {
count: Number(counts.users_active_day), count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day), formattedCount: numToSI(counts.users_active_day),
} },
)} )}
> >
{I18NextService.i18n.t("number_of_users", { {I18NextService.i18n.t("number_of_users", {
@ -50,7 +50,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{ {
count: Number(counts.users_active_week), count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week), formattedCount: numToSI(counts.users_active_week),
} },
)} )}
> >
{I18NextService.i18n.t("number_of_users", { {I18NextService.i18n.t("number_of_users", {
@ -66,7 +66,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{ {
count: Number(counts.users_active_month), count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month), formattedCount: numToSI(counts.users_active_month),
} },
)} )}
> >
{I18NextService.i18n.t("number_of_users", { {I18NextService.i18n.t("number_of_users", {
@ -82,7 +82,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{ {
count: Number(counts.users_active_half_year), count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year), formattedCount: numToSI(counts.users_active_half_year),
} },
)} )}
> >
{I18NextService.i18n.t("number_of_users", { {I18NextService.i18n.t("number_of_users", {

View file

@ -48,6 +48,9 @@ export class CommentSortSelect extends Component<
{I18NextService.i18n.t("sort_type")} {I18NextService.i18n.t("sort_type")}
</option> </option>
<option value={"Hot"}>{I18NextService.i18n.t("hot")}</option>, <option value={"Hot"}>{I18NextService.i18n.t("hot")}</option>,
<option value={"Controversial"}>
{I18NextService.i18n.t("controversial")}
</option>
<option value={"Top"}>{I18NextService.i18n.t("top")}</option>, <option value={"Top"}>{I18NextService.i18n.t("top")}</option>,
<option value={"New"}>{I18NextService.i18n.t("new")}</option> <option value={"New"}>{I18NextService.i18n.t("new")}</option>
<option value={"Old"}>{I18NextService.i18n.t("old")}</option> <option value={"Old"}>{I18NextService.i18n.t("old")}</option>

View file

@ -1,4 +1,4 @@
import { Component } from "inferno"; import { Component, RefObject, createRef } from "inferno";
import { getEmojiMart } from "../../markdown"; import { getEmojiMart } from "../../markdown";
interface EmojiMartProps { interface EmojiMartProps {
@ -7,21 +7,24 @@ interface EmojiMartProps {
} }
export class EmojiMart extends Component<EmojiMartProps> { export class EmojiMart extends Component<EmojiMartProps> {
div: RefObject<HTMLDivElement>;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.div = createRef();
this.handleEmojiClick = this.handleEmojiClick.bind(this); this.handleEmojiClick = this.handleEmojiClick.bind(this);
} }
componentDidMount() { componentDidMount() {
const div: any = document.getElementById("emoji-picker"); this.div.current?.appendChild(
if (div) { getEmojiMart(this.handleEmojiClick, this.props.pickerOptions) as any,
div.appendChild( );
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)
);
}
} }
render() { render() {
return <div id="emoji-picker"></div>; return <div id="emoji-picker" ref={this.div} />;
} }
handleEmojiClick(e: any) { handleEmojiClick(e: any) {

View file

@ -77,5 +77,6 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
handleEmojiClick(e: any) { handleEmojiClick(e: any) {
this.props.onEmojiClick?.(e); this.props.onEmojiClick?.(e);
this.setState({ showPicker: false });
} }
} }

View file

@ -8,6 +8,7 @@ import { I18NextService } from "../../services";
interface HtmlTagsProps { interface HtmlTagsProps {
title: string; title: string;
path: string; path: string;
canonicalPath?: string;
description?: string; description?: string;
image?: string; image?: string;
} }
@ -16,6 +17,8 @@ interface HtmlTagsProps {
export class HtmlTags extends Component<HtmlTagsProps, any> { export class HtmlTags extends Component<HtmlTagsProps, any> {
render() { render() {
const url = httpExternalPath(this.props.path); const url = httpExternalPath(this.props.path);
const canonicalUrl =
this.props.canonicalPath ?? httpExternalPath(this.props.path);
const desc = this.props.description; const desc = this.props.description;
const image = this.props.image; const image = this.props.image;
@ -30,6 +33,8 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
<meta key={u} property={u} content={url} /> <meta key={u} property={u} content={url} />
))} ))}
<link rel="canonical" href={canonicalUrl} />
{/* Open Graph / Facebook */} {/* Open Graph / Facebook */}
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
@ -45,10 +50,10 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
name={n} name={n}
content={htmlToText(md.renderInline(desc))} content={htmlToText(md.renderInline(desc))}
/> />
) ),
)} )}
{["og:image", "twitter:image"].map( {["og:image", "twitter:image"].map(
p => image && <meta key={p} property={p} content={image} /> p => image && <meta key={p} property={p} content={image} />,
)} )}
</Helmet> </Helmet>
); );

View file

@ -53,12 +53,12 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
<label <label
className={classNames( className={classNames(
"col-form-label", "col-form-label",
`col-sm-${this.props.multiple ? 3 : 2}` `col-sm-${this.props.multiple ? 3 : 2}`,
)} )}
htmlFor={this.id} htmlFor={this.id}
> >
{I18NextService.i18n.t( {I18NextService.i18n.t(
this.props.multiple ? "language_plural" : "language" this.props.multiple ? "language_plural" : "language",
)} )}
</label> </label>
{this.props.multiple && this.props.showLanguageWarning && ( {this.props.multiple && this.props.showLanguageWarning && (
@ -97,7 +97,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
this.props.siteLanguages, this.props.siteLanguages,
this.props.showAll, this.props.showAll,
this.props.showSite, this.props.showSite,
UserService.Instance.myUserInfo UserService.Instance.myUserInfo,
); );
return ( return (

View file

@ -30,7 +30,7 @@ export class ListingTypeSelect extends Component<
} }
static getDerivedStateFromProps( static getDerivedStateFromProps(
props: ListingTypeSelectProps props: ListingTypeSelectProps,
): ListingTypeSelectState { ): ListingTypeSelectState {
return { return {
type_: props.type_, type_: props.type_,
@ -107,6 +107,27 @@ export class ListingTypeSelect extends Component<
> >
{I18NextService.i18n.t("all")} {I18NextService.i18n.t("all")}
</label> </label>
{(UserService.Instance.myUserInfo?.moderates.length ?? 0) > 0 && (
<>
<input
id={`${this.id}-moderator-view`}
type="radio"
className="btn-check"
value={"ModeratorView"}
checked={this.state.type_ === "ModeratorView"}
onChange={linkEvent(this, this.handleTypeChange)}
/>
<label
htmlFor={`${this.id}-moderator-view`}
title={I18NextService.i18n.t("moderator_view_description")}
className={classNames("pointer btn btn-outline-secondary", {
active: this.state.type_ === "ModeratorView",
})}
>
{I18NextService.i18n.t("moderator_view")}
</label>
</>
)}
</div> </div>
); );
} }

View file

@ -0,0 +1,34 @@
import { Component } from "inferno";
interface LoadingEllipsesState {
ellipses: string;
}
export class LoadingEllipses extends Component<any, LoadingEllipsesState> {
state: LoadingEllipsesState = {
ellipses: "...",
};
#interval?: NodeJS.Timer;
constructor(props: any, context: any) {
super(props, context);
}
render() {
return this.state.ellipses;
}
componentDidMount() {
this.#interval = setInterval(this.#updateEllipses, 1000);
}
componentWillUnmount() {
clearInterval(this.#interval);
}
#updateEllipses = () => {
this.setState(({ ellipses }) => ({
ellipses: ellipses.length === 3 ? "" : ellipses + ".",
}));
};
}

View file

@ -1,9 +1,10 @@
import { isBrowser } from "@utils/browser"; import { isBrowser, platform } from "@utils/browser";
import { numToSI, randomStr } from "@utils/helpers"; import { numToSI, randomStr } from "@utils/helpers";
import autosize from "autosize"; import autosize from "autosize";
import classNames from "classnames"; import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import { Language } from "lemmy-js-client"; import { Language } from "lemmy-js-client";
import { import {
concurrentImageUpload, concurrentImageUpload,
@ -19,9 +20,8 @@ import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiPicker } from "./emoji-picker"; import { EmojiPicker } from "./emoji-picker";
import { Icon, Spinner } from "./icon"; import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select"; import { LanguageSelect } from "./language-select";
import NavigationPrompt from "./navigation-prompt";
import ProgressBar from "./progress-bar"; import ProgressBar from "./progress-bar";
import validUrl from "@utils/helpers/valid-url";
interface MarkdownTextAreaProps { interface MarkdownTextAreaProps {
/** /**
* Initial content inside the textarea * Initial content inside the textarea
@ -49,7 +49,7 @@ interface MarkdownTextAreaProps {
hideNavigationWarnings?: boolean; hideNavigationWarnings?: boolean;
onContentChange?(val: string): void; onContentChange?(val: string): void;
onReplyCancel?(): void; onReplyCancel?(): void;
onSubmit?(content: string, formId: string, languageId?: number): void; onSubmit?(content: string, languageId?: number): void;
allLanguages: Language[]; // TODO should probably be nullable allLanguages: Language[]; // TODO should probably be nullable
siteLanguages: number[]; // TODO same siteLanguages: number[]; // TODO same
} }
@ -89,6 +89,7 @@ export class MarkdownTextArea extends Component<
super(props, context); super(props, context);
this.handleLanguageChange = this.handleLanguageChange.bind(this); this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.handleEmoji = this.handleEmoji.bind(this);
if (isBrowser()) { if (isBrowser()) {
this.tribute = setupTribute(); this.tribute = setupTribute();
@ -138,18 +139,14 @@ export class MarkdownTextArea extends Component<
render() { render() {
const languageId = this.state.languageId; const languageId = this.state.languageId;
// TODO add these prompts back in at some point
// <Prompt
// when={!this.props.hideNavigationWarnings && this.state.content}
// message={I18NextService.i18n.t("block_leaving")}
// />
return ( return (
<form <form
className="markdown-textarea" className="markdown-textarea"
id={this.formId} id={this.formId}
onSubmit={linkEvent(this, this.handleSubmit)} onSubmit={linkEvent(this, this.handleSubmit)}
> >
<NavigationPrompt <Prompt
message={I18NextService.i18n.t("block_leaving")}
when={ when={
!this.props.hideNavigationWarnings && !this.props.hideNavigationWarnings &&
!!this.state.content && !!this.state.content &&
@ -167,9 +164,7 @@ export class MarkdownTextArea extends Component<
{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={this.handleEmoji}></EmojiPicker>
onEmojiClick={e => this.handleEmoji(this, e)}
></EmojiPicker>
<label <label
htmlFor={`file-upload-${this.id}`} htmlFor={`file-upload-${this.id}`}
className={classNames("mb-0", { className={classNames("mb-0", {
@ -206,7 +201,7 @@ export class MarkdownTextArea extends Component<
{this.getFormatButton("header", this.handleInsertHeader)} {this.getFormatButton("header", this.handleInsertHeader)}
{this.getFormatButton( {this.getFormatButton(
"strikethrough", "strikethrough",
this.handleInsertStrikethrough this.handleInsertStrikethrough,
)} )}
{this.getFormatButton("quote", this.handleInsertQuote)} {this.getFormatButton("quote", this.handleInsertQuote)}
{this.getFormatButton("list", this.handleInsertList)} {this.getFormatButton("list", this.handleInsertList)}
@ -214,7 +209,7 @@ export class MarkdownTextArea extends Component<
{this.getFormatButton("subscript", this.handleInsertSubscript)} {this.getFormatButton("subscript", this.handleInsertSubscript)}
{this.getFormatButton( {this.getFormatButton(
"superscript", "superscript",
this.handleInsertSuperscript this.handleInsertSuperscript,
)} )}
{this.getFormatButton("spoiler", this.handleInsertSpoiler)} {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
<a <a
@ -234,11 +229,11 @@ export class MarkdownTextArea extends Component<
"form-control border-0 rounded-top-0 rounded-bottom", "form-control border-0 rounded-top-0 rounded-bottom",
{ {
"d-none": this.state.previewMode, "d-none": this.state.previewMode,
} },
)} )}
value={this.state.content} value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)} onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} onPaste={linkEvent(this, this.handlePaste)}
onKeyDown={linkEvent(this, this.handleKeyBinds)} onKeyDown={linkEvent(this, this.handleKeyBinds)}
required required
disabled={this.isDisabled} disabled={this.isDisabled}
@ -263,7 +258,7 @@ export class MarkdownTextArea extends Component<
value={this.state.imageUploadStatus.uploaded} value={this.state.imageUploadStatus.uploaded}
max={this.state.imageUploadStatus.total} max={this.state.imageUploadStatus.total}
text={ text={
I18NextService.i18n.t("pictures_uploded_progess", { I18NextService.i18n.t("pictures_uploaded_progess", {
uploaded: this.state.imageUploadStatus.uploaded, uploaded: this.state.imageUploadStatus.uploaded,
total: this.state.imageUploadStatus.total, total: this.state.imageUploadStatus.total,
}) ?? undefined }) ?? undefined
@ -333,7 +328,7 @@ export class MarkdownTextArea extends Component<
getFormatButton( getFormatButton(
type: NoOptionI18nKeys, type: NoOptionI18nKeys,
handleClick: (i: MarkdownTextArea, event: any) => void handleClick: (i: MarkdownTextArea, event: any) => void,
) { ) {
let iconType: string; let iconType: string;
@ -363,26 +358,65 @@ export class MarkdownTextArea extends Component<
); );
} }
handleEmoji(i: MarkdownTextArea, e: any) { handleEmoji(e: any) {
let value = e.native; let value = e.native;
if (value == null) { if (!value) {
const emoji = customEmojisLookup.get(e.id)?.custom_emoji; const emoji = customEmojisLookup.get(e.id)?.custom_emoji;
if (emoji) { if (emoji) {
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`; value = `![${emoji.alt_text}](${emoji.image_url} "emoji ${emoji.shortcode}")`;
} }
} }
i.setState({ this.setState({
content: `${i.state.content ?? ""} ${value} `, content: `${this.state.content ?? ""} ${value} `,
}); });
i.contentChange(); this.contentChange();
const textarea: any = document.getElementById(i.id); const textarea: any = document.getElementById(this.id);
autosize.update(textarea); autosize.update(textarea);
} }
handleImageUploadPaste(i: MarkdownTextArea, event: any) { handlePaste(i: MarkdownTextArea, event: ClipboardEvent) {
if (!event.clipboardData) return;
// check clipboard files
const image = event.clipboardData.files[0]; const image = event.clipboardData.files[0];
if (image) { if (image) {
i.handleImageUpload(i, image); i.handleImageUpload(i, image);
return;
}
// check clipboard url
const url = event.clipboardData.getData("text");
if (validUrl(url)) {
i.handleUrlPaste(url, i, event);
}
}
handleUrlPaste(url: string, i: MarkdownTextArea, event: ClipboardEvent) {
// query textarea element
const textarea = document.getElementById(i.id);
if (textarea instanceof HTMLTextAreaElement) {
const { selectionStart, selectionEnd } = textarea;
// if no selection, just insert url
if (selectionStart === selectionEnd) return;
event.preventDefault();
const selectedText = i.getSelectedText();
// update textarea content
i.setState(({ content }) => ({
content: `${
content?.substring(0, selectionStart) ?? ""
}[${selectedText}](${url})${content?.substring(selectionEnd) ?? ""}`,
}));
i.contentChange();
// shift selection 1 to the right
textarea.setSelectionRange(
selectionStart + 1,
selectionStart + 1 + selectedText.length,
);
} }
} }
@ -401,7 +435,7 @@ export class MarkdownTextArea extends Component<
count: Number(maxUploadImages), count: Number(maxUploadImages),
formattedCount: numToSI(maxUploadImages), formattedCount: numToSI(maxUploadImages),
}), }),
"danger" "danger",
); );
} else { } else {
i.setState({ i.setState({
@ -429,7 +463,7 @@ export class MarkdownTextArea extends Component<
uploaded: (imageUploadStatus?.uploaded ?? 0) + 1, uploaded: (imageUploadStatus?.uploaded ?? 0) + 1,
}, },
})); }));
}) }),
); );
} catch (e) { } catch (e) {
errorOccurred = true; errorOccurred = true;
@ -481,7 +515,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 || event.metaKey) { if (platform.isMac() ? event.metaKey : event.ctrlKey) {
switch (event.key) { switch (event.key) {
case "k": { case "k": {
i.handleInsertLink(i, event); i.handleInsertLink(i, event);
@ -539,7 +573,7 @@ export class MarkdownTextArea extends Component<
event.preventDefault(); event.preventDefault();
if (i.state.content) { if (i.state.content) {
i.setState({ loading: true, submitted: true }); i.setState({ loading: true, submitted: true });
i.props.onSubmit?.(i.state.content, i.formId, i.state.languageId); i.props.onSubmit?.(i.state.content, i.state.languageId);
} }
} }
@ -565,7 +599,7 @@ export class MarkdownTextArea extends Component<
i.setState({ i.setState({
content: `${content?.substring( content: `${content?.substring(
0, 0,
start start,
)}[${selectedText}]()${content?.substring(end)}`, )}[${selectedText}]()${content?.substring(end)}`,
}); });
textarea.focus(); textarea.focus();
@ -589,7 +623,7 @@ export class MarkdownTextArea extends Component<
simpleSurroundBeforeAfter( simpleSurroundBeforeAfter(
beforeChars: string, beforeChars: string,
afterChars: string, afterChars: string,
emptyChars = "___" emptyChars = "___",
) { ) {
const content = this.state.content ?? ""; const content = this.state.content ?? "";
if (!this.state.content) { if (!this.state.content) {
@ -604,7 +638,7 @@ export class MarkdownTextArea extends Component<
this.setState({ this.setState({
content: `${content?.substring( content: `${content?.substring(
0, 0,
start start,
)}${beforeChars}${selectedText}${afterChars}${content?.substring(end)}`, )}${beforeChars}${selectedText}${afterChars}${content?.substring(end)}`,
}); });
} else { } else {
@ -619,12 +653,12 @@ export class MarkdownTextArea extends Component<
if (start !== end) { if (start !== end) {
textarea.setSelectionRange( textarea.setSelectionRange(
start + beforeChars.length, start + beforeChars.length,
end + afterChars.length end + afterChars.length,
); );
} else { } else {
textarea.setSelectionRange( textarea.setSelectionRange(
start + beforeChars.length, start + beforeChars.length,
end + emptyChars.length + afterChars.length end + emptyChars.length + afterChars.length,
); );
} }

View file

@ -25,11 +25,11 @@ export class MomentTime extends Component<MomentTimeProps, any> {
createdAndModifiedTimes() { createdAndModifiedTimes() {
const updated = this.props.updated; const updated = this.props.updated;
let line = `${capitalizeFirstLetter( let line = `${capitalizeFirstLetter(
I18NextService.i18n.t("created") I18NextService.i18n.t("created"),
)}: ${formatDate(this.props.published)}`; )}: ${formatDate(this.props.published)}`;
if (updated) { if (updated) {
line += `\n\n\n${capitalizeFirstLetter( line += `\n\n\n${capitalizeFirstLetter(
I18NextService.i18n.t("modified") I18NextService.i18n.t("modified"),
)} ${formatDate(updated)}`; )} ${formatDate(updated)}`;
} }
return line; return line;

View file

@ -1,53 +0,0 @@
import { Component } from "inferno";
import { I18NextService } from "../../services";
export interface IPromptProps {
when: boolean;
}
export default class NavigationPrompt extends Component<IPromptProps, any> {
public unblock;
public enable() {
if (this.unblock) {
this.unblock();
}
this.unblock = this.context.router.history.block(tx => {
if (window.confirm(I18NextService.i18n.t("block_leaving") ?? undefined)) {
this.unblock();
tx.retry();
}
});
}
public disable() {
if (this.unblock) {
this.unblock();
this.unblock = null;
}
}
public componentWillMount() {
if (this.props.when) {
this.enable();
}
}
public componentWillReceiveProps(nextProps: IPromptProps) {
if (nextProps.when) {
if (!this.props.when) {
this.enable();
}
} else {
this.disable();
}
}
public componentWillUnmount() {
this.disable();
}
public render() {
return null;
}
}

View file

@ -0,0 +1,46 @@
import { Component, linkEvent } from "inferno";
import { I18NextService } from "../../services";
import { PaginationCursor } from "lemmy-js-client";
interface PaginatorCursorProps {
prevPage?: PaginationCursor;
nextPage?: PaginationCursor;
onNext(val: PaginationCursor): void;
onPrev(): void;
}
function handlePrev(i: PaginatorCursor) {
i.props.onPrev();
}
function handleNext(i: PaginatorCursor) {
if (i.props.nextPage) {
i.props.onNext(i.props.nextPage);
}
}
export class PaginatorCursor extends Component<PaginatorCursorProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<div className="paginator my-2">
<button
className="btn btn-secondary me-2"
disabled={!this.props.prevPage}
onClick={linkEvent(this, handlePrev)}
>
{I18NextService.i18n.t("prev")}
</button>
<button
className="btn btn-secondary"
onClick={linkEvent(this, handleNext)}
disabled={!this.props.nextPage}
>
{I18NextService.i18n.t("next")}
</button>
</div>
);
}
}

View file

@ -4,6 +4,7 @@ import { I18NextService } from "../../services";
interface PaginatorProps { interface PaginatorProps {
page: number; page: number;
onChange(val: number): any; onChange(val: number): any;
nextDisabled: boolean;
} }
export class Paginator extends Component<PaginatorProps, any> { export class Paginator extends Component<PaginatorProps, any> {
@ -15,7 +16,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<div className="paginator my-2"> <div className="paginator my-2">
<button <button
className="btn btn-secondary me-2" className="btn btn-secondary me-2"
disabled={this.props.page == 1} disabled={this.props.page === 1}
onClick={linkEvent(this, this.handlePrev)} onClick={linkEvent(this, this.handlePrev)}
> >
{I18NextService.i18n.t("prev")} {I18NextService.i18n.t("prev")}
@ -23,6 +24,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<button <button
className="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleNext)} onClick={linkEvent(this, this.handleNext)}
disabled={this.props.nextDisabled || false}
> >
{I18NextService.i18n.t("next")} {I18NextService.i18n.t("next")}
</button> </button>

View file

@ -0,0 +1,159 @@
import { Options, passwordStrength } from "check-password-strength";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, FormEventHandler, linkEvent } from "inferno";
import { NavLink } from "inferno-router";
import { I18NextService } from "../../services";
import { Icon } from "./icon";
interface PasswordInputProps {
id: string;
value?: string;
onInput: FormEventHandler<HTMLInputElement>;
className?: string;
showStrength?: boolean;
label?: string | null;
showForgotLink?: boolean;
isNew?: boolean;
}
interface PasswordInputState {
show: boolean;
}
const passwordStrengthOptions: Options<string> = [
{
id: 0,
value: "very_weak",
minDiversity: 0,
minLength: 0,
},
{
id: 1,
value: "weak",
minDiversity: 2,
minLength: 10,
},
{
id: 2,
value: "medium",
minDiversity: 3,
minLength: 12,
},
{
id: 3,
value: "strong",
minDiversity: 4,
minLength: 14,
},
];
function handleToggleShow(i: PasswordInput) {
i.setState(prev => ({
...prev,
show: !prev.show,
}));
}
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
state: PasswordInputState = {
show: false,
};
constructor(props: PasswordInputProps, context: any) {
super(props, context);
}
render() {
const {
props: {
id,
value,
onInput,
className,
showStrength,
label,
showForgotLink,
isNew,
},
state: { show },
} = this;
return (
<>
<div className={classNames("row", className)}>
{label && (
<label className="col-sm-2 col-form-label" htmlFor={id}>
{label}
</label>
)}
<div className={`col-sm-${label ? 10 : 12}`}>
<div className="input-group">
<input
type={show ? "text" : "password"}
className="form-control"
aria-describedby={id}
autoComplete={isNew ? "new-password" : "current-password"}
onInput={onInput}
value={value}
required
maxLength={60}
minLength={10}
/>
<button
className="btn btn-outline-dark"
type="button"
id={id}
onClick={linkEvent(this, handleToggleShow)}
aria-label={I18NextService.i18n.t(
`${show ? "show" : "hide"}_password`,
)}
data-tippy-content={I18NextService.i18n.t(
`${show ? "show" : "hide"}_password`,
)}
>
<Icon icon={`eye${show ? "-slash" : ""}`} inline />
</button>
</div>
{showStrength && value && (
<div className={this.passwordColorClass}>
{I18NextService.i18n.t(
this.passwordStrength as NoOptionI18nKeys,
)}
</div>
)}
{showForgotLink && (
<NavLink
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
to="/login_reset"
>
{I18NextService.i18n.t("forgot_password")}
</NavLink>
)}
</div>
</div>
</>
);
}
get passwordStrength(): string | undefined {
const password = this.props.value;
return password
? passwordStrength(password, passwordStrengthOptions).value
: undefined;
}
get passwordColorClass(): string {
const strength = this.passwordStrength;
if (strength && ["weak", "medium"].includes(strength)) {
return "text-warning";
} else if (strength === "strong") {
return "text-success";
} else {
return "text-danger";
}
}
}
export default PasswordInput;

View file

@ -1,6 +1,8 @@
import classNames from "classnames"; import classNames from "classnames";
import { Component } from "inferno"; import { Component } from "inferno";
import { UserService } from "../../services";
const iconThumbnailSize = 96; const iconThumbnailSize = 96;
const thumbnailSize = 256; const thumbnailSize = 256;
@ -13,6 +15,7 @@ interface PictrsImageProps {
nsfw?: boolean; nsfw?: boolean;
iconOverlay?: boolean; iconOverlay?: boolean;
pushup?: boolean; pushup?: boolean;
cardTop?: boolean;
} }
export class PictrsImage extends Component<PictrsImageProps, any> { export class PictrsImage extends Component<PictrsImageProps, any> {
@ -21,28 +24,40 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
} }
render() { render() {
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
this.props;
let user_blur_nsfw = true;
if (UserService.Instance.myUserInfo) {
user_blur_nsfw =
UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw;
}
const blur_image = nsfw && user_blur_nsfw;
return ( return (
<picture> <picture>
<source srcSet={this.src("webp")} type="image/webp" /> <source srcSet={this.src("webp")} type="image/webp" />
<source srcSet={this.props.src} /> <source srcSet={src} />
<source srcSet={this.src("jpg")} type="image/jpeg" /> <source srcSet={this.src("jpg")} type="image/jpeg" />
<img <img
src={this.props.src} src={src}
alt={this.alt()} alt={this.alt()}
title={this.alt()} title={this.alt()}
loading="lazy" loading="lazy"
className={classNames("overflow-hidden pictrs-image", { className={classNames("overflow-hidden pictrs-image", {
"img-fluid": !this.props.icon && !this.props.iconOverlay, "img-fluid": !(icon || iconOverlay),
banner: this.props.banner, banner,
"thumbnail rounded object-fit-cover": "thumbnail rounded object-fit-cover":
this.props.thumbnail && !this.props.icon && !this.props.banner, thumbnail && !(icon || banner),
"img-expanded slight-radius": "img-expanded slight-radius": !(thumbnail || icon),
!this.props.thumbnail && !this.props.icon, "img-blur": thumbnail && nsfw,
"img-blur": this.props.thumbnail && this.props.nsfw, "object-fit-cover img-icon me-1": icon,
"object-fit-cover img-icon me-1": this.props.icon, "img-blur-icon": icon && blur_image,
"img-blur-thumb": thumbnail && blur_image,
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay": "ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
this.props.iconOverlay, iconOverlay,
"avatar-pushup": this.props.pushup, "avatar-pushup": pushup,
"card-img-top": cardTop,
})} })}
/> />
</picture> </picture>
@ -56,7 +71,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
const split = this.props.src.split("/pictrs/image/"); const split = this.props.src.split("/pictrs/image/");
// If theres not multiple, then its not a pictrs image // If theres not multiple, then its not a pictrs image
if (split.length == 1) { if (split.length === 1) {
return this.props.src; return this.props.src;
} }

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -42,9 +41,9 @@ export class RegistrationApplication extends Component<
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly< nextProps: Readonly<
{ children?: InfernoNode } & RegistrationApplicationProps { children?: InfernoNode } & RegistrationApplicationProps
> >,
): void { ): void {
if (this.props != nextProps) { if (this.props !== nextProps) {
this.setState({ this.setState({
denyExpanded: false, denyExpanded: false,
approveLoading: false, approveLoading: false,
@ -149,7 +148,6 @@ export class RegistrationApplication extends Component<
i.props.onApproveApplication({ i.props.onApproveApplication({
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
approve: true, approve: true,
auth: myAuthRequired(),
}); });
} }
@ -160,7 +158,6 @@ export class RegistrationApplication extends Component<
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
approve: false, approve: false,
deny_reason: i.state.denyReason, deny_reason: i.state.denyReason,
auth: myAuthRequired(),
}); });
} else { } else {
i.setState({ denyExpanded: true }); i.setState({ denyExpanded: true });

View file

@ -0,0 +1,69 @@
import { Component, linkEvent } from "inferno";
import { I18NextService } from "../../services/I18NextService";
import { Spinner } from "./icon";
import { randomStr } from "@utils/helpers";
interface ReportFormProps {
onSubmit: (reason: string) => void;
}
interface ReportFormState {
loading: boolean;
reason: string;
}
function handleReportReasonChange(i: ReportForm, event: any) {
i.setState({ reason: event.target.value });
}
function handleReportSubmit(i: ReportForm, event: any) {
event.preventDefault();
i.setState({ loading: true });
i.props.onSubmit(i.state.reason);
i.setState({
loading: false,
reason: "",
});
}
export default class ReportForm extends Component<
ReportFormProps,
ReportFormState
> {
state: ReportFormState = {
loading: false,
reason: "",
};
constructor(props, context) {
super(props, context);
}
render() {
const { loading, reason } = this.state;
const id = `report-form-${randomStr()}`;
return (
<form
className="form-inline"
onSubmit={linkEvent(this, handleReportSubmit)}
>
<label className="visually-hidden" htmlFor={id}>
{I18NextService.i18n.t("reason")}
</label>
<input
type="text"
id={id}
className="form-control me-2"
placeholder={I18NextService.i18n.t("reason")}
required
value={reason}
onInput={linkEvent(this, handleReportReasonChange)}
/>
<button type="submit" className="btn btn-secondary">
{loading ? <Spinner /> : I18NextService.i18n.t("create_report")}
</button>
</form>
);
}
}

View file

@ -9,6 +9,7 @@ import {
} from "inferno"; } from "inferno";
import { I18NextService } from "../../services"; import { I18NextService } from "../../services";
import { Icon, Spinner } from "./icon"; import { Icon, Spinner } from "./icon";
import { LoadingEllipses } from "./loading-ellipses";
interface SearchableSelectProps { interface SearchableSelectProps {
id: string; id: string;
@ -22,7 +23,6 @@ interface SearchableSelectProps {
interface SearchableSelectState { interface SearchableSelectState {
selectedIndex: number; selectedIndex: number;
searchText: string; searchText: string;
loadingEllipses: string;
} }
function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) { function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
@ -70,12 +70,10 @@ export class SearchableSelect extends Component<
> { > {
searchInputRef: RefObject<HTMLInputElement> = createRef(); searchInputRef: RefObject<HTMLInputElement> = createRef();
toggleButtonRef: RefObject<HTMLButtonElement> = createRef(); toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
state: SearchableSelectState = { state: SearchableSelectState = {
selectedIndex: 0, selectedIndex: 0,
searchText: "", searchText: "",
loadingEllipses: "...",
}; };
constructor(props: SearchableSelectProps, context: any) { constructor(props: SearchableSelectProps, context: any) {
@ -83,7 +81,7 @@ export class SearchableSelect extends Component<
if (props.value) { if (props.value) {
let selectedIndex = props.options.findIndex( let selectedIndex = props.options.findIndex(
({ value }) => value === props.value?.toString() ({ value }) => value === props.value?.toString(),
); );
if (selectedIndex < 0) { if (selectedIndex < 0) {
@ -99,7 +97,7 @@ export class SearchableSelect extends Component<
render() { render() {
const { id, options, onSearch, loading } = this.props; const { id, options, onSearch, loading } = this.props;
const { searchText, selectedIndex, loadingEllipses } = this.state; const { searchText, selectedIndex } = this.state;
return ( return (
<div className="searchable-select dropdown col-12 col-sm-auto flex-grow-1"> <div className="searchable-select dropdown col-12 col-sm-auto flex-grow-1">
@ -116,9 +114,14 @@ export class SearchableSelect extends Component<
onClick={linkEvent(this, focusSearch)} onClick={linkEvent(this, focusSearch)}
ref={this.toggleButtonRef} ref={this.toggleButtonRef}
> >
{loading {loading ? (
? `${I18NextService.i18n.t("loading")}${loadingEllipses}` <>
: options[selectedIndex].label} {I18NextService.i18n.t("loading")}
<LoadingEllipses />
</>
) : (
options[selectedIndex].label
)}
</button> </button>
<div className="modlog-choices-font-size dropdown-menu w-100 p-2"> <div className="modlog-choices-font-size dropdown-menu w-100 p-2">
<div className="input-group"> <div className="input-group">
@ -140,7 +143,7 @@ export class SearchableSelect extends Component<
(onSearch || searchText.length === 0 (onSearch || searchText.length === 0
? options ? options
: options.filter(({ label }) => : options.filter(({ label }) =>
label.toLowerCase().includes(searchText.toLowerCase()) label.toLowerCase().includes(searchText.toLowerCase()),
) )
).map((option, index) => ( ).map((option, index) => (
<button <button
@ -180,24 +183,4 @@ export class SearchableSelect extends Component<
selectedIndex, selectedIndex,
}; };
} }
componentDidUpdate() {
const { loading } = this.props;
if (loading && !this.loadingEllipsesInterval) {
this.loadingEllipsesInterval = setInterval(() => {
this.setState(({ loadingEllipses }) => ({
loadingEllipses:
loadingEllipses.length === 3 ? "" : loadingEllipses + ".",
}));
}, 750);
} else if (!loading && this.loadingEllipsesInterval) {
clearInterval(this.loadingEllipsesInterval);
}
}
componentWillUnmount() {
if (this.loadingEllipsesInterval) {
clearInterval(this.loadingEllipsesInterval);
}
}
} }

View file

@ -53,7 +53,13 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<option key={"Active"} value={"Active"}> <option key={"Active"} value={"Active"}>
{I18NextService.i18n.t("active")} {I18NextService.i18n.t("active")}
</option>, </option>,
<option key={"Scaled"} value={"Scaled"}>
{I18NextService.i18n.t("scaled")}
</option>,
]} ]}
<option value={"Controversial"}>
{I18NextService.i18n.t("controversial")}
</option>
<option value={"New"}>{I18NextService.i18n.t("new")}</option> <option value={"New"}>{I18NextService.i18n.t("new")}</option>
<option value={"Old"}>{I18NextService.i18n.t("old")}</option> <option value={"Old"}>{I18NextService.i18n.t("old")}</option>
{!this.props.hideMostComments && [ {!this.props.hideMostComments && [
@ -79,6 +85,15 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<option value={"TopMonth"}> <option value={"TopMonth"}>
{I18NextService.i18n.t("top_month")} {I18NextService.i18n.t("top_month")}
</option> </option>
<option value={"TopThreeMonths"}>
{I18NextService.i18n.t("top_three_months")}
</option>
<option value={"TopSixMonths"}>
{I18NextService.i18n.t("top_six_months")}
</option>
<option value={"TopNineMonths"}>
{I18NextService.i18n.t("top_nine_months")}
</option>
<option value={"TopYear"}>{I18NextService.i18n.t("top_year")}</option> <option value={"TopYear"}>{I18NextService.i18n.t("top_year")}</option>
<option value={"TopAll"}>{I18NextService.i18n.t("top_all")}</option> <option value={"TopAll"}>{I18NextService.i18n.t("top_all")}</option>
</select> </select>

View file

@ -0,0 +1,229 @@
import { validInstanceTLD } from "@utils/helpers";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, MouseEventHandler, linkEvent } from "inferno";
import { CommunityView } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { VERSION } from "../../version";
import { Icon, Spinner } from "./icon";
import { toast } from "../../toast";
interface SubscribeButtonProps {
communityView: CommunityView;
onFollow: MouseEventHandler;
onUnFollow: MouseEventHandler;
loading?: boolean;
isLink?: boolean;
}
export function SubscribeButton({
communityView: {
subscribed,
community: { actor_id },
},
onFollow,
onUnFollow,
loading = false,
isLink = false,
}: SubscribeButtonProps) {
let i18key: NoOptionI18nKeys;
switch (subscribed) {
case "NotSubscribed": {
i18key = "subscribe";
break;
}
case "Subscribed": {
i18key = "joined";
break;
}
default: {
i18key = "subscribe_pending";
break;
}
}
const buttonClass = classNames(
"btn",
isLink ? "btn-link d-inline-block" : "d-block mb-2 w-100",
);
if (!UserService.Instance.myUserInfo) {
return (
<>
<button
type="button"
className={classNames(buttonClass, {
"btn-secondary": !isLink,
})}
data-bs-toggle="modal"
data-bs-target="#remoteFetchModal"
>
{I18NextService.i18n.t("subscribe")}
</button>
<RemoteFetchModal communityActorId={actor_id} />
</>
);
}
return (
<button
type="button"
className={classNames(buttonClass, {
[`btn-${subscribed === "Pending" ? "warning" : "secondary"}`]: !isLink,
})}
onClick={subscribed === "NotSubscribed" ? onFollow : onUnFollow}
>
{loading ? (
<Spinner />
) : (
<>
{subscribed === "Subscribed" && (
<Icon icon="check" classes="icon-inline me-1" />
)}
{I18NextService.i18n.t(i18key)}
</>
)}
</button>
);
}
interface RemoteFetchModalProps {
communityActorId: string;
}
interface RemoteFetchModalState {
instanceText: string;
}
function handleInput(i: RemoteFetchModal, event: any) {
i.setState({ instanceText: event.target.value });
}
function focusInput() {
document.getElementById("remoteFetchInstance")?.focus();
}
function submitRemoteFollow(
{ state: { instanceText }, props: { communityActorId } }: RemoteFetchModal,
event: Event,
) {
event.preventDefault();
instanceText = instanceText.trim();
if (!validInstanceTLD(instanceText)) {
toast(
I18NextService.i18n.t("remote_follow_invalid_instance", {
instance: instanceText,
}),
"danger",
);
return;
}
const protocolRegex = /^https?:\/\//;
if (instanceText.replace(protocolRegex, "") === window.location.host) {
toast(I18NextService.i18n.t("remote_follow_local_instance"), "danger");
return;
}
if (!protocolRegex.test(instanceText)) {
instanceText = `http${VERSION !== "dev" ? "s" : ""}://${instanceText}`;
}
window.location.href = `${instanceText}/activitypub/externalInteraction?uri=${encodeURIComponent(
communityActorId,
)}`;
}
class RemoteFetchModal extends Component<
RemoteFetchModalProps,
RemoteFetchModalState
> {
state: RemoteFetchModalState = {
instanceText: "",
};
constructor(props: any, context: any) {
super(props, context);
}
componentDidMount() {
document
.getElementById("remoteFetchModal")
?.addEventListener("shown.bs.modal", focusInput);
}
componentWillUnmount(): void {
document
.getElementById("remoteFetchModal")
?.removeEventListener("shown.bs.modal", focusInput);
}
render() {
return (
<div
className="modal fade"
id="remoteFetchModal"
tabIndex={-1}
aria-hidden
aria-labelledby="#remoteFetchModalTitle"
>
<div className="modal-dialog modal-fullscreen-sm-down">
<div className="modal-content">
<header className="modal-header">
<h3 className="modal-title" id="remoteFetchModalTitle">
{I18NextService.i18n.t("remote_follow_modal_title")}
</h3>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
/>
</header>
<form
id="remote-fetch-form"
className="modal-body d-flex flex-column justify-content-center"
onSubmit={linkEvent(this, submitRemoteFollow)}
>
<label className="form-label" htmlFor="remoteFetchInstance">
{I18NextService.i18n.t("remote_follow_prompt")}
</label>
<input
type="text"
id="remoteFetchInstance"
className="form-control"
name="instance"
value={this.state.instanceText}
onInput={linkEvent(this, handleInput)}
required
enterKeyHint="go"
inputMode="url"
/>
</form>
<footer className="modal-footer">
<button
type="button"
className="btn btn-danger"
data-bs-dismiss="modal"
>
{I18NextService.i18n.t("cancel")}
</button>
<button
type="submit"
className="btn btn-success"
form="remote-fetch-form"
>
{I18NextService.i18n.t("fetch_community")}
</button>
</footer>
</div>
</div>
</div>
);
}
}

View file

@ -0,0 +1,244 @@
import {
Component,
MouseEventHandler,
RefObject,
createRef,
linkEvent,
} from "inferno";
import { I18NextService } from "../../services";
import { toast } from "../../toast";
import type { Modal } from "bootstrap";
interface TotpModalProps {
/**Takes totp as param, returns whether submit was successful*/
onSubmit: (totp: string) => Promise<boolean>;
onClose: MouseEventHandler;
type: "login" | "remove" | "generate";
secretUrl?: string;
show?: boolean;
}
interface TotpModalState {
totp: string;
qrCode?: string;
}
const TOTP_LENGTH = 6;
async function handleSubmit(i: TotpModal, totp: string) {
const successful = await i.props.onSubmit(totp);
if (!successful) {
i.setState({ totp: "" });
i.inputRef.current?.focus();
}
}
function handleInput(i: TotpModal, event: any) {
if (isNaN(event.target.value)) {
return;
}
i.setState({
totp: event.target.value,
});
const { totp } = i.state;
if (totp.length >= TOTP_LENGTH) {
handleSubmit(i, totp);
}
}
function handlePaste(i: TotpModal, event: any) {
event.preventDefault();
const text: string = event.clipboardData.getData("text")?.trim();
if (text.length > TOTP_LENGTH || isNaN(Number(text))) {
toast(I18NextService.i18n.t("invalid_totp_code"), "danger");
i.clearTotp();
} else {
i.setState({ totp: text });
if (text.length === TOTP_LENGTH) {
handleSubmit(i, text);
}
}
}
export default class TotpModal extends Component<
TotpModalProps,
TotpModalState
> {
readonly modalDivRef: RefObject<HTMLDivElement>;
readonly inputRef: RefObject<HTMLInputElement>;
modal: Modal;
state: TotpModalState = {
totp: "",
};
constructor(props: TotpModalProps, context: any) {
super(props, context);
this.modalDivRef = createRef();
this.inputRef = createRef();
this.clearTotp = this.clearTotp.bind(this);
this.handleShow = this.handleShow.bind(this);
}
async componentDidMount() {
this.modalDivRef.current?.addEventListener(
"shown.bs.modal",
this.handleShow,
);
this.modalDivRef.current?.addEventListener(
"hidden.bs.modal",
this.clearTotp,
);
const Modal = (await import("bootstrap/js/dist/modal")).default;
this.modal = new Modal(this.modalDivRef.current!);
if (this.props.show) {
this.modal.show();
}
}
componentWillUnmount() {
this.modalDivRef.current?.removeEventListener(
"shown.bs.modal",
this.handleShow,
);
this.modalDivRef.current?.removeEventListener(
"hidden.bs.modal",
this.clearTotp,
);
this.modal.dispose();
}
componentDidUpdate({ show: prevShow }: TotpModalProps) {
if (!!prevShow !== !!this.props.show) {
if (this.props.show) {
this.modal.show();
} else {
this.modal.hide();
}
}
}
render() {
const { type, secretUrl, onClose } = this.props;
const { totp } = this.state;
return (
<div
className="modal fade"
id="totpModal"
tabIndex={-1}
aria-hidden
aria-labelledby="#totpModalTitle"
data-bs-backdrop="static"
ref={this.modalDivRef}
>
<div className="modal-dialog modal-fullscreen-sm-down">
<div className="modal-content">
<header className="modal-header">
<h3 className="modal-title" id="totpModalTitle">
{I18NextService.i18n.t(
type === "generate"
? "enable_totp"
: type === "remove"
? "disable_totp"
: "enter_totp_code",
)}
</h3>
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={onClose}
/>
</header>
<div className="modal-body d-flex flex-column align-items-center justify-content-center">
{type === "generate" && (
<div>
<a
className="btn btn-secondary mx-auto d-block totp-link"
href={secretUrl}
>
{I18NextService.i18n.t("totp_link")}
</a>
<div className="mx-auto mt-3 w-50 h-50 text-center">
<strong className="fw-semibold">
{I18NextService.i18n.t("totp_qr_segue")}
</strong>
<img
src={this.state.qrCode}
className="d-block mt-1 mx-auto"
alt={I18NextService.i18n.t("totp_qr")}
/>
</div>
</div>
)}
<form id="totp-form">
<label
className="form-label ms-2 mt-4 fw-bold"
htmlFor="totp-input"
>
{I18NextService.i18n.t("enter_totp_code")}
</label>
<div className="d-flex justify-content-between align-items-center p-2">
<input
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={TOTP_LENGTH}
id="totp-input"
className="form-control form-control-lg mx-2 p-1 p-md-2 text-center"
onInput={linkEvent(this, handleInput)}
onPaste={linkEvent(this, handlePaste)}
ref={this.inputRef}
enterKeyHint="done"
value={totp}
/>
</div>
</form>
</div>
<footer className="modal-footer">
<button
type="button"
className="btn btn-danger"
onClick={onClose}
>
{I18NextService.i18n.t("cancel")}
</button>
</footer>
</div>
</div>
</div>
);
}
clearTotp() {
this.setState({ totp: "" });
}
async handleShow() {
this.inputRef.current?.focus();
if (this.props.type === "generate") {
const { getSVG } = await import("@shortcm/qr-image/lib/svg");
this.setState({
qrCode: URL.createObjectURL(
new Blob([(await getSVG(this.props.secretUrl!)).buffer], {
type: "image/svg+xml",
}),
),
});
}
}
}

View file

@ -45,7 +45,7 @@ export class UserBadges extends Component<UserBadgesProps> {
<span <span
className={classNames( className={classNames(
"row d-inline-flex gx-1", "row d-inline-flex gx-1",
this.props.classNames this.props.classNames,
)} )}
> >
{this.props.isBanned && ( {this.props.isBanned && (

View file

@ -1,4 +1,4 @@
import { myAuthRequired, newVote, showScores } from "@utils/app"; import { newVote, showScores } from "@utils/app";
import { numToSI } from "@utils/helpers"; import { numToSI } from "@utils/helpers";
import classNames from "classnames"; import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
@ -53,7 +53,6 @@ const handleUpvote = (i: VoteButtons) => {
i.props.onVote({ i.props.onVote({
comment_id: i.props.id, comment_id: i.props.id,
score: newVote(VoteType.Upvote, i.props.my_vote), score: newVote(VoteType.Upvote, i.props.my_vote),
auth: myAuthRequired(),
}); });
break; break;
case VoteContentType.Post: case VoteContentType.Post:
@ -61,7 +60,6 @@ const handleUpvote = (i: VoteButtons) => {
i.props.onVote({ i.props.onVote({
post_id: i.props.id, post_id: i.props.id,
score: newVote(VoteType.Upvote, i.props.my_vote), score: newVote(VoteType.Upvote, i.props.my_vote),
auth: myAuthRequired(),
}); });
} }
}; };
@ -73,7 +71,6 @@ const handleDownvote = (i: VoteButtons) => {
i.props.onVote({ i.props.onVote({
comment_id: i.props.id, comment_id: i.props.id,
score: newVote(VoteType.Downvote, i.props.my_vote), score: newVote(VoteType.Downvote, i.props.my_vote),
auth: myAuthRequired(),
}); });
break; break;
case VoteContentType.Post: case VoteContentType.Post:
@ -81,7 +78,6 @@ const handleDownvote = (i: VoteButtons) => {
i.props.onVote({ i.props.onVote({
post_id: i.props.id, post_id: i.props.id,
score: newVote(VoteType.Downvote, i.props.my_vote), score: newVote(VoteType.Downvote, i.props.my_vote),
auth: myAuthRequired(),
}); });
} }
}; };
@ -193,7 +189,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
<button <button
type="button" type="button"
className={`btn-animate btn btn-link p-0 ${ className={`btn-animate btn btn-link p-0 ${
this.props.my_vote == 1 ? "text-info" : "text-muted" this.props.my_vote === 1 ? "text-info" : "text-muted"
}`} }`}
onClick={linkEvent(this, handleUpvote)} onClick={linkEvent(this, handleUpvote)}
data-tippy-content={I18NextService.i18n.t("upvote")} data-tippy-content={I18NextService.i18n.t("upvote")}
@ -220,7 +216,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
<button <button
type="button" type="button"
className={`btn-animate btn btn-link p-0 ${ className={`btn-animate btn btn-link p-0 ${
this.props.my_vote == -1 ? "text-danger" : "text-muted" this.props.my_vote === -1 ? "text-danger" : "text-muted"
}`} }`}
onClick={linkEvent(this, handleDownvote)} onClick={linkEvent(this, handleDownvote)}
data-tippy-content={I18NextService.i18n.t("downvote")} data-tippy-content={I18NextService.i18n.t("downvote")}

View file

@ -1,10 +1,4 @@
import { import { editCommunity, setIsoData, showLocal } from "@utils/app";
editCommunity,
myAuth,
myAuthRequired,
setIsoData,
showLocal,
} from "@utils/app";
import { import {
getPageFromString, getPageFromString,
getQueryParams, getQueryParams,
@ -20,17 +14,25 @@ import {
ListCommunities, ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
SortType,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services"; import { FirstLoadService, I18NextService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { ListingTypeSelect } from "../common/listing-type-select"; import { ListingTypeSelect } from "../common/listing-type-select";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "./community-link"; import { CommunityLink } from "./community-link";
const communityLimit = 50; import { communityLimit } from "../../config";
import { SubscribeButton } from "../common/subscribe-button";
type CommunitiesData = RouteDataResponse<{ type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse; listCommunitiesResponse: ListCommunitiesResponse;
@ -45,6 +47,7 @@ interface CommunitiesState {
interface CommunitiesProps { interface CommunitiesProps {
listingType: ListingType; listingType: ListingType;
sort: SortType;
page: number; page: number;
} }
@ -52,10 +55,21 @@ function getListingTypeFromQuery(listingType?: string): ListingType {
return listingType ? (listingType as ListingType) : "Local"; return listingType ? (listingType as ListingType) : "Local";
} }
function getSortTypeFromQuery(type?: string): SortType {
return type ? (type as SortType) : "TopMonth";
}
function getCommunitiesQueryParams() {
return getQueryParams<CommunitiesProps>({
listingType: getListingTypeFromQuery,
sort: getSortTypeFromQuery,
page: getPageFromString,
});
}
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private isoData = setIsoData<CommunitiesData>(this.context); private isoData = setIsoData<CommunitiesData>(this.context);
state: CommunitiesState = { state: CommunitiesState = {
listCommunitiesResponse: { state: "empty" }, listCommunitiesResponse: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
searchText: "", searchText: "",
isIsomorphic: false, isIsomorphic: false,
@ -64,6 +78,7 @@ export class Communities extends Component<any, CommunitiesState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
@ -99,13 +114,13 @@ export class Communities extends Component<any, CommunitiesState> {
</h5> </h5>
); );
case "success": { case "success": {
const { listingType, page } = this.getCommunitiesQueryParams(); const { listingType, sort, page } = getCommunitiesQueryParams();
return ( return (
<div> <div>
<h1 className="h4 mb-4"> <h1 className="h4 mb-4">
{I18NextService.i18n.t("list_of_communities")} {I18NextService.i18n.t("list_of_communities")}
</h1> </h1>
<div className="row g-2 justify-content-between"> <div className="row g-3 align-items-center mb-2">
<div className="col-auto"> <div className="col-auto">
<ListingTypeSelect <ListingTypeSelect
type_={listingType} type_={listingType}
@ -114,6 +129,9 @@ export class Communities extends Component<any, CommunitiesState> {
onChange={this.handleListingTypeChange} onChange={this.handleListingTypeChange}
/> />
</div> </div>
<div className="col-auto me-auto">
<SortSelect sort={sort} onChange={this.handleSortChange} />
</div>
<div className="col-auto">{this.searchForm()}</div> <div className="col-auto">{this.searchForm()}</div>
</div> </div>
@ -161,49 +179,41 @@ export class Communities extends Component<any, CommunitiesState> {
{numToSI(cv.counts.comments)} {numToSI(cv.counts.comments)}
</td> </td>
<td className="text-right"> <td className="text-right">
{cv.subscribed == "Subscribed" && ( <SubscribeButton
<button communityView={cv}
className="btn btn-link d-inline-block" onFollow={linkEvent(
onClick={linkEvent( {
{ i: this,
i: this, communityId: cv.community.id,
communityId: cv.community.id, follow: false,
follow: false, },
}, this.handleFollow,
this.handleFollow )}
)} onUnFollow={linkEvent(
> {
{I18NextService.i18n.t("unsubscribe")} i: this,
</button> communityId: cv.community.id,
)} follow: true,
{cv.subscribed === "NotSubscribed" && ( },
<button this.handleFollow,
className="btn btn-link d-inline-block" )}
onClick={linkEvent( isLink
{ />
i: this,
communityId: cv.community.id,
follow: true,
},
this.handleFollow
)}
>
{I18NextService.i18n.t("subscribe")}
</button>
)}
{cv.subscribed === "Pending" && (
<div className="text-warning d-inline-block">
{I18NextService.i18n.t("subscribe_pending")}
</div>
)}
</td> </td>
</tr> </tr>
) ),
)} )}
</tbody> </tbody>
</table> </table>
</div> </div>
<Paginator page={page} onChange={this.handlePageChange} /> <Paginator
page={page}
onChange={this.handlePageChange}
nextDisabled={
communityLimit >
this.state.listCommunitiesResponse.data.communities.length
}
/>
</div> </div>
); );
} }
@ -224,10 +234,7 @@ export class Communities extends Component<any, CommunitiesState> {
searchForm() { searchForm() {
return ( return (
<form <form className="row" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
className="row mb-2"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
<div className="col-auto"> <div className="col-auto">
<input <input
type="text" type="text"
@ -252,12 +259,16 @@ export class Communities extends Component<any, CommunitiesState> {
); );
} }
async updateUrl({ listingType, page }: Partial<CommunitiesProps>) { async updateUrl({ listingType, sort, page }: Partial<CommunitiesProps>) {
const { listingType: urlListingType, page: urlPage } = const {
this.getCommunitiesQueryParams(); listingType: urlListingType,
sort: urlSort,
page: urlPage,
} = getCommunitiesQueryParams();
const queryParams: QueryParams<CommunitiesProps> = { const queryParams: QueryParams<CommunitiesProps> = {
listingType: listingType ?? urlListingType, listingType: listingType ?? urlListingType,
sort: sort ?? urlSort,
page: (page ?? urlPage)?.toString(), page: (page ?? urlPage)?.toString(),
}; };
@ -270,6 +281,10 @@ export class Communities extends Component<any, CommunitiesState> {
this.updateUrl({ page }); this.updateUrl({ page });
} }
handleSortChange(val: SortType) {
this.updateUrl({ sort: val, page: 1 });
}
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.updateUrl({ this.updateUrl({
listingType: val, listingType: val,
@ -284,40 +299,32 @@ 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);
const { listingType } = getCommunitiesQueryParams();
i.context.router.history.push( i.context.router.history.push(
`/search?q=${searchParamEncoded}&type=Communities` `/search?q=${searchParamEncoded}&type=Communities&listingType=${listingType}`,
); );
} }
static async fetchInitialData({ static async fetchInitialData({
query: { listingType, page }, query: { listingType, sort, page },
client, client,
auth,
}: InitialFetchRequest< }: InitialFetchRequest<
QueryParams<CommunitiesProps> QueryParams<CommunitiesProps>
>): Promise<CommunitiesData> { >): Promise<CommunitiesData> {
const listCommunitiesForm: ListCommunities = { const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType), type_: getListingTypeFromQuery(listingType),
sort: "TopMonth", sort: getSortTypeFromQuery(sort),
limit: communityLimit, limit: communityLimit,
page: getPageFromString(page), page: getPageFromString(page),
auth: auth,
}; };
return { return {
listCommunitiesResponse: await client.listCommunities( listCommunitiesResponse: await client.listCommunities(
listCommunitiesForm listCommunitiesForm,
), ),
}; };
} }
getCommunitiesQueryParams() {
return getQueryParams<CommunitiesProps>({
listingType: getListingTypeFromQuery,
page: getPageFromString,
});
}
async handleFollow(data: { async handleFollow(data: {
i: Communities; i: Communities;
communityId: number; communityId: number;
@ -326,23 +333,21 @@ export class Communities extends Component<any, CommunitiesState> {
const res = await HttpService.client.followCommunity({ const res = await HttpService.client.followCommunity({
community_id: data.communityId, community_id: data.communityId,
follow: data.follow, follow: data.follow,
auth: myAuthRequired(),
}); });
data.i.findAndUpdateCommunity(res); data.i.findAndUpdateCommunity(res);
} }
async refetch() { async refetch() {
this.setState({ listCommunitiesResponse: { state: "loading" } }); this.setState({ listCommunitiesResponse: LOADING_REQUEST });
const { listingType, page } = this.getCommunitiesQueryParams(); const { listingType, sort, page } = getCommunitiesQueryParams();
this.setState({ this.setState({
listCommunitiesResponse: await HttpService.client.listCommunities({ listCommunitiesResponse: await HttpService.client.listCommunities({
type_: listingType, type_: listingType,
sort: "TopMonth", sort: sort,
limit: communityLimit, limit: communityLimit,
page, page,
auth: myAuth(),
}), }),
}); });
@ -352,12 +357,12 @@ export class Communities extends Component<any, CommunitiesState> {
findAndUpdateCommunity(res: RequestState<CommunityResponse>) { findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
this.setState(s => { this.setState(s => {
if ( if (
s.listCommunitiesResponse.state == "success" && s.listCommunitiesResponse.state === "success" &&
res.state == "success" res.state === "success"
) { ) {
s.listCommunitiesResponse.data.communities = editCommunity( s.listCommunitiesResponse.data.communities = editCommunity(
res.data.community_view, res.data.community_view,
s.listCommunitiesResponse.data.communities s.listCommunitiesResponse.data.communities,
); );
} }
return s; return s;

View file

@ -1,6 +1,6 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter, randomStr } from "@utils/helpers"; import { capitalizeFirstLetter, randomStr } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import { import {
CommunityView, CommunityView,
CreateCommunity, CreateCommunity,
@ -12,7 +12,6 @@ import { Icon, Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form"; import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select"; import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt";
interface CommunityFormProps { interface CommunityFormProps {
community_view?: CommunityView; // If a community is given, that means this is an edit community_view?: CommunityView; // If a community is given, that means this is an edit
@ -90,7 +89,8 @@ export class CommunityForm extends Component<
className="community-form" className="community-form"
onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)} onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}
> >
<NavigationPrompt <Prompt
message={I18NextService.i18n.t("block_leaving")}
when={ when={
!this.props.loading && !this.props.loading &&
!!( !!(
@ -230,7 +230,7 @@ export class CommunityForm extends Component<
checked={this.state.form.posting_restricted_to_mods} checked={this.state.form.posting_restricted_to_mods}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleCommunityPostingRestrictedToMods this.handleCommunityPostingRestrictedToMods,
)} )}
/> />
</div> </div>
@ -278,7 +278,6 @@ export class CommunityForm extends Component<
event.preventDefault(); event.preventDefault();
i.setState({ submitted: true }); i.setState({ submitted: true });
const cForm = i.state.form; const cForm = i.state.form;
const auth = myAuthRequired();
const cv = i.props.community_view; const cv = i.props.community_view;
@ -292,7 +291,6 @@ export class CommunityForm extends Component<
nsfw: cForm.nsfw, nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods, posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages, discussion_languages: cForm.discussion_languages,
auth,
}); });
} else { } else {
if (cForm.title && cForm.name) { if (cForm.title && cForm.name) {
@ -305,7 +303,6 @@ export class CommunityForm extends Component<
nsfw: cForm.nsfw, nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods, posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages, discussion_languages: cForm.discussion_languages,
auth,
}); });
} }
} }
@ -329,7 +326,7 @@ export class CommunityForm extends Component<
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) { handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
i.setState( i.setState(
s => ((s.form.posting_restricted_to_mods = event.target.checked), s) s => ((s.form.posting_restricted_to_mods = event.target.checked), s),
); );
} }

View file

@ -21,20 +21,19 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
render() { render() {
const community = this.props.community; const community = this.props.community;
let name_: string, title: string, link: string; let title: string, link: string;
const local = community.local == null ? true : community.local; const local = community.local === null ? true : community.local;
const domain = hostname(community.actor_id);
if (local) { if (local) {
name_ = community.name;
title = community.title; title = community.title;
link = `/c/${community.name}`; link = `/c/${community.name}`;
} else { } else {
const domain = hostname(community.actor_id); const name_ = `${community.name}@${domain}`;
name_ = `${community.name}@${domain}`;
title = `${community.title}@${domain}`; title = `${community.title}@${domain}`;
link = !this.props.realLink ? `/c/${name_}` : community.actor_id; link = !this.props.realLink ? `/c/${name_}` : community.actor_id;
} }
const apubName = `!${name_}`; const apubName = `!${community.name}@${domain}`;
const displayName = this.props.useApubName ? apubName : title; const displayName = this.props.useApubName ? apubName : title;
return !this.props.realLink ? ( return !this.props.realLink ? (
<Link <Link
@ -58,12 +57,14 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
avatarAndName(displayName: string) { avatarAndName(displayName: string) {
const icon = this.props.community.icon; const icon = this.props.community.icon;
const nsfw = this.props.community.nsfw;
return ( return (
<> <>
{!this.props.hideAvatar && {!this.props.hideAvatar &&
!this.props.community.removed && !this.props.community.removed &&
showAvatars() && showAvatars() &&
icon && <PictrsImage src={icon} icon />} icon && <PictrsImage src={icon} icon nsfw={nsfw} />}
<span className="overflow-wrap-anywhere">{displayName}</span> <span className="overflow-wrap-anywhere">{displayName}</span>
</> </>
); );

View file

@ -8,18 +8,13 @@ import {
enableNsfw, enableNsfw,
getCommentParentId, getCommentParentId,
getDataTypeString, getDataTypeString,
myAuth,
postToCommentSortType, postToCommentSortType,
setIsoData, setIsoData,
showLocal, showLocal,
updateCommunityBlock, updateCommunityBlock,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
import { import { getQueryParams, getQueryString } from "@utils/helpers";
getPageFromString,
getQueryParams,
getQueryString,
} from "@utils/helpers";
import type { QueryParams } from "@utils/types"; import type { QueryParams } from "@utils/types";
import { RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import { Component, RefObject, createRef, linkEvent } from "inferno"; import { Component, RefObject, createRef, linkEvent } from "inferno";
@ -62,6 +57,8 @@ import {
LockPost, LockPost,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
MarkPostAsRead,
PaginationCursor,
PostResponse, PostResponse,
PurgeComment, PurgeComment,
PurgeCommunity, PurgeCommunity,
@ -83,7 +80,12 @@ import {
InitialFetchRequest, InitialFetchRequest,
} from "../../interfaces"; } from "../../interfaces";
import { FirstLoadService, I18NextService, UserService } from "../../services"; import { FirstLoadService, I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
@ -91,12 +93,12 @@ import { BannerIconHeader } from "../common/banner-icon-header";
import { DataTypeSelect } from "../common/data-type-select"; import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select"; import { SortSelect } from "../common/sort-select";
import { Sidebar } from "../community/sidebar"; import { Sidebar } from "../community/sidebar";
import { SiteSidebar } from "../home/site-sidebar"; import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings"; import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link"; import { CommunityLink } from "./community-link";
import { PaginatorCursor } from "../common/paginator-cursor";
type CommunityData = RouteDataResponse<{ type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse; communityRes: GetCommunityResponse;
@ -117,13 +119,13 @@ interface State {
interface CommunityProps { interface CommunityProps {
dataType: DataType; dataType: DataType;
sort: SortType; sort: SortType;
page: number; pageCursor?: PaginationCursor;
} }
function getCommunityQueryParams() { function getCommunityQueryParams() {
return getQueryParams<CommunityProps>({ return getQueryParams<CommunityProps>({
dataType: getDataTypeFromQuery, dataType: getDataTypeFromQuery,
page: getPageFromString, pageCursor: cursor => cursor,
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
}); });
} }
@ -146,9 +148,9 @@ export class Community extends Component<
> { > {
private isoData = setIsoData<CommunityData>(this.context); private isoData = setIsoData<CommunityData>(this.context);
state: State = { state: State = {
communityRes: { state: "empty" }, communityRes: EMPTY_REQUEST,
postsRes: { state: "empty" }, postsRes: EMPTY_REQUEST,
commentsRes: { state: "empty" }, commentsRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showSidebarMobile: false, showSidebarMobile: false,
finished: new Map(), finished: new Map(),
@ -160,7 +162,8 @@ export class Community extends Component<
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageNext = this.handlePageNext.bind(this);
this.handlePagePrev = this.handlePagePrev.bind(this);
// All of the action binds // All of the action binds
this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this); this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this);
@ -195,6 +198,7 @@ export class Community extends Component<
this.handleSavePost = this.handleSavePost.bind(this); this.handleSavePost = this.handleSavePost.bind(this);
this.handlePurgePost = this.handlePurgePost.bind(this); this.handlePurgePost = this.handlePurgePost.bind(this);
this.handleFeaturePost = this.handleFeaturePost.bind(this); this.handleFeaturePost = this.handleFeaturePost.bind(this);
this.handleMarkPostAsRead = this.handleMarkPostAsRead.bind(this);
this.mainContentRef = createRef(); this.mainContentRef = createRef();
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
@ -211,11 +215,10 @@ export class Community extends Component<
} }
async fetchCommunity() { async fetchCommunity() {
this.setState({ communityRes: { state: "loading" } }); this.setState({ communityRes: LOADING_REQUEST });
this.setState({ this.setState({
communityRes: await HttpService.client.getCommunity({ communityRes: await HttpService.client.getCommunity({
name: this.props.match.params.name, name: this.props.match.params.name,
auth: myAuth(),
}), }),
}); });
} }
@ -231,8 +234,7 @@ export class Community extends Component<
static async fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort }, query: { dataType: urlDataType, pageCursor, sort: urlSort },
auth,
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise< }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
Promise<CommunityData> Promise<CommunityData>
> { > {
@ -241,41 +243,33 @@ export class Community extends Component<
const communityName = pathSplit[2]; const communityName = pathSplit[2];
const communityForm: GetCommunity = { const communityForm: GetCommunity = {
name: communityName, name: communityName,
auth,
}; };
const dataType = getDataTypeFromQuery(urlDataType); const dataType = getDataTypeFromQuery(urlDataType);
const sort = getSortTypeFromQuery(urlSort); const sort = getSortTypeFromQuery(urlSort);
const page = getPageFromString(urlPage); let postsResponse: RequestState<GetPostsResponse> = EMPTY_REQUEST;
let commentsResponse: RequestState<GetCommentsResponse> = EMPTY_REQUEST;
let postsResponse: RequestState<GetPostsResponse> = { state: "empty" };
let commentsResponse: RequestState<GetCommentsResponse> = {
state: "empty",
};
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
const getPostsForm: GetPosts = { const getPostsForm: GetPosts = {
community_name: communityName, community_name: communityName,
page, page_cursor: pageCursor,
limit: fetchLimit, limit: fetchLimit,
sort, sort,
type_: "All", type_: "All",
saved_only: false, saved_only: false,
auth,
}; };
postsResponse = await client.getPosts(getPostsForm); postsResponse = await client.getPosts(getPostsForm);
} else { } else {
const getCommentsForm: GetComments = { const getCommentsForm: GetComments = {
community_name: communityName, community_name: communityName,
page,
limit: fetchLimit, limit: fetchLimit,
sort: postToCommentSortType(sort), sort: postToCommentSortType(sort),
type_: "All", type_: "All",
saved_only: false, saved_only: false,
auth,
}; };
commentsResponse = await client.getComments(getCommentsForm); commentsResponse = await client.getComments(getCommentsForm);
@ -288,14 +282,21 @@ export class Community extends Component<
}; };
} }
get getNextPage(): PaginationCursor | undefined {
return this.state.postsRes.state === "success"
? this.state.postsRes.data.next_page
: undefined;
}
get documentTitle(): string { get documentTitle(): string {
const cRes = this.state.communityRes; const cRes = this.state.communityRes;
return cRes.state == "success" return cRes.state === "success"
? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}` ? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
: ""; : "";
} }
renderCommunity() { renderCommunity() {
const { pageCursor } = getCommunityQueryParams();
switch (this.state.communityRes.state) { switch (this.state.communityRes.state) {
case "loading": case "loading":
return ( return (
@ -305,13 +306,13 @@ export class Community extends Component<
); );
case "success": { case "success": {
const res = this.state.communityRes.data; const res = this.state.communityRes.data;
const { page } = getCommunityQueryParams();
return ( return (
<> <>
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
canonicalPath={res.community_view.community.actor_id}
description={res.community_view.community.description} description={res.community_view.community.description}
image={res.community_view.community.icon} image={res.community_view.community.icon}
/> />
@ -341,7 +342,12 @@ export class Community extends Component<
</div> </div>
{this.selects(res)} {this.selects(res)}
{this.listings(res)} {this.listings(res)}
<Paginator page={page} onChange={this.handlePageChange} /> <PaginatorCursor
prevPage={pageCursor}
nextPage={this.getNextPage}
onNext={this.handlePageNext}
onPrev={this.handlePagePrev}
/>
</main> </main>
<aside className="d-none d-md-block col-md-4 col-lg-3"> <aside className="d-none d-md-block col-md-4 col-lg-3">
{this.sidebar(res)} {this.sidebar(res)}
@ -430,6 +436,7 @@ export class Community extends Component<
onAddAdmin={this.handleAddAdmin} onAddAdmin={this.handleAddAdmin}
onTransferCommunity={this.handleTransferCommunity} onTransferCommunity={this.handleTransferCommunity}
onFeaturePost={this.handleFeaturePost} onFeaturePost={this.handleFeaturePost}
onMarkPostAsRead={this.handleMarkPostAsRead}
/> />
); );
} }
@ -447,7 +454,7 @@ export class Community extends Component<
nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)} nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished} finished={this.state.finished}
noIndent isTopLevel
showContext showContext
enableDownvotes={enableDownvotes(site_res)} enableDownvotes={enableDownvotes(site_res)}
moderators={communityRes.moderators} moderators={communityRes.moderators}
@ -534,18 +541,22 @@ export class Community extends Component<
); );
} }
handlePageChange(page: number) { handlePagePrev() {
this.updateUrl({ page }); this.props.history.back();
}
handlePageNext(nextPage: PaginationCursor) {
this.updateUrl({ pageCursor: nextPage });
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleSortChange(sort: SortType) { handleSortChange(sort: SortType) {
this.updateUrl({ sort, page: 1 }); this.updateUrl({ sort, pageCursor: undefined });
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleDataTypeChange(dataType: DataType) { handleDataTypeChange(dataType: DataType) {
this.updateUrl({ dataType, page: 1 }); this.updateUrl({ dataType, pageCursor: undefined });
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@ -555,54 +566,47 @@ export class Community extends Component<
})); }));
} }
async updateUrl({ dataType, page, sort }: Partial<CommunityProps>) { async updateUrl({ dataType, pageCursor, sort }: Partial<CommunityProps>) {
const { const { dataType: urlDataType, sort: urlSort } = getCommunityQueryParams();
dataType: urlDataType,
page: urlPage,
sort: urlSort,
} = getCommunityQueryParams();
const queryParams: QueryParams<CommunityProps> = { const queryParams: QueryParams<CommunityProps> = {
dataType: getDataTypeString(dataType ?? urlDataType), dataType: getDataTypeString(dataType ?? urlDataType),
page: (page ?? urlPage).toString(), pageCursor: pageCursor,
sort: sort ?? urlSort, sort: sort ?? urlSort,
}; };
this.props.history.push( this.props.history.push(
`/c/${this.props.match.params.name}${getQueryString(queryParams)}` `/c/${this.props.match.params.name}${getQueryString(queryParams)}`,
); );
await this.fetchData(); await this.fetchData();
} }
async fetchData() { async fetchData() {
const { dataType, page, sort } = getCommunityQueryParams(); const { dataType, pageCursor, sort } = getCommunityQueryParams();
const { name } = this.props.match.params; const { name } = this.props.match.params;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
this.setState({ postsRes: { state: "loading" } }); this.setState({ postsRes: LOADING_REQUEST });
this.setState({ this.setState({
postsRes: await HttpService.client.getPosts({ postsRes: await HttpService.client.getPosts({
page, page_cursor: pageCursor,
limit: fetchLimit, limit: fetchLimit,
sort, sort,
type_: "All", type_: "All",
community_name: name, community_name: name,
saved_only: false, saved_only: false,
auth: myAuth(),
}), }),
}); });
} else { } else {
this.setState({ commentsRes: { state: "loading" } }); this.setState({ commentsRes: LOADING_REQUEST });
this.setState({ this.setState({
commentsRes: await HttpService.client.getComments({ commentsRes: await HttpService.client.getComments({
page,
limit: fetchLimit, limit: fetchLimit,
sort: postToCommentSortType(sort), sort: postToCommentSortType(sort),
type_: "All", type_: "All",
community_name: name, community_name: name,
saved_only: false, saved_only: false,
auth: myAuth(),
}), }),
}); });
} }
@ -625,11 +629,11 @@ export class Community extends Component<
this.updateCommunity(followCommunityRes); this.updateCommunity(followCommunityRes);
// Update myUserInfo // Update myUserInfo
if (followCommunityRes.state == "success") { if (followCommunityRes.state === "success") {
const communityId = followCommunityRes.data.community_view.community.id; const communityId = followCommunityRes.data.community_view.community.id;
const mui = UserService.Instance.myUserInfo; const mui = UserService.Instance.myUserInfo;
if (mui) { if (mui) {
mui.follows = mui.follows.filter(i => i.community.id != communityId); mui.follows = mui.follows.filter(i => i.community.id !== communityId);
} }
} }
} }
@ -656,10 +660,10 @@ export class Community extends Component<
async handleBlockCommunity(form: BlockCommunity) { async handleBlockCommunity(form: BlockCommunity) {
const blockCommunityRes = await HttpService.client.blockCommunity(form); const blockCommunityRes = await HttpService.client.blockCommunity(form);
if (blockCommunityRes.state == "success") { if (blockCommunityRes.state === "success") {
updateCommunityBlock(blockCommunityRes.data); updateCommunityBlock(blockCommunityRes.data);
this.setState(s => { this.setState(s => {
if (s.communityRes.state == "success") { if (s.communityRes.state === "success") {
s.communityRes.data.community_view.blocked = s.communityRes.data.community_view.blocked =
blockCommunityRes.data.blocked; blockCommunityRes.data.blocked;
} }
@ -669,7 +673,7 @@ export class Community extends Component<
async handleBlockPerson(form: BlockPerson) { async handleBlockPerson(form: BlockPerson) {
const blockPersonRes = await HttpService.client.blockPerson(form); const blockPersonRes = await HttpService.client.blockPerson(form);
if (blockPersonRes.state == "success") { if (blockPersonRes.state === "success") {
updatePersonBlock(blockPersonRes.data); updatePersonBlock(blockPersonRes.data);
} }
} }
@ -695,7 +699,7 @@ export class Community extends Component<
async handleEditComment(form: EditComment) { async handleEditComment(form: EditComment) {
const editCommentRes = await HttpService.client.editComment(form); const editCommentRes = await HttpService.client.editComment(form);
this.findAndUpdateComment(editCommentRes); this.findAndUpdateCommentEdit(editCommentRes);
return editCommentRes; return editCommentRes;
} }
@ -752,14 +756,14 @@ export class Community extends Component<
async handleCommentReport(form: CreateCommentReport) { async handleCommentReport(form: CreateCommentReport) {
const reportRes = await HttpService.client.createCommentReport(form); const reportRes = await HttpService.client.createCommentReport(form);
if (reportRes.state == "success") { if (reportRes.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} }
} }
async handlePostReport(form: CreatePostReport) { async handlePostReport(form: CreatePostReport) {
const reportRes = await HttpService.client.createPostReport(form); const reportRes = await HttpService.client.createPostReport(form);
if (reportRes.state == "success") { if (reportRes.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} }
} }
@ -777,14 +781,14 @@ export class Community extends Component<
async handleAddAdmin(form: AddAdmin) { async handleAddAdmin(form: AddAdmin) {
const addAdminRes = await HttpService.client.addAdmin(form); const addAdminRes = await HttpService.client.addAdmin(form);
if (addAdminRes.state == "success") { if (addAdminRes.state === "success") {
this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s)); this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
} }
} }
async handleTransferCommunity(form: TransferCommunity) { async handleTransferCommunity(form: TransferCommunity) {
const transferCommunityRes = await HttpService.client.transferCommunity( const transferCommunityRes = await HttpService.client.transferCommunity(
form form,
); );
toast(I18NextService.i18n.t("transfer_community")); toast(I18NextService.i18n.t("transfer_community"));
this.updateCommunityFull(transferCommunityRes); this.updateCommunityFull(transferCommunityRes);
@ -800,6 +804,11 @@ export class Community extends Component<
await HttpService.client.markPersonMentionAsRead(form); await HttpService.client.markPersonMentionAsRead(form);
} }
async handleMarkPostAsRead(form: MarkPostAsRead) {
const res = await HttpService.client.markPostAsRead(form);
this.findAndUpdatePost(res);
}
async handleBanFromCommunity(form: BanFromCommunity) { async handleBanFromCommunity(form: BanFromCommunity) {
const banRes = await HttpService.client.banFromCommunity(form); const banRes = await HttpService.client.banFromCommunity(form);
this.updateBanFromCommunity(banRes); this.updateBanFromCommunity(banRes);
@ -812,20 +821,20 @@ export class Community extends Component<
updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) { updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success") { if (s.postsRes.state === "success") {
s.postsRes.data.posts s.postsRes.data.posts
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
if (s.commentsRes.state == "success") { if (s.commentsRes.state === "success") {
s.commentsRes.data.comments s.commentsRes.data.comments
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
return s; return s;
@ -835,16 +844,16 @@ export class Community extends Component<
updateBan(banRes: RequestState<BanPersonResponse>) { updateBan(banRes: RequestState<BanPersonResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success") { if (s.postsRes.state === "success") {
s.postsRes.data.posts s.postsRes.data.posts
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
if (s.commentsRes.state == "success") { if (s.commentsRes.state === "success") {
s.commentsRes.data.comments s.commentsRes.data.comments
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
return s; return s;
@ -854,7 +863,7 @@ export class Community extends Component<
updateCommunity(res: RequestState<CommunityResponse>) { updateCommunity(res: RequestState<CommunityResponse>) {
this.setState(s => { this.setState(s => {
if (s.communityRes.state == "success" && res.state == "success") { if (s.communityRes.state === "success" && res.state === "success") {
s.communityRes.data.community_view = res.data.community_view; s.communityRes.data.community_view = res.data.community_view;
s.communityRes.data.discussion_languages = s.communityRes.data.discussion_languages =
res.data.discussion_languages; res.data.discussion_languages;
@ -865,7 +874,7 @@ export class Community extends Component<
updateCommunityFull(res: RequestState<GetCommunityResponse>) { updateCommunityFull(res: RequestState<GetCommunityResponse>) {
this.setState(s => { this.setState(s => {
if (s.communityRes.state == "success" && res.state == "success") { if (s.communityRes.state === "success" && res.state === "success") {
s.communityRes.data.community_view = res.data.community_view; s.communityRes.data.community_view = res.data.community_view;
s.communityRes.data.moderators = res.data.moderators; s.communityRes.data.moderators = res.data.moderators;
} }
@ -874,18 +883,18 @@ export class Community extends Component<
} }
purgeItem(purgeRes: RequestState<PurgeItemResponse>) { purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
if (purgeRes.state == "success") { if (purgeRes.state === "success") {
toast(I18NextService.i18n.t("purge_success")); toast(I18NextService.i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} }
findAndUpdateComment(res: RequestState<CommentResponse>) { findAndUpdateCommentEdit(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editComment( s.commentsRes.data.comments = editComment(
res.data.comment_view, res.data.comment_view,
s.commentsRes.data.comments s.commentsRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true); s.finished.set(res.data.comment_view.comment.id, true);
} }
@ -893,15 +902,27 @@ export class Community extends Component<
}); });
} }
findAndUpdateComment(res: RequestState<CommentResponse>) {
this.setState(s => {
if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editComment(
res.data.comment_view,
s.commentsRes.data.comments,
);
}
return s;
});
}
createAndUpdateComments(res: RequestState<CommentResponse>) { createAndUpdateComments(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments.unshift(res.data.comment_view); s.commentsRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent // Set finished for the parent
s.finished.set( s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0, getCommentParentId(res.data.comment_view.comment) ?? 0,
true true,
); );
} }
return s; return s;
@ -910,10 +931,10 @@ export class Community extends Component<
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) { findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editWith( s.commentsRes.data.comments = editWith(
res.data.comment_reply_view, res.data.comment_reply_view,
s.commentsRes.data.comments s.commentsRes.data.comments,
); );
} }
return s; return s;
@ -922,10 +943,10 @@ export class Community extends Component<
findAndUpdatePost(res: RequestState<PostResponse>) { findAndUpdatePost(res: RequestState<PostResponse>) {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success" && res.state == "success") { if (s.postsRes.state === "success" && res.state === "success") {
s.postsRes.data.posts = editPost( s.postsRes.data.posts = editPost(
res.data.post_view, res.data.post_view,
s.postsRes.data.posts s.postsRes.data.posts,
); );
} }
return s; return s;
@ -935,7 +956,7 @@ export class Community extends Component<
updateModerators(res: RequestState<AddModToCommunityResponse>) { updateModerators(res: RequestState<AddModToCommunityResponse>) {
// Update the moderators // Update the moderators
this.setState(s => { this.setState(s => {
if (s.communityRes.state == "success" && res.state == "success") { if (s.communityRes.state === "success" && res.state === "success") {
s.communityRes.data.moderators = res.data.moderators; s.communityRes.data.moderators = res.data.moderators;
} }
return s; return s;

View file

@ -1,5 +1,4 @@
import { myAuthRequired } from "@utils/app"; import { hostname } from "@utils/helpers";
import { getUnixTime, hostname } from "@utils/helpers";
import { amAdmin, amMod, amTopMod } from "@utils/roles"; import { amAdmin, amMod, amTopMod } from "@utils/roles";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
@ -22,6 +21,7 @@ import { I18NextService, UserService } from "../../services";
import { Badges } from "../common/badges"; import { Badges } from "../common/badges";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon, PurgeWarning, Spinner } from "../common/icon"; import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { SubscribeButton } from "../common/subscribe-button";
import { CommunityForm } from "../community/community-form"; import { CommunityForm } from "../community/community-form";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -79,15 +79,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & SidebarProps> nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>,
): void { ): void {
if (this.props.moderators != nextProps.moderators) { if (this.props.moderators !== nextProps.moderators) {
this.setState({ this.setState({
showConfirmLeaveModTeam: false, showConfirmLeaveModTeam: false,
}); });
} }
if (this.props.community_view != nextProps.community_view) { if (this.props.community_view !== nextProps.community_view) {
this.setState({ this.setState({
showEdit: false, showEdit: false,
showPurgeDialog: false, showPurgeDialog: false,
@ -123,7 +123,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
sidebar() { sidebar() {
const myUSerInfo = UserService.Instance.myUserInfo; const myUSerInfo = UserService.Instance.myUserInfo;
const { name, actor_id } = this.props.community_view.community; const {
community: { name, actor_id },
} = this.props.community_view;
return ( return (
<aside className="mb-3"> <aside className="mb-3">
<div id="sidebarContainer"> <div id="sidebarContainer">
@ -131,7 +133,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div className="card-body"> <div className="card-body">
{this.communityTitle()} {this.communityTitle()}
{this.props.editable && this.adminButtons()} {this.props.editable && this.adminButtons()}
{myUSerInfo && this.subscribe()} <SubscribeButton
communityView={this.props.community_view}
onFollow={linkEvent(this, this.handleFollowCommunity)}
onUnFollow={linkEvent(this, this.handleUnfollowCommunity)}
loading={this.state.followCommunityLoading}
/>
{this.canPost && this.createPost()} {this.canPost && this.createPost()}
{myUSerInfo && this.blockCommunity()} {myUSerInfo && this.blockCommunity()}
{!myUSerInfo && ( {!myUSerInfo && (
@ -230,58 +237,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
); );
} }
subscribe() {
const community_view = this.props.community_view;
if (community_view.subscribed === "NotSubscribed") {
return (
<button
className="btn btn-secondary d-block mb-2 w-100"
onClick={linkEvent(this, this.handleFollowCommunity)}
>
{this.state.followCommunityLoading ? (
<Spinner />
) : (
I18NextService.i18n.t("subscribe")
)}
</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 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;
@ -292,7 +247,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
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>
) )
@ -332,7 +287,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
className="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmLeaveModTeamClick this.handleShowConfirmLeaveModTeamClick,
)} )}
> >
{I18NextService.i18n.t("leave_mod_team")} {I18NextService.i18n.t("leave_mod_team")}
@ -356,7 +311,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
className="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelLeaveModTeamClick this.handleCancelLeaveModTeamClick,
)} )}
> >
{I18NextService.i18n.t("no")} {I18NextService.i18n.t("no")}
@ -544,7 +499,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.props.onFollowCommunity({ i.props.onFollowCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
follow: false, follow: false,
auth: myAuthRequired(),
}); });
} }
@ -553,7 +507,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.props.onFollowCommunity({ i.props.onFollowCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
follow: true, follow: true,
auth: myAuthRequired(),
}); });
} }
@ -563,7 +516,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.props.onBlockCommunity({ i.props.onBlockCommunity({
community_id: community.id, community_id: community.id,
block: !blocked, block: !blocked,
auth: myAuthRequired(),
}); });
} }
@ -573,9 +525,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.setState({ leaveModTeamLoading: true }); i.setState({ leaveModTeamLoading: true });
i.props.onLeaveModTeam({ i.props.onLeaveModTeam({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
person_id: 92, person_id: myId,
added: false, added: false,
auth: myAuthRequired(),
}); });
} }
} }
@ -585,7 +536,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.props.onDeleteCommunity({ i.props.onDeleteCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
deleted: !i.props.community_view.community.deleted, deleted: !i.props.community_view.community.deleted,
auth: myAuthRequired(),
}); });
} }
@ -596,8 +546,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
removed: !i.props.community_view.community.removed, removed: !i.props.community_view.community.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
expires: getUnixTime(i.state.removeExpires), // TODO fix this
auth: myAuthRequired(),
}); });
} }
@ -607,7 +555,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.props.onPurgeCommunity({ i.props.onPurgeCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
reason: i.state.purgeReason, reason: i.state.purgeReason,
auth: myAuthRequired(),
}); });
} }
} }

View file

@ -1,9 +1,4 @@
import { import { fetchThemeList, setIsoData, showLocal } from "@utils/app";
fetchThemeList,
myAuthRequired,
setIsoData,
showLocal,
} from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import classNames from "classnames"; import classNames from "classnames";
@ -21,7 +16,12 @@ import {
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown"; import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
import { FirstLoadService, I18NextService } from "../../services"; import { FirstLoadService, I18NextService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -55,9 +55,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
banned: [], banned: [],
currentTab: "site", currentTab: "site",
bannedRes: { state: "empty" }, bannedRes: EMPTY_REQUEST,
instancesRes: { state: "empty" }, instancesRes: EMPTY_REQUEST,
leaveAdminTeamRes: { state: "empty" }, leaveAdminTeamRes: EMPTY_REQUEST,
loading: false, loading: false,
themeList: [], themeList: [],
isIsomorphic: false, isIsomorphic: false,
@ -85,16 +85,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
static async fetchInitialData({ static async fetchInitialData({
auth,
client, client,
}: InitialFetchRequest): Promise<AdminSettingsData> { }: InitialFetchRequest): Promise<AdminSettingsData> {
return { return {
bannedRes: await client.getBannedPersons({ bannedRes: await client.getBannedPersons(),
auth: auth as string, instancesRes: await client.getFederatedInstances(),
}),
instancesRes: await client.getFederatedInstances({
auth: auth as string,
}),
}; };
} }
@ -150,15 +145,26 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
loading={this.state.loading} loading={this.state.loading}
/> />
</div> </div>
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">{this.admins()}</div>
{this.admins()}
<hr />
{this.bannedUsers()}
</div>
</div> </div>
</div> </div>
), ),
}, },
{
key: "banned_users",
label: I18NextService.i18n.t("banned_users"),
getNode: isSelected => (
<div
className={classNames("tab-pane", {
active: isSelected,
})}
role="tabpanel"
id="banned_users-tab-pane"
>
{this.bannedUsers()}
</div>
),
},
{ {
key: "rate_limiting", key: "rate_limiting",
label: "Rate Limiting", label: "Rate Limiting",
@ -230,16 +236,14 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
async fetchData() { async fetchData() {
this.setState({ this.setState({
bannedRes: { state: "loading" }, bannedRes: LOADING_REQUEST,
instancesRes: { state: "loading" }, instancesRes: LOADING_REQUEST,
themeList: [], themeList: [],
}); });
const auth = myAuthRequired();
const [bannedRes, instancesRes, themeList] = await Promise.all([ const [bannedRes, instancesRes, themeList] = await Promise.all([
HttpService.client.getBannedPersons({ auth }), HttpService.client.getBannedPersons(),
HttpService.client.getFederatedInstances({ auth }), HttpService.client.getFederatedInstances(),
fetchThemeList(), fetchThemeList(),
]); ]);
@ -274,7 +278,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
onClick={linkEvent(this, this.handleLeaveAdminTeam)} onClick={linkEvent(this, this.handleLeaveAdminTeam)}
className="btn btn-danger mb-2" className="btn btn-danger mb-2"
> >
{this.state.leaveAdminTeamRes.state == "loading" ? ( {this.state.leaveAdminTeamRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
I18NextService.i18n.t("leave_admin_team") I18NextService.i18n.t("leave_admin_team")
@ -295,7 +299,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
const bans = this.state.bannedRes.data.banned; const bans = this.state.bannedRes.data.banned;
return ( return (
<> <>
<h2 className="h5">{I18NextService.i18n.t("banned_users")}</h2> <h1 className="h4 mb-4">{I18NextService.i18n.t("banned_users")}</h1>
<ul className="list-unstyled"> <ul className="list-unstyled">
{bans.map(banned => ( {bans.map(banned => (
<li key={banned.person.id} className="list-inline-item"> <li key={banned.person.id} className="list-inline-item">
@ -334,11 +338,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
async handleLeaveAdminTeam(i: AdminSettings) { async handleLeaveAdminTeam(i: AdminSettings) {
i.setState({ leaveAdminTeamRes: { state: "loading" } }); i.setState({ leaveAdminTeamRes: LOADING_REQUEST });
this.setState({ this.setState({
leaveAdminTeamRes: await HttpService.client.leaveAdmin({ leaveAdminTeamRes: await HttpService.client.leaveAdmin(),
auth: myAuthRequired(),
}),
}); });
if (this.state.leaveAdminTeamRes.state === "success") { if (this.state.leaveAdminTeamRes.state === "success") {

View file

@ -1,4 +1,4 @@
import { myAuthRequired, setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
@ -11,7 +11,6 @@ import { customEmojisLookup } from "../../markdown";
import { HttpService, I18NextService } from "../../services"; import { HttpService, I18NextService } from "../../services";
import { pictrsDeleteToast, toast } from "../../toast"; import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart"; import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
@ -66,17 +65,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleEmojiClick = this.handleEmojiClick.bind(this); this.handleEmojiClick = this.handleEmojiClick.bind(this);
} }
get documentTitle(): string {
return I18NextService.i18n.t("custom_emojis");
}
render() { render() {
return ( return (
<div className="home-emojis-form col-12"> <div className="home-emojis-form col-12">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1> <h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1>
{customEmojisLookup.size > 0 && ( {customEmojisLookup.size > 0 && (
<div> <div>
@ -118,8 +109,8 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
Number((this.state.page - 1) * this.itemsPerPage), Number((this.state.page - 1) * this.itemsPerPage),
Number( Number(
(this.state.page - 1) * this.itemsPerPage + (this.state.page - 1) * this.itemsPerPage +
this.itemsPerPage this.itemsPerPage,
) ),
) )
.map((cv, index) => ( .map((cv, index) => (
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}> <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
@ -139,11 +130,11 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
className="btn btn-sm btn-secondary pointer" className="btn btn-sm btn-secondary pointer"
htmlFor={`file-uploader-${index}`} htmlFor={`file-uploader-${index}`}
data-tippy-content={I18NextService.i18n.t( data-tippy-content={I18NextService.i18n.t(
"upload_image" "upload_image",
)} )}
> >
{capitalizeFirstLetter( {capitalizeFirstLetter(
I18NextService.i18n.t("upload") I18NextService.i18n.t("upload"),
)} )}
<input <input
name={`file-uploader-${index}`} name={`file-uploader-${index}`}
@ -153,7 +144,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
className="d-none" className="d-none"
onChange={linkEvent( onChange={linkEvent(
{ form: this, index: index }, { form: this, index: index },
this.handleImageUpload this.handleImageUpload,
)} )}
/> />
</label> </label>
@ -168,7 +159,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.shortcode} value={cv.shortcode}
onInput={linkEvent( onInput={linkEvent(
{ form: this, index: index }, { form: this, index: index },
this.handleEmojiShortCodeChange this.handleEmojiShortCodeChange,
)} )}
/> />
</td> </td>
@ -180,7 +171,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.category} value={cv.category}
onInput={linkEvent( onInput={linkEvent(
{ form: this, index: index }, { form: this, index: index },
this.handleEmojiCategoryChange this.handleEmojiCategoryChange,
)} )}
/> />
</td> </td>
@ -192,7 +183,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.image_url} value={cv.image_url}
onInput={linkEvent( onInput={linkEvent(
{ form: this, index: index, overrideValue: null }, { form: this, index: index, overrideValue: null },
this.handleEmojiImageUrlChange this.handleEmojiImageUrlChange,
)} )}
/> />
</td> </td>
@ -204,7 +195,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.alt_text} value={cv.alt_text}
onInput={linkEvent( onInput={linkEvent(
{ form: this, index: index }, { form: this, index: index },
this.handleEmojiAltTextChange this.handleEmojiAltTextChange,
)} )}
/> />
</td> </td>
@ -216,7 +207,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.keywords} value={cv.keywords}
onInput={linkEvent( onInput={linkEvent(
{ form: this, index: index }, { form: this, index: index },
this.handleEmojiKeywordChange this.handleEmojiKeywordChange,
)} )}
/> />
</td> </td>
@ -231,7 +222,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
} }
onClick={linkEvent( onClick={linkEvent(
{ i: this, cv: cv }, { i: this, cv: cv },
this.handleEditEmojiClick this.handleEditEmojiClick,
)} )}
data-tippy-content={I18NextService.i18n.t("save")} data-tippy-content={I18NextService.i18n.t("save")}
aria-label={I18NextService.i18n.t("save")} aria-label={I18NextService.i18n.t("save")}
@ -241,7 +232,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
<Spinner /> <Spinner />
) : ( ) : (
capitalizeFirstLetter( capitalizeFirstLetter(
I18NextService.i18n.t("save") I18NextService.i18n.t("save"),
) )
)} )}
</button> </button>
@ -250,7 +241,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
{ i: this, index: index, cv: cv }, { i: this, index: index, cv: cv },
this.handleDeleteEmojiClick this.handleDeleteEmojiClick,
)} )}
data-tippy-content={I18NextService.i18n.t("delete")} data-tippy-content={I18NextService.i18n.t("delete")}
aria-label={I18NextService.i18n.t("delete")} aria-label={I18NextService.i18n.t("delete")}
@ -276,7 +267,11 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
{I18NextService.i18n.t("add_custom_emoji")} {I18NextService.i18n.t("add_custom_emoji")}
</button> </button>
<Paginator page={this.state.page} onChange={this.handlePageChange} /> <Paginator
page={this.state.page}
onChange={this.handlePageChange}
nextDisabled={false}
/>
</div> </div>
</div> </div>
); );
@ -290,8 +285,8 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
cv.shortcode.length > 0; cv.shortcode.length > 0;
const noDuplicateShortCodes = const noDuplicateShortCodes =
this.state.customEmojis.filter( this.state.customEmojis.filter(
x => x.shortcode == cv.shortcode && x.id != cv.id x => x.shortcode === cv.shortcode && x.id !== cv.id,
).length == 0; ).length === 0;
return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed; return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
} }
@ -308,7 +303,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
const view = customEmojisLookup.get(e.id); const view = customEmojisLookup.get(e.id);
if (view) { if (view) {
const page = this.state.customEmojis.find( const page = this.state.customEmojis.find(
x => x.id == view.custom_emoji.id x => x.id === view.custom_emoji.id,
)?.page; )?.page;
if (page) { if (page) {
this.setState({ page: page }); this.setState({ page: page });
@ -319,7 +314,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiCategoryChange( handleEmojiCategoryChange(
props: { form: EmojiForm; index: number }, props: { form: EmojiForm; index: number },
event: any event: any,
) { ) {
const custom_emojis = [...props.form.state.customEmojis]; const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex = const pagedIndex =
@ -335,7 +330,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiShortCodeChange( handleEmojiShortCodeChange(
props: { form: EmojiForm; index: number }, props: { form: EmojiForm; index: number },
event: any event: any,
) { ) {
const custom_emojis = [...props.form.state.customEmojis]; const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex = const pagedIndex =
@ -355,7 +350,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
index, index,
overrideValue, overrideValue,
}: { form: EmojiForm; index: number; overrideValue: string | null }, }: { form: EmojiForm; index: number; overrideValue: string | null },
event: any event: any,
) { ) {
form.setState(prevState => { form.setState(prevState => {
const custom_emojis = [...form.state.customEmojis]; const custom_emojis = [...form.state.customEmojis];
@ -376,7 +371,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
changed: true, changed: true,
loading: false, loading: false,
} }
: ce : ce,
), ),
}; };
}); });
@ -384,7 +379,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiAltTextChange( handleEmojiAltTextChange(
props: { form: EmojiForm; index: number }, props: { form: EmojiForm; index: number },
event: any event: any,
) { ) {
const custom_emojis = [...props.form.state.customEmojis]; const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex = const pagedIndex =
@ -400,7 +395,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiKeywordChange( handleEmojiKeywordChange(
props: { form: EmojiForm; index: number }, props: { form: EmojiForm; index: number },
event: any event: any,
) { ) {
const custom_emojis = [...props.form.state.customEmojis]; const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex = const pagedIndex =
@ -420,10 +415,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
cv: CustomEmojiViewForm; cv: CustomEmojiViewForm;
}) { }) {
const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index; const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
if (d.cv.id != 0) { if (d.cv.id !== 0) {
d.i.props.onDelete({ d.i.props.onDelete({
id: d.cv.id, id: d.cv.id,
auth: myAuthRequired(),
}); });
} else { } else {
const custom_emojis = [...d.i.state.customEmojis]; const custom_emojis = [...d.i.state.customEmojis];
@ -444,7 +438,6 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
image_url: d.cv.image_url, image_url: d.cv.image_url,
alt_text: d.cv.alt_text, alt_text: d.cv.alt_text,
keywords: uniqueKeywords, keywords: uniqueKeywords,
auth: myAuthRequired(),
}); });
} else { } else {
d.i.props.onCreate({ d.i.props.onCreate({
@ -453,7 +446,6 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
image_url: d.cv.image_url, image_url: d.cv.image_url,
alt_text: d.cv.alt_text, alt_text: d.cv.alt_text,
keywords: uniqueKeywords, keywords: uniqueKeywords,
auth: myAuthRequired(),
}); });
} }
} }
@ -485,7 +477,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleImageUpload( handleImageUpload(
{ form, index }: { form: EmojiForm; index: number }, { form, index }: { form: EmojiForm; index: number },
event: any event: any,
) { ) {
let file: any; let file: any;
if (event.target) { if (event.target) {
@ -498,7 +490,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
form.setState(prevState => ({ form.setState(prevState => ({
...prevState, ...prevState,
customEmojis: prevState.customEmojis.map((cv, i) => customEmojis: prevState.customEmojis.map((cv, i) =>
i === index ? { ...cv, loading: true } : cv i === index ? { ...cv, loading: true } : cv,
), ),
})); }));
@ -510,7 +502,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
pictrsDeleteToast(file.name, res.data.delete_url as string); pictrsDeleteToast(file.name, res.data.delete_url as string);
form.handleEmojiImageUrlChange( form.handleEmojiImageUrlChange(
{ 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") { } else if (res.data.msg === "too_large") {
toast(I18NextService.i18n.t("upload_too_large"), "danger"); toast(I18NextService.i18n.t("upload_too_large"), "danger");

View file

@ -14,7 +14,6 @@ import {
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
import { import {
getPageFromString,
getQueryParams, getQueryParams,
getQueryString, getQueryString,
getRandomFromList, getRandomFromList,
@ -59,6 +58,8 @@ import {
LockPost, LockPost,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
MarkPostAsRead,
PaginationCursor,
PostResponse, PostResponse,
PurgeComment, PurgeComment,
PurgeItemResponse, PurgeItemResponse,
@ -84,7 +85,12 @@ import {
I18NextService, I18NextService,
UserService, UserService,
} from "../../services"; } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
@ -92,11 +98,11 @@ import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { ListingTypeSelect } from "../common/listing-type-select"; import { ListingTypeSelect } from "../common/listing-type-select";
import { Paginator } from "../common/paginator";
import { SortSelect } from "../common/sort-select"; import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
import { PostListings } from "../post/post-listings"; import { PostListings } from "../post/post-listings";
import { SiteSidebar } from "./site-sidebar"; import { SiteSidebar } from "./site-sidebar";
import { PaginatorCursor } from "../common/paginator-cursor";
interface HomeState { interface HomeState {
postsRes: RequestState<GetPostsResponse>; postsRes: RequestState<GetPostsResponse>;
@ -117,7 +123,7 @@ interface HomeProps {
listingType?: ListingType; listingType?: ListingType;
dataType: DataType; dataType: DataType;
sort: SortType; sort: SortType;
page: number; pageCursor?: PaginationCursor;
} }
type HomeData = RouteDataResponse<{ type HomeData = RouteDataResponse<{
@ -128,7 +134,6 @@ type HomeData = RouteDataResponse<{
function getRss(listingType: ListingType) { function getRss(listingType: ListingType) {
const { sort } = getHomeQueryParams(); const { sort } = getHomeQueryParams();
const auth = myAuth();
let rss: string | undefined = undefined; let rss: string | undefined = undefined;
@ -142,6 +147,7 @@ function getRss(listingType: ListingType) {
break; break;
} }
case "Subscribed": { case "Subscribed": {
const auth = myAuth();
rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined; rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
break; break;
} }
@ -179,13 +185,14 @@ function getSortTypeFromQuery(type?: string): SortType {
return (type ? (type as SortType) : mySortType) ?? "Active"; return (type ? (type as SortType) : mySortType) ?? "Active";
} }
const getHomeQueryParams = () => function getHomeQueryParams() {
getQueryParams<HomeProps>({ return getQueryParams<HomeProps>({
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
listingType: getListingTypeFromQuery, listingType: getListingTypeFromQuery,
page: getPageFromString, pageCursor: cursor => cursor,
dataType: getDataTypeFromQuery, dataType: getDataTypeFromQuery,
}); });
}
const MobileButton = ({ const MobileButton = ({
textKey, textKey,
@ -220,9 +227,9 @@ const LinkButton = ({
export class Home extends Component<any, HomeState> { export class Home extends Component<any, HomeState> {
private isoData = setIsoData<HomeData>(this.context); private isoData = setIsoData<HomeData>(this.context);
state: HomeState = { state: HomeState = {
postsRes: { state: "empty" }, postsRes: EMPTY_REQUEST,
commentsRes: { state: "empty" }, commentsRes: EMPTY_REQUEST,
trendingCommunitiesRes: { state: "empty" }, trendingCommunitiesRes: EMPTY_REQUEST,
scrolled: true, scrolled: true,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showSubscribedMobile: false, showSubscribedMobile: false,
@ -239,7 +246,8 @@ export class Home extends Component<any, HomeState> {
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageNext = this.handlePageNext.bind(this);
this.handlePagePrev = this.handlePagePrev.bind(this);
this.handleCreateComment = this.handleCreateComment.bind(this); this.handleCreateComment = this.handleCreateComment.bind(this);
this.handleEditComment = this.handleEditComment.bind(this); this.handleEditComment = this.handleEditComment.bind(this);
@ -268,6 +276,7 @@ export class Home extends Component<any, HomeState> {
this.handleSavePost = this.handleSavePost.bind(this); this.handleSavePost = this.handleSavePost.bind(this);
this.handlePurgePost = this.handlePurgePost.bind(this); this.handlePurgePost = this.handlePurgePost.bind(this);
this.handleFeaturePost = this.handleFeaturePost.bind(this); this.handleFeaturePost = this.handleFeaturePost.bind(this);
this.handleMarkPostAsRead = this.handleMarkPostAsRead.bind(this);
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
@ -285,9 +294,8 @@ export class Home extends Component<any, HomeState> {
HomeCacheService.postsRes = postsRes; HomeCacheService.postsRes = postsRes;
} }
this.state.tagline = getRandomFromList( this.state.tagline = getRandomFromList(this.state?.siteRes?.taglines ?? [])
this.state?.siteRes?.taglines ?? [] ?.content;
)?.content;
} }
componentWillUnmount() { componentWillUnmount() {
@ -298,7 +306,7 @@ export class Home extends Component<any, HomeState> {
if ( if (
!this.state.isIsomorphic || !this.state.isIsomorphic ||
!Object.values(this.isoData.routeData).some( !Object.values(this.isoData.routeData).some(
res => res.state === "success" || res.state === "failed" res => res.state === "success" || res.state === "failed",
) )
) { ) {
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]); await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
@ -309,8 +317,7 @@ export class Home extends Component<any, HomeState> {
static async fetchInitialData({ static async fetchInitialData({
client, client,
auth, query: { dataType: urlDataType, listingType, pageCursor, sort: urlSort },
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
site, site,
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> { }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
const dataType = getDataTypeFromQuery(urlDataType); const dataType = getDataTypeFromQuery(urlDataType);
@ -319,32 +326,25 @@ export class Home extends Component<any, HomeState> {
site.site_view.local_site.default_post_listing_type; site.site_view.local_site.default_post_listing_type;
const sort = getSortTypeFromQuery(urlSort); const sort = getSortTypeFromQuery(urlSort);
const page = urlPage ? Number(urlPage) : 1; let postsRes: RequestState<GetPostsResponse> = EMPTY_REQUEST;
let commentsRes: RequestState<GetCommentsResponse> = EMPTY_REQUEST;
let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
let commentsRes: RequestState<GetCommentsResponse> = {
state: "empty",
};
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
const getPostsForm: GetPosts = { const getPostsForm: GetPosts = {
type_, type_,
page, page_cursor: pageCursor,
limit: fetchLimit, limit: fetchLimit,
sort, sort,
saved_only: false, saved_only: false,
auth,
}; };
postsRes = await client.getPosts(getPostsForm); postsRes = await client.getPosts(getPostsForm);
} else { } else {
const getCommentsForm: GetComments = { const getCommentsForm: GetComments = {
page,
limit: fetchLimit, limit: fetchLimit,
sort: postToCommentSortType(sort), sort: postToCommentSortType(sort),
type_, type_,
saved_only: false, saved_only: false,
auth,
}; };
commentsRes = await client.getComments(getCommentsForm); commentsRes = await client.getComments(getCommentsForm);
@ -354,12 +354,11 @@ export class Home extends Component<any, HomeState> {
type_: "Local", type_: "Local",
sort: "Hot", sort: "Hot",
limit: trendingFetchLimit, limit: trendingFetchLimit,
auth,
}; };
return { return {
trendingCommunitiesRes: await client.listCommunities( trendingCommunitiesRes: await client.listCommunities(
trendingCommunitiesForm trendingCommunitiesForm,
), ),
commentsRes, commentsRes,
postsRes, postsRes,
@ -616,18 +615,22 @@ export class Home extends Component<any, HomeState> {
); );
} }
async updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) { async updateUrl({
dataType,
listingType,
pageCursor,
sort,
}: Partial<HomeProps>) {
const { const {
dataType: urlDataType, dataType: urlDataType,
listingType: urlListingType, listingType: urlListingType,
page: urlPage,
sort: urlSort, sort: urlSort,
} = getHomeQueryParams(); } = getHomeQueryParams();
const queryParams: QueryParams<HomeProps> = { const queryParams: QueryParams<HomeProps> = {
dataType: getDataTypeString(dataType ?? urlDataType), dataType: getDataTypeString(dataType ?? urlDataType),
listingType: listingType ?? urlListingType, listingType: listingType ?? urlListingType,
page: (page ?? urlPage).toString(), pageCursor: pageCursor,
sort: sort ?? urlSort, sort: sort ?? urlSort,
}; };
@ -645,19 +648,30 @@ export class Home extends Component<any, HomeState> {
} }
get posts() { get posts() {
const { page } = getHomeQueryParams(); const { pageCursor } = getHomeQueryParams();
return ( return (
<div className="main-content-wrapper"> <div className="main-content-wrapper">
<div> <div>
{this.selects} {this.selects}
{this.listings} {this.listings}
<Paginator page={page} onChange={this.handlePageChange} /> <PaginatorCursor
prevPage={pageCursor}
nextPage={this.getNextPage}
onNext={this.handlePageNext}
onPrev={this.handlePagePrev}
/>
</div> </div>
</div> </div>
); );
} }
get getNextPage(): PaginationCursor | undefined {
return this.state.postsRes.state === "success"
? this.state.postsRes.data.next_page
: undefined;
}
get listings() { get listings() {
const { dataType } = getHomeQueryParams(); const { dataType } = getHomeQueryParams();
const siteRes = this.state.siteRes; const siteRes = this.state.siteRes;
@ -699,6 +713,7 @@ export class Home extends Component<any, HomeState> {
onAddAdmin={this.handleAddAdmin} onAddAdmin={this.handleAddAdmin}
onTransferCommunity={this.handleTransferCommunity} onTransferCommunity={this.handleTransferCommunity}
onFeaturePost={this.handleFeaturePost} onFeaturePost={this.handleFeaturePost}
onMarkPostAsRead={this.handleMarkPostAsRead}
/> />
); );
} }
@ -718,7 +733,7 @@ export class Home extends Component<any, HomeState> {
nodes={commentsToFlatNodes(comments)} nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished} finished={this.state.finished}
noIndent isTopLevel
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
@ -777,7 +792,7 @@ export class Home extends Component<any, HomeState> {
<div className="col-auto ps-0"> <div className="col-auto ps-0">
{getRss( {getRss(
listingType ?? listingType ??
this.state.siteRes.site_view.local_site.default_post_listing_type this.state.siteRes.site_view.local_site.default_post_listing_type,
)} )}
</div> </div>
</div> </div>
@ -785,20 +800,18 @@ export class Home extends Component<any, HomeState> {
} }
async fetchTrendingCommunities() { async fetchTrendingCommunities() {
this.setState({ trendingCommunitiesRes: { state: "loading" } }); this.setState({ trendingCommunitiesRes: LOADING_REQUEST });
this.setState({ this.setState({
trendingCommunitiesRes: await HttpService.client.listCommunities({ trendingCommunitiesRes: await HttpService.client.listCommunities({
type_: "Local", type_: "Local",
sort: "Hot", sort: "Hot",
limit: trendingFetchLimit, limit: trendingFetchLimit,
auth: myAuth(),
}), }),
}); });
} }
async fetchData() { async fetchData() {
const auth = myAuth(); const { dataType, pageCursor, listingType, sort } = getHomeQueryParams();
const { dataType, page, listingType, sort } = getHomeQueryParams();
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
if (HomeCacheService.active) { if (HomeCacheService.active) {
@ -808,33 +821,29 @@ export class Home extends Component<any, HomeState> {
window.scrollTo({ window.scrollTo({
left: 0, left: 0,
top: scrollY, top: scrollY,
behavior: "instant",
}); });
} else { } else {
this.setState({ postsRes: { state: "loading" } }); this.setState({ postsRes: LOADING_REQUEST });
this.setState({ this.setState({
postsRes: await HttpService.client.getPosts({ postsRes: await HttpService.client.getPosts({
page, page_cursor: pageCursor,
limit: fetchLimit, limit: fetchLimit,
sort, sort,
saved_only: false, saved_only: false,
type_: listingType, type_: listingType,
auth,
}), }),
}); });
HomeCacheService.postsRes = this.state.postsRes; HomeCacheService.postsRes = this.state.postsRes;
} }
} else { } else {
this.setState({ commentsRes: { state: "loading" } }); this.setState({ commentsRes: LOADING_REQUEST });
this.setState({ this.setState({
commentsRes: await HttpService.client.getComments({ commentsRes: await HttpService.client.getComments({
page,
limit: fetchLimit, limit: fetchLimit,
sort: postToCommentSortType(sort), sort: postToCommentSortType(sort),
saved_only: false, saved_only: false,
type_: listingType, type_: listingType,
auth,
}), }),
}); });
} }
@ -858,24 +867,32 @@ export class Home extends Component<any, HomeState> {
i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed }); i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
} }
handlePageChange(page: number) { handlePagePrev() {
this.props.history.back();
// A hack to scroll to top
setTimeout(() => {
window.scrollTo(0, 0);
}, 50);
}
handlePageNext(nextPage: PaginationCursor) {
this.setState({ scrolled: false }); this.setState({ scrolled: false });
this.updateUrl({ page }); this.updateUrl({ pageCursor: nextPage });
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.setState({ scrolled: false }); this.setState({ scrolled: false });
this.updateUrl({ sort: val, page: 1 }); this.updateUrl({ sort: val, pageCursor: undefined });
} }
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.setState({ scrolled: false }); this.setState({ scrolled: false });
this.updateUrl({ listingType: val, page: 1 }); this.updateUrl({ listingType: val, pageCursor: undefined });
} }
handleDataTypeChange(val: DataType) { handleDataTypeChange(val: DataType) {
this.setState({ scrolled: false }); this.setState({ scrolled: false });
this.updateUrl({ dataType: val, page: 1 }); this.updateUrl({ dataType: val, pageCursor: undefined });
} }
async handleAddModToCommunity(form: AddModToCommunity) { async handleAddModToCommunity(form: AddModToCommunity) {
@ -900,7 +917,7 @@ export class Home extends Component<any, HomeState> {
async handleBlockPerson(form: BlockPerson) { async handleBlockPerson(form: BlockPerson) {
const blockPersonRes = await HttpService.client.blockPerson(form); const blockPersonRes = await HttpService.client.blockPerson(form);
if (blockPersonRes.state == "success") { if (blockPersonRes.state === "success") {
updatePersonBlock(blockPersonRes.data); updatePersonBlock(blockPersonRes.data);
} }
} }
@ -914,7 +931,7 @@ export class Home extends Component<any, HomeState> {
async handleEditComment(form: EditComment) { async handleEditComment(form: EditComment) {
const editCommentRes = await HttpService.client.editComment(form); const editCommentRes = await HttpService.client.editComment(form);
this.findAndUpdateComment(editCommentRes); this.findAndUpdateCommentEdit(editCommentRes);
return editCommentRes; return editCommentRes;
} }
@ -971,14 +988,14 @@ export class Home extends Component<any, HomeState> {
async handleCommentReport(form: CreateCommentReport) { async handleCommentReport(form: CreateCommentReport) {
const reportRes = await HttpService.client.createCommentReport(form); const reportRes = await HttpService.client.createCommentReport(form);
if (reportRes.state == "success") { if (reportRes.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} }
} }
async handlePostReport(form: CreatePostReport) { async handlePostReport(form: CreatePostReport) {
const reportRes = await HttpService.client.createPostReport(form); const reportRes = await HttpService.client.createPostReport(form);
if (reportRes.state == "success") { if (reportRes.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} }
} }
@ -996,7 +1013,7 @@ export class Home extends Component<any, HomeState> {
async handleAddAdmin(form: AddAdmin) { async handleAddAdmin(form: AddAdmin) {
const addAdminRes = await HttpService.client.addAdmin(form); const addAdminRes = await HttpService.client.addAdmin(form);
if (addAdminRes.state == "success") { if (addAdminRes.state === "success") {
this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s)); this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
} }
} }
@ -1026,22 +1043,27 @@ export class Home extends Component<any, HomeState> {
this.updateBan(banRes); this.updateBan(banRes);
} }
async handleMarkPostAsRead(form: MarkPostAsRead) {
const res = await HttpService.client.markPostAsRead(form);
this.findAndUpdatePost(res);
}
updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) { updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success") { if (s.postsRes.state === "success") {
s.postsRes.data.posts s.postsRes.data.posts
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
if (s.commentsRes.state == "success") { if (s.commentsRes.state === "success") {
s.commentsRes.data.comments s.commentsRes.data.comments
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
return s; return s;
@ -1051,16 +1073,16 @@ export class Home extends Component<any, HomeState> {
updateBan(banRes: RequestState<BanPersonResponse>) { updateBan(banRes: RequestState<BanPersonResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success") { if (s.postsRes.state === "success") {
s.postsRes.data.posts s.postsRes.data.posts
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
if (s.commentsRes.state == "success") { if (s.commentsRes.state === "success") {
s.commentsRes.data.comments s.commentsRes.data.comments
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
return s; return s;
@ -1069,18 +1091,18 @@ export class Home extends Component<any, HomeState> {
} }
purgeItem(purgeRes: RequestState<PurgeItemResponse>) { purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
if (purgeRes.state == "success") { if (purgeRes.state === "success") {
toast(I18NextService.i18n.t("purge_success")); toast(I18NextService.i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} }
findAndUpdateComment(res: RequestState<CommentResponse>) { findAndUpdateCommentEdit(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editComment( s.commentsRes.data.comments = editComment(
res.data.comment_view, res.data.comment_view,
s.commentsRes.data.comments s.commentsRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true); s.finished.set(res.data.comment_view.comment.id, true);
} }
@ -1088,15 +1110,27 @@ export class Home extends Component<any, HomeState> {
}); });
} }
findAndUpdateComment(res: RequestState<CommentResponse>) {
this.setState(s => {
if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editComment(
res.data.comment_view,
s.commentsRes.data.comments,
);
}
return s;
});
}
createAndUpdateComments(res: RequestState<CommentResponse>) { createAndUpdateComments(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments.unshift(res.data.comment_view); s.commentsRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent // Set finished for the parent
s.finished.set( s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0, getCommentParentId(res.data.comment_view.comment) ?? 0,
true true,
); );
} }
return s; return s;
@ -1105,10 +1139,10 @@ export class Home extends Component<any, HomeState> {
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) { findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state == "success" && res.state == "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments = editWith( s.commentsRes.data.comments = editWith(
res.data.comment_reply_view, res.data.comment_reply_view,
s.commentsRes.data.comments s.commentsRes.data.comments,
); );
} }
return s; return s;
@ -1117,10 +1151,10 @@ export class Home extends Component<any, HomeState> {
findAndUpdatePost(res: RequestState<PostResponse>) { findAndUpdatePost(res: RequestState<PostResponse>) {
this.setState(s => { this.setState(s => {
if (s.postsRes.state == "success" && res.state == "success") { if (s.postsRes.state === "success" && res.state === "success") {
s.postsRes.data.posts = editPost( s.postsRes.data.posts = editPost(
res.data.post_view, res.data.post_view,
s.postsRes.data.posts s.postsRes.data.posts,
); );
} }
return s; return s;

View file

@ -6,12 +6,19 @@ import {
GetSiteResponse, GetSiteResponse,
Instance, Instance,
} from "lemmy-js-client"; } from "lemmy-js-client";
import classNames from "classnames";
import { relTags } from "../../config"; import { relTags } from "../../config";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services"; import { FirstLoadService, I18NextService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import Tabs from "../common/tabs";
type InstancesData = RouteDataResponse<{ type InstancesData = RouteDataResponse<{
federatedInstancesResponse: GetFederatedInstancesResponse; federatedInstancesResponse: GetFederatedInstancesResponse;
@ -26,7 +33,7 @@ interface InstancesState {
export class Instances extends Component<any, InstancesState> { export class Instances extends Component<any, InstancesState> {
private isoData = setIsoData<InstancesData>(this.context); private isoData = setIsoData<InstancesData>(this.context);
state: InstancesState = { state: InstancesState = {
instancesRes: { state: "empty" }, instancesRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
isIsomorphic: false, isIsomorphic: false,
}; };
@ -52,11 +59,11 @@ export class Instances extends Component<any, InstancesState> {
async fetchInstances() { async fetchInstances() {
this.setState({ this.setState({
instancesRes: { state: "loading" }, instancesRes: LOADING_REQUEST,
}); });
this.setState({ this.setState({
instancesRes: await HttpService.client.getFederatedInstances({}), instancesRes: await HttpService.client.getFederatedInstances(),
}); });
} }
@ -64,7 +71,7 @@ export class Instances extends Component<any, InstancesState> {
client, client,
}: InitialFetchRequest): Promise<InstancesData> { }: InitialFetchRequest): Promise<InstancesData> {
return { return {
federatedInstancesResponse: await client.getFederatedInstances({}), federatedInstancesResponse: await client.getFederatedInstances(),
}; };
} }
@ -85,37 +92,32 @@ export class Instances extends Component<any, InstancesState> {
case "success": { case "success": {
const instances = this.state.instancesRes.data.federated_instances; const instances = this.state.instancesRes.data.federated_instances;
return instances ? ( return instances ? (
<> <div className="row">
<h1 className="h4 mb-4">{I18NextService.i18n.t("instances")}</h1> <div className="col-lg-8">
<div className="row"> <Tabs
<div className="col-md-6"> tabs={["linked", "allowed", "blocked"]
<h2 className="h5 mb-3"> .filter(status => instances[status].length)
{I18NextService.i18n.t("linked_instances")} .map(status => ({
</h2> key: status,
{this.itemList(instances.linked)} label: I18NextService.i18n.t(`${status}_instances`),
</div> getNode: isSelected => (
<div
role="tabpanel"
className={classNames("tab-pane show", {
active: isSelected,
})}
>
{status === "blocked"
? this.itemList(instances[status], false)
: this.itemList(instances[status])}
</div>
),
}))}
/>
</div> </div>
<div className="row"> </div>
{instances.allowed && instances.allowed.length > 0 && (
<div className="col-md-6">
<h2 className="h5 mb-3">
{I18NextService.i18n.t("allowed_instances")}
</h2>
{this.itemList(instances.allowed)}
</div>
)}
{instances.blocked && instances.blocked.length > 0 && (
<div className="col-md-6">
<h2 className="h5 mb-3">
{I18NextService.i18n.t("blocked_instances")}
</h2>
{this.itemList(instances.blocked)}
</div>
)}
</div>
</>
) : ( ) : (
<></> <h5>No linked instance</h5>
); );
} }
} }
@ -133,7 +135,7 @@ export class Instances extends Component<any, InstancesState> {
); );
} }
itemList(items: Instance[]) { itemList(items: Instance[], link = true) {
return items.length > 0 ? ( return items.length > 0 ? (
<div className="table-responsive"> <div className="table-responsive">
<table id="instances_table" className="table table-sm table-hover"> <table id="instances_table" className="table table-sm table-hover">
@ -148,9 +150,13 @@ export class Instances extends Component<any, InstancesState> {
{items.map(i => ( {items.map(i => (
<tr key={i.domain}> <tr key={i.domain}>
<td> <td>
<a href={`https://${i.domain}`} rel={relTags}> {link ? (
{i.domain} <a href={`https://${i.domain}`} rel={relTags}>
</a> {i.domain}{" "}
</a>
) : (
<span>{i.domain}</span>
)}
</td> </td>
<td>{i.software}</td> <td>{i.software}</td>
<td>{i.version}</td> <td>{i.version}</td>

View file

@ -2,7 +2,7 @@ import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter, validEmail } from "@utils/helpers"; import { capitalizeFirstLetter, validEmail } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import { HttpService, I18NextService, UserService } from "../../services"; import { HttpService, I18NextService } from "../../services";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -30,15 +30,9 @@ export class LoginReset extends Component<any, State> {
super(props, context); super(props, context);
} }
componentDidMount() {
if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/");
}
}
get documentTitle(): string { get documentTitle(): string {
return `${capitalizeFirstLetter( return `${capitalizeFirstLetter(
I18NextService.i18n.t("forgot_password") I18NextService.i18n.t("forgot_password"),
)} - ${this.state.siteRes.site_view.site.name}`; )} - ${this.state.siteRes.site_view.site.name}`;
} }
@ -127,7 +121,7 @@ export class LoginReset extends Component<any, State> {
const res = await HttpService.client.passwordReset({ email }); const res = await HttpService.client.passwordReset({ email });
if (res.state == "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("reset_password_mail_sent")); toast(I18NextService.i18n.t("reset_password_mail_sent"));
i.context.router.history.push("/login"); i.context.router.history.push("/login");
} }

View file

@ -1,44 +1,130 @@
import { myAuth, setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { isBrowser } from "@utils/browser"; import { isBrowser } from "@utils/browser";
import { getQueryParams } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { NavLink } from "inferno-router"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client"; import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
import TotpModal from "../common/totp-modal";
import { UnreadCounterService } from "../../services";
interface LoginProps {
prev?: string;
}
const getLoginQueryParams = () =>
getQueryParams<LoginProps>({
prev(param) {
return param ? decodeURIComponent(param) : undefined;
},
});
interface State { interface State {
loginRes: RequestState<LoginResponse>; loginRes: RequestState<LoginResponse>;
form: { form: {
username_or_email?: string; username_or_email: string;
password?: string; password: string;
totp_2fa_token?: string;
}; };
showTotp: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
show2faModal: boolean;
} }
export class Login extends Component<any, State> { async function handleLoginSuccess(i: Login, loginRes: LoginResponse) {
UserService.Instance.login({
res: loginRes,
});
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
}
const { prev } = getLoginQueryParams();
prev
? i.props.history.replace(prev)
: i.props.history.action === "PUSH"
? i.props.history.back()
: i.props.history.replace("/");
UnreadCounterService.Instance.updateAll();
}
async function handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
const { password, username_or_email } = i.state.form;
if (username_or_email && password) {
i.setState({ loginRes: LOADING_REQUEST });
const loginRes = await HttpService.client.login({
username_or_email,
password,
});
switch (loginRes.state) {
case "failed": {
if (loginRes.msg === "missing_totp_token") {
i.setState({ show2faModal: true });
} else {
toast(I18NextService.i18n.t(loginRes.msg), "danger");
}
i.setState({ loginRes });
break;
}
case "success": {
handleLoginSuccess(i, loginRes.data);
break;
}
}
}
}
function handleLoginUsernameChange(i: Login, event: any) {
i.setState(
prevState => (prevState.form.username_or_email = event.target.value.trim()),
);
}
function handleLoginPasswordChange(i: Login, event: any) {
i.setState(prevState => (prevState.form.password = event.target.value));
}
function handleClose2faModal(i: Login) {
i.setState({ show2faModal: false });
}
export class Login extends Component<
RouteComponentProps<Record<string, never>>,
State
> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
state: State = { state: State = {
loginRes: { state: "empty" }, loginRes: EMPTY_REQUEST,
form: {}, form: {
showTotp: false, username_or_email: "",
password: "",
},
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
show2faModal: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
}
componentDidMount() { this.handleSubmitTotp = this.handleSubmitTotp.bind(this);
// Navigate to home if already logged in
if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/");
}
} }
get documentTitle(): string { get documentTitle(): string {
@ -48,7 +134,7 @@ export class Login extends Component<any, State> {
} }
get isLemmyMl(): boolean { get isLemmyMl(): boolean {
return isBrowser() && window.location.hostname == "lemmy.ml"; return isBrowser() && window.location.hostname === "lemmy.ml";
} }
render() { render() {
@ -58,6 +144,12 @@ export class Login extends Component<any, State> {
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
/> />
<TotpModal
type="login"
onSubmit={this.handleSubmitTotp}
show={this.state.show2faModal}
onClose={linkEvent(this, handleClose2faModal)}
/>
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div> <div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
</div> </div>
@ -65,10 +157,28 @@ export class Login extends Component<any, State> {
); );
} }
async handleSubmitTotp(totp: string) {
const loginRes = await HttpService.client.login({
password: this.state.form.password,
username_or_email: this.state.form.username_or_email,
totp_2fa_token: totp,
});
const successful = loginRes.state === "success";
if (successful) {
this.setState({ show2faModal: false });
handleLoginSuccess(this, loginRes.data);
} else {
toast(I18NextService.i18n.t("incorrect_totp_code"), "danger");
}
return successful;
}
loginForm() { loginForm() {
return ( return (
<div> <div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}> <form onSubmit={linkEvent(this, handleLoginSubmit)}>
<h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1> <h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1>
<div className="mb-3 row"> <div className="mb-3 row">
<label <label
@ -83,62 +193,26 @@ export class Login extends Component<any, State> {
className="form-control" className="form-control"
id="login-email-or-username" id="login-email-or-username"
value={this.state.form.username_or_email} value={this.state.form.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)} onInput={linkEvent(this, handleLoginUsernameChange)}
autoComplete="email" autoComplete="email"
required required
minLength={3} minLength={3}
/> />
</div> </div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label className="col-sm-2 col-form-label" htmlFor="login-password"> <PasswordInput
{I18NextService.i18n.t("password")} id="login-password"
</label> value={this.state.form.password}
<div className="col-sm-10"> onInput={linkEvent(this, handleLoginPasswordChange)}
<input label={I18NextService.i18n.t("password")}
type="password" showForgotLink
id="login-password" />
value={this.state.form.password}
onInput={linkEvent(this, this.handleLoginPasswordChange)}
className="form-control"
autoComplete="current-password"
required
maxLength={60}
/>
<NavLink
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
to="/login_reset"
>
{I18NextService.i18n.t("forgot_password")}
</NavLink>
</div>
</div> </div>
{this.state.showTotp && (
<div className="mb-3 row">
<label
className="col-sm-6 col-form-label"
htmlFor="login-totp-token"
>
{I18NextService.i18n.t("two_factor_token")}
</label>
<div className="col-sm-6">
<input
type="number"
inputMode="numeric"
className="form-control"
id="login-totp-token"
pattern="[0-9]*"
autoComplete="one-time-code"
value={this.state.form.totp_2fa_token}
onInput={linkEvent(this, this.handleLoginTotpChange)}
/>
</div>
</div>
)}
<div className="mb-3 row"> <div className="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button type="submit" className="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.loginRes.state == "loading" ? ( {this.state.loginRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
I18NextService.i18n.t("login") I18NextService.i18n.t("login")
@ -150,64 +224,4 @@ export class Login extends Component<any, State> {
</div> </div>
); );
} }
async handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
const { password, totp_2fa_token, username_or_email } = i.state.form;
if (username_or_email && password) {
i.setState({ loginRes: { state: "loading" } });
const loginRes = await HttpService.client.login({
username_or_email,
password,
totp_2fa_token,
});
switch (loginRes.state) {
case "failed": {
if (loginRes.msg === "missing_totp_token") {
i.setState({ showTotp: true });
toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
}
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
break;
}
case "success": {
UserService.Instance.login({
res: loginRes.data,
});
const site = await HttpService.client.getSite({
auth: myAuth(),
});
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
}
i.props.history.action === "PUSH"
? i.props.history.back()
: i.props.history.replace("/");
break;
}
}
}
}
handleLoginUsernameChange(i: Login, event: any) {
i.state.form.username_or_email = event.target.value.trim();
i.setState(i.state);
}
handleLoginTotpChange(i: Login, event: any) {
i.state.form.totp_2fa_token = event.target.value;
i.setState(i.state);
}
handleLoginPasswordChange(i: Login, event: any) {
i.state.form.password = event.target.value;
i.setState(i.state);
}
} }

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import classNames from "classnames"; import classNames from "classnames";
import { Component, FormEventHandler, linkEvent } from "inferno"; import { Component, FormEventHandler, linkEvent } from "inferno";
@ -88,7 +87,7 @@ function RateLimits({
function handleRateLimitChange( function handleRateLimitChange(
{ rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm }, { rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm },
event: any event: any,
) { ) {
ctx.setState(prev => ({ ctx.setState(prev => ({
...prev, ...prev,
@ -101,7 +100,7 @@ function handleRateLimitChange(
function handlePerSecondChange( function handlePerSecondChange(
{ rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm }, { rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm },
event: any event: any,
) { ) {
ctx.setState(prev => ({ ctx.setState(prev => ({
...prev, ...prev,
@ -114,15 +113,12 @@ function handlePerSecondChange(
function submitRateLimitForm(i: RateLimitsForm, event: any) { function submitRateLimitForm(i: RateLimitsForm, event: any) {
event.preventDefault(); event.preventDefault();
const auth = myAuthRequired();
const form: EditSite = Object.entries(i.state.form).reduce( const form: EditSite = Object.entries(i.state.form).reduce(
(acc, [key, val]) => { (acc, [key, val]) => {
acc[`rate_limit_${key}`] = val; acc[`rate_limit_${key}`] = val;
return acc; return acc;
}, },
{ {},
auth,
}
); );
i.props.onSaveSite(form); i.props.onSaveSite(form);
@ -159,11 +155,11 @@ export default class RateLimitsForm extends Component<
})} })}
handleRateLimit={linkEvent( handleRateLimit={linkEvent(
{ rateLimitType, ctx: this }, { rateLimitType, ctx: this },
handleRateLimitChange handleRateLimitChange,
)} )}
handleRateLimitPerSecond={linkEvent( handleRateLimitPerSecond={linkEvent(
{ rateLimitType, ctx: this }, { rateLimitType, ctx: this },
handlePerSecondChange handlePerSecondChange,
)} )}
rateLimitValue={this.state.form[rateLimitType]} rateLimitValue={this.state.form[rateLimitType]}
rateLimitPerSecondValue={ rateLimitPerSecondValue={

View file

@ -8,8 +8,14 @@ import {
Register, Register,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
interface State { interface State {
@ -34,7 +40,7 @@ export class Setup extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
state: State = { state: State = {
registerRes: { state: "empty" }, registerRes: EMPTY_REQUEST,
themeList: [], themeList: [],
form: { form: {
show_nsfw: true, show_nsfw: true,
@ -121,46 +127,28 @@ export class Setup extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label className="col-sm-2 col-form-label" htmlFor="password"> <PasswordInput
{I18NextService.i18n.t("password")} id="password"
</label> value={this.state.form.password}
<div className="col-sm-10"> onInput={linkEvent(this, this.handleRegisterPasswordChange)}
<input label={I18NextService.i18n.t("password")}
type="password" isNew
id="password" />
value={this.state.form.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
className="form-control"
required
autoComplete="new-password"
minLength={10}
maxLength={60}
/>
</div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label className="col-sm-2 col-form-label" htmlFor="verify-password"> <PasswordInput
{I18NextService.i18n.t("verify_password")} id="verify-password"
</label> value={this.state.form.password_verify}
<div className="col-sm-10"> onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
<input label={I18NextService.i18n.t("verify_password")}
type="password" isNew
id="verify-password" />
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
className="form-control"
required
autoComplete="new-password"
minLength={10}
maxLength={60}
/>
</div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button type="submit" className="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.registerRes.state == "loading" ? ( {this.state.registerRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
I18NextService.i18n.t("sign_up") I18NextService.i18n.t("sign_up")
@ -174,7 +162,7 @@ export class Setup extends Component<any, State> {
async handleRegisterSubmit(i: Setup, event: any) { async handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ registerRes: { state: "loading" } }); i.setState({ registerRes: LOADING_REQUEST });
const { const {
username, username,
password_verify, password_verify,
@ -203,7 +191,7 @@ export class Setup extends Component<any, State> {
registerRes: await HttpService.client.register(form), registerRes: await HttpService.client.register(form),
}); });
if (i.state.registerRes.state == "success") { if (i.state.registerRes.state === "success") {
const data = i.state.registerRes.data; const data = i.state.registerRes.data;
UserService.Instance.login({ res: data }); UserService.Instance.login({ res: data });

View file

@ -1,8 +1,6 @@
import { myAuth, setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { isBrowser } from "@utils/browser"; import { isBrowser } from "@utils/browser";
import { validEmail } from "@utils/helpers"; import { validEmail } from "@utils/helpers";
import { Options, passwordStrength } from "check-password-strength";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -15,38 +13,17 @@ import {
import { joinLemmyUrl } from "../../config"; import { joinLemmyUrl } from "../../config";
import { mdToHtml } from "../../markdown"; import { mdToHtml } from "../../markdown";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import PasswordInput from "../common/password-input";
const passwordStrengthOptions: Options<string> = [
{
id: 0,
value: "very_weak",
minDiversity: 0,
minLength: 0,
},
{
id: 1,
value: "weak",
minDiversity: 2,
minLength: 10,
},
{
id: 2,
value: "medium",
minDiversity: 3,
minLength: 12,
},
{
id: 3,
value: "strong",
minDiversity: 4,
minLength: 14,
},
];
interface State { interface State {
registerRes: RequestState<LoginResponse>; registerRes: RequestState<LoginResponse>;
@ -71,8 +48,8 @@ export class Signup extends Component<any, State> {
private audio?: HTMLAudioElement; private audio?: HTMLAudioElement;
state: State = { state: State = {
registerRes: { state: "empty" }, registerRes: EMPTY_REQUEST,
captchaRes: { state: "empty" }, captchaRes: EMPTY_REQUEST,
form: { form: {
show_nsfw: false, show_nsfw: false,
}, },
@ -93,13 +70,13 @@ export class Signup extends Component<any, State> {
} }
async fetchCaptcha() { async fetchCaptcha() {
this.setState({ captchaRes: { state: "loading" } }); this.setState({ captchaRes: LOADING_REQUEST });
this.setState({ this.setState({
captchaRes: await HttpService.client.getCaptcha({}), captchaRes: await HttpService.client.getCaptcha(),
}); });
this.setState(s => { this.setState(s => {
if (s.captchaRes.state == "success") { if (s.captchaRes.state === "success") {
s.form.captcha_uuid = s.captchaRes.data.ok?.uuid; s.form.captcha_uuid = s.captchaRes.data.ok?.uuid;
} }
return s; return s;
@ -113,12 +90,12 @@ export class Signup extends Component<any, State> {
titleName(siteView: SiteView): string { titleName(siteView: SiteView): string {
return I18NextService.i18n.t( return I18NextService.i18n.t(
siteView.local_site.private_instance ? "apply_to_join" : "sign_up" siteView.local_site.private_instance ? "apply_to_join" : "sign_up",
); );
} }
get isLemmyMl(): boolean { get isLemmyMl(): boolean {
return isBrowser() && window.location.hostname == "lemmy.ml"; return isBrowser() && window.location.hostname === "lemmy.ml";
} }
render() { render() {
@ -219,57 +196,28 @@ export class Signup extends Component<any, State> {
</div> </div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label <PasswordInput
className="col-sm-2 col-form-label" id="register-password"
htmlFor="register-password" value={this.state.form.password}
> onInput={linkEvent(this, this.handleRegisterPasswordChange)}
{I18NextService.i18n.t("password")} showStrength
</label> label={I18NextService.i18n.t("password")}
<div className="col-sm-10"> isNew
<input />
type="password"
id="register-password"
value={this.state.form.password}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10}
maxLength={60}
className="form-control"
required
/>
{this.state.form.password && (
<div className={this.passwordColorClass}>
{I18NextService.i18n.t(
this.passwordStrength as NoOptionI18nKeys
)}
</div>
)}
</div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label <PasswordInput
className="col-sm-2 col-form-label" id="register-verify-password"
htmlFor="register-verify-password" value={this.state.form.password_verify}
> onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
{I18NextService.i18n.t("verify_password")} label={I18NextService.i18n.t("verify_password")}
</label> isNew
<div className="col-sm-10"> />
<input
type="password"
id="register-verify-password"
value={this.state.form.password_verify}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
maxLength={60}
className="form-control"
required
/>
</div>
</div> </div>
{siteView.local_site.registration_mode == "RequireApplication" && ( {siteView.local_site.registration_mode === "RequireApplication" && (
<> <>
<div className="mb-3 row"> <div className="mb-3 row">
<div className="offset-sm-2 col-sm-10"> <div className="offset-sm-2 col-sm-10">
@ -281,7 +229,7 @@ export class Signup extends Component<any, State> {
<div <div
className="md-div" className="md-div"
dangerouslySetInnerHTML={mdToHtml( dangerouslySetInnerHTML={mdToHtml(
siteView.local_site.application_question siteView.local_site.application_question,
)} )}
/> />
)} )}
@ -337,7 +285,7 @@ export class Signup extends Component<any, State> {
<div className="mb-3 row"> <div className="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button type="submit" className="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.registerRes.state == "loading" ? ( {this.state.registerRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
this.titleName(siteView) this.titleName(siteView)
@ -379,7 +327,7 @@ export class Signup extends Component<any, State> {
value={this.state.form.captcha_answer} value={this.state.form.captcha_answer}
onInput={linkEvent( onInput={linkEvent(
this, this,
this.handleRegisterCaptchaAnswerChange this.handleRegisterCaptchaAnswerChange,
)} )}
required required
/> />
@ -420,25 +368,6 @@ export class Signup extends Component<any, State> {
); );
} }
get passwordStrength(): string | undefined {
const password = this.state.form.password;
return password
? passwordStrength(password, passwordStrengthOptions).value
: undefined;
}
get passwordColorClass(): string {
const strength = this.passwordStrength;
if (strength && ["weak", "medium"].includes(strength)) {
return "text-warning";
} else if (strength == "strong") {
return "text-success";
} else {
return "text-danger";
}
}
async handleRegisterSubmit(i: Signup, event: any) { async handleRegisterSubmit(i: Signup, event: any) {
event.preventDefault(); event.preventDefault();
const { const {
@ -453,7 +382,7 @@ export class Signup extends Component<any, State> {
username, username,
} = i.state.form; } = i.state.form;
if (username && password && password_verify) { if (username && password && password_verify) {
i.setState({ registerRes: { state: "loading" } }); i.setState({ registerRes: LOADING_REQUEST });
const registerRes = await HttpService.client.register({ const registerRes = await HttpService.client.register({
username, username,
@ -469,7 +398,7 @@ export class Signup extends Component<any, State> {
switch (registerRes.state) { switch (registerRes.state) {
case "failed": { case "failed": {
toast(registerRes.msg, "danger"); toast(registerRes.msg, "danger");
i.setState({ registerRes: { state: "empty" } }); i.setState({ registerRes: EMPTY_REQUEST });
break; break;
} }
@ -482,7 +411,7 @@ export class Signup extends Component<any, State> {
res: data, res: data,
}); });
const site = await HttpService.client.getSite({ auth: myAuth() }); const site = await HttpService.client.getSite();
if (site.state === "success") { if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user; UserService.Instance.myUserInfo = site.data.my_user;
@ -511,7 +440,7 @@ export class Signup extends Component<any, State> {
handleRegisterEmailChange(i: Signup, event: any) { handleRegisterEmailChange(i: Signup, event: any) {
i.state.form.email = event.target.value; i.state.form.email = event.target.value;
if (i.state.form.email == "") { if (i.state.form.email === "") {
i.state.form.email = undefined; i.state.form.email = undefined;
} }
i.setState(i.state); i.setState(i.state);
@ -556,7 +485,7 @@ export class Signup extends Component<any, State> {
// This was a bad bug, it should only build the new audio on a new file. // This was a bad bug, it should only build the new audio on a new file.
// Replays would stop prematurely if this was rebuilt every time. // Replays would stop prematurely if this was rebuilt every time.
if (i.state.captchaRes.state == "success" && i.state.captchaRes.data.ok) { if (i.state.captchaRes.state === "success" && i.state.captchaRes.data.ok) {
const captchaRes = i.state.captchaRes.data.ok; const captchaRes = i.state.captchaRes.data.ok;
if (!i.audio) { if (!i.audio) {
const base64 = `data:audio/wav;base64,${captchaRes.wav}`; const base64 = `data:audio/wav;base64,${captchaRes.wav}`;

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers"; import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers";
import { import {
Component, Component,
@ -7,6 +6,7 @@ import {
InfernoNode, InfernoNode,
linkEvent, linkEvent,
} from "inferno"; } from "inferno";
import { Prompt } from "inferno-router";
import { import {
CreateSite, CreateSite,
EditSite, EditSite,
@ -21,7 +21,6 @@ import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select"; import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select"; import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt";
interface SiteFormProps { interface SiteFormProps {
blockedInstances?: Instance[]; blockedInstances?: Instance[];
@ -85,7 +84,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
captcha_difficulty: ls.captcha_difficulty, captcha_difficulty: ls.captcha_difficulty,
allowed_instances: this.props.allowedInstances?.map(i => i.domain), allowed_instances: this.props.allowedInstances?.map(i => i.domain),
blocked_instances: this.props.blockedInstances?.map(i => i.domain), blocked_instances: this.props.blockedInstances?.map(i => i.domain),
auth: "TODO",
}; };
} }
@ -123,7 +121,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
className="site-form" className="site-form"
onSubmit={linkEvent(this, this.handleSaveSiteSubmit)} onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}
> >
<NavigationPrompt <Prompt
message={I18NextService.i18n.t("block_leaving")}
when={ when={
!this.props.loading && !this.props.loading &&
!siteSetup && !siteSetup &&
@ -292,7 +291,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</select> </select>
</div> </div>
</div> </div>
{this.state.siteForm.registration_mode == "RequireApplication" && ( {this.state.siteForm.registration_mode === "RequireApplication" && (
<div className="mb-3 row"> <div className="mb-3 row">
<label className="col-12 col-form-label"> <label className="col-12 col-form-label">
{I18NextService.i18n.t("application_questionnaire")} {I18NextService.i18n.t("application_questionnaire")}
@ -318,7 +317,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.community_creation_admin_only} checked={this.state.siteForm.community_creation_admin_only}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteCommunityCreationAdminOnly this.handleSiteCommunityCreationAdminOnly,
)} )}
/> />
<label <label
@ -340,7 +339,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.require_email_verification} checked={this.state.siteForm.require_email_verification}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteRequireEmailVerification this.handleSiteRequireEmailVerification,
)} )}
/> />
<label <label
@ -362,7 +361,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.application_email_admins} checked={this.state.siteForm.application_email_admins}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteApplicationEmailAdmins this.handleSiteApplicationEmailAdmins,
)} )}
/> />
<label <label
@ -410,6 +409,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<option value="browser"> <option value="browser">
{I18NextService.i18n.t("browser_default")} {I18NextService.i18n.t("browser_default")}
</option> </option>
<option value="browser-compact">
{I18NextService.i18n.t("browser_default_compact")}
</option>
{this.props.themeList?.map(theme => ( {this.props.themeList?.map(theme => (
<option key={theme} value={theme}> <option key={theme} value={theme}>
{theme} {theme}
@ -627,7 +629,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
componentDidUpdate( componentDidUpdate(
prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps> prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps>,
) { ) {
if ( if (
!( !(
@ -690,7 +692,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
className="btn btn-sm bg-danger" className="btn btn-sm bg-danger"
onClick={linkEvent( onClick={linkEvent(
{ key, instance }, { key, instance },
this.handleRemoveInstance this.handleRemoveInstance,
)} )}
> >
<Icon <Icon
@ -718,7 +720,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleInstanceEnterPress( handleInstanceEnterPress(
key: InstanceKey, key: InstanceKey,
event: InfernoKeyboardEvent<HTMLInputElement> event: InfernoKeyboardEvent<HTMLInputElement>,
) { ) {
if (event.code.toLowerCase() === "enter") { if (event.code.toLowerCase() === "enter") {
event.preventDefault(); event.preventDefault();
@ -729,8 +731,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleSaveSiteSubmit(i: SiteForm, event: any) { handleSaveSiteSubmit(i: SiteForm, event: any) {
event.preventDefault(); event.preventDefault();
const auth = myAuthRequired();
i.setState(s => ((s.siteForm.auth = auth), s));
i.setState({ submitted: true }); i.setState({ submitted: true });
const stateSiteForm = i.state.siteForm; const stateSiteForm = i.state.siteForm;
@ -784,7 +784,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
allowed_instances: stateSiteForm.allowed_instances, allowed_instances: stateSiteForm.allowed_instances,
blocked_instances: stateSiteForm.blocked_instances, blocked_instances: stateSiteForm.blocked_instances,
discussion_languages: stateSiteForm.discussion_languages, discussion_languages: stateSiteForm.discussion_languages,
auth,
}; };
} }
@ -859,7 +858,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleDeleteTaglineClick( handleDeleteTaglineClick(
i: SiteForm, i: SiteForm,
index: number, index: number,
event: InfernoMouseEvent<HTMLButtonElement> event: InfernoMouseEvent<HTMLButtonElement>,
) { ) {
event.preventDefault(); event.preventDefault();
const taglines = i.state.siteForm.taglines; const taglines = i.state.siteForm.taglines;
@ -874,7 +873,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleAddTaglineClick( handleAddTaglineClick(
i: SiteForm, i: SiteForm,
event: InfernoMouseEvent<HTMLButtonElement> event: InfernoMouseEvent<HTMLButtonElement>,
) { ) {
event.preventDefault(); event.preventDefault();
if (!i.state.siteForm.taglines) { if (!i.state.siteForm.taglines) {
@ -965,7 +964,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleSiteActorNameMaxLength(i: SiteForm, event: any) { handleSiteActorNameMaxLength(i: SiteForm, event: any) {
i.setState( i.setState(
s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s) s => ((s.siteForm.actor_name_max_length = Number(event.target.value)), s),
); );
} }

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { PersonView, Site, SiteAggregates } from "lemmy-js-client"; import { PersonView, Site, SiteAggregates } from "lemmy-js-client";
import { mdToHtml } from "../../markdown"; import { mdToHtml } from "../../markdown";
@ -32,10 +33,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
return ( return (
<div className="site-sidebar accordion"> <div className="site-sidebar accordion">
<section id="sidebarInfo" className="card border-secondary mb-3"> <section id="sidebarInfo" className="card border-secondary mb-3">
<header <header className="card-header" id="sidebarInfoHeader">
className="card-header d-flex align-items-center"
id="sidebarInfoHeader"
>
{this.siteName()} {this.siteName()}
{!this.state.collapsed && ( {!this.state.collapsed && (
<BannerIconHeader banner={this.props.site.banner} /> <BannerIconHeader banner={this.props.site.banner} />
@ -54,7 +52,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
siteName() { siteName() {
return ( return (
<> <div className={classNames({ "mb-2": !this.state.collapsed })}>
<h5 className="mb-0 d-inline">{this.props.site.name}</h5> <h5 className="mb-0 d-inline">{this.props.site.name}</h5>
{!this.props.isMobile && ( {!this.props.isMobile && (
<button <button
@ -83,7 +81,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
)} )}
</button> </button>
)} )}
</> </div>
); );
} }

View file

@ -1,9 +1,7 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, InfernoMouseEvent, linkEvent } from "inferno"; import { Component, InfernoMouseEvent, linkEvent } from "inferno";
import { EditSite, Tagline } from "lemmy-js-client"; import { EditSite, Tagline } from "lemmy-js-client";
import { I18NextService } from "../../services"; import { I18NextService } from "../../services";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
@ -26,17 +24,10 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
get documentTitle(): string {
return I18NextService.i18n.t("taglines");
}
render() { render() {
return ( return (
<div className="tagline-form col-12"> <div className="tagline-form col-12">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h1 className="h4 mb-4">{I18NextService.i18n.t("taglines")}</h1> <h1 className="h4 mb-4">{I18NextService.i18n.t("taglines")}</h1>
<div className="table-responsive col-12"> <div className="table-responsive col-12">
<table id="taglines_table" className="table table-sm table-hover"> <table id="taglines_table" className="table table-sm table-hover">
@ -48,7 +39,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
{this.state.taglines.map((cv, index) => ( {this.state.taglines.map((cv, index) => (
<tr key={index}> <tr key={index}>
<td> <td>
{this.state.editingRow == index && ( {this.state.editingRow === index && (
<MarkdownTextArea <MarkdownTextArea
initialContent={cv} initialContent={cv}
onContentChange={s => onContentChange={s =>
@ -59,14 +50,14 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
siteLanguages={[]} siteLanguages={[]}
/> />
)} )}
{this.state.editingRow != index && <div>{cv}</div>} {this.state.editingRow !== index && <div>{cv}</div>}
</td> </td>
<td className="text-right"> <td className="text-right">
<button <button
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
{ i: this, index: index }, { i: this, index: index },
this.handleEditTaglineClick this.handleEditTaglineClick,
)} )}
data-tippy-content={I18NextService.i18n.t("edit")} data-tippy-content={I18NextService.i18n.t("edit")}
aria-label={I18NextService.i18n.t("edit")} aria-label={I18NextService.i18n.t("edit")}
@ -78,7 +69,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
{ i: this, index: index }, { i: this, index: index },
this.handleDeleteTaglineClick this.handleDeleteTaglineClick,
)} )}
data-tippy-content={I18NextService.i18n.t("delete")} data-tippy-content={I18NextService.i18n.t("delete")}
aria-label={I18NextService.i18n.t("delete")} aria-label={I18NextService.i18n.t("delete")}
@ -141,7 +132,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 (d.i.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 });
@ -151,13 +142,12 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
async handleSaveClick(i: TaglineForm) { async handleSaveClick(i: TaglineForm) {
i.props.onSaveSite({ i.props.onSaveSite({
taglines: i.state.taglines, taglines: i.state.taglines,
auth: myAuthRequired(),
}); });
} }
handleAddTaglineClick( handleAddTaglineClick(
i: TaglineForm, i: TaglineForm,
event: InfernoMouseEvent<HTMLButtonElement> event: InfernoMouseEvent<HTMLButtonElement>,
) { ) {
event.preventDefault(); event.preventDefault();
const newTaglines = [...i.state.taglines]; const newTaglines = [...i.state.taglines];

View file

@ -1,7 +1,6 @@
import { import {
fetchUsers, fetchUsers,
getUpdatedSearchId, getUpdatedSearchId,
myAuth,
personToChoice, personToChoice,
setIsoData, setIsoData,
} from "@utils/app"; } from "@utils/app";
@ -48,7 +47,12 @@ import {
import { fetchLimit } from "../config"; import { fetchLimit } from "../config";
import { InitialFetchRequest } from "../interfaces"; import { InitialFetchRequest } from "../interfaces";
import { FirstLoadService, I18NextService } from "../services"; import { FirstLoadService, I18NextService } from "../services";
import { HttpService, RequestState } from "../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../services/HttpService";
import { HtmlTags } from "./common/html-tags"; import { HtmlTags } from "./common/html-tags";
import { Icon, Spinner } from "./common/icon"; import { Icon, Spinner } from "./common/icon";
import { MomentTime } from "./common/moment-time"; import { MomentTime } from "./common/moment-time";
@ -121,7 +125,7 @@ function getActionFromString(action?: string): ModlogActionType {
const getModlogActionMapper = const getModlogActionMapper =
( (
actionType: ModlogActionType, actionType: ModlogActionType,
getAction: (view: View) => { id: number; when_: string } getAction: (view: View) => { id: number; when_: string },
) => ) =>
(view: View & { moderator?: Person; admin?: Person }): ModlogType => { (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
const { id, when_ } = getAction(view); const { id, when_ } = getAction(view);
@ -155,111 +159,111 @@ function buildCombined({
.map( .map(
getModlogActionMapper( getModlogActionMapper(
"ModRemovePost", "ModRemovePost",
({ mod_remove_post }: ModRemovePostView) => mod_remove_post ({ mod_remove_post }: ModRemovePostView) => mod_remove_post,
) ),
) )
.concat( .concat(
locked_posts.map( locked_posts.map(
getModlogActionMapper( getModlogActionMapper(
"ModLockPost", "ModLockPost",
({ mod_lock_post }: ModLockPostView) => mod_lock_post ({ mod_lock_post }: ModLockPostView) => mod_lock_post,
) ),
) ),
) )
.concat( .concat(
featured_posts.map( featured_posts.map(
getModlogActionMapper( getModlogActionMapper(
"ModFeaturePost", "ModFeaturePost",
({ mod_feature_post }: ModFeaturePostView) => mod_feature_post ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post,
) ),
) ),
) )
.concat( .concat(
removed_comments.map( removed_comments.map(
getModlogActionMapper( getModlogActionMapper(
"ModRemoveComment", "ModRemoveComment",
({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment,
) ),
) ),
) )
.concat( .concat(
removed_communities.map( removed_communities.map(
getModlogActionMapper( getModlogActionMapper(
"ModRemoveCommunity", "ModRemoveCommunity",
({ mod_remove_community }: ModRemoveCommunityView) => ({ mod_remove_community }: ModRemoveCommunityView) =>
mod_remove_community mod_remove_community,
) ),
) ),
) )
.concat( .concat(
banned_from_community.map( banned_from_community.map(
getModlogActionMapper( getModlogActionMapper(
"ModBanFromCommunity", "ModBanFromCommunity",
({ mod_ban_from_community }: ModBanFromCommunityView) => ({ mod_ban_from_community }: ModBanFromCommunityView) =>
mod_ban_from_community mod_ban_from_community,
) ),
) ),
) )
.concat( .concat(
added_to_community.map( added_to_community.map(
getModlogActionMapper( getModlogActionMapper(
"ModAddCommunity", "ModAddCommunity",
({ mod_add_community }: ModAddCommunityView) => mod_add_community ({ mod_add_community }: ModAddCommunityView) => mod_add_community,
) ),
) ),
) )
.concat( .concat(
transferred_to_community.map( transferred_to_community.map(
getModlogActionMapper( getModlogActionMapper(
"ModTransferCommunity", "ModTransferCommunity",
({ mod_transfer_community }: ModTransferCommunityView) => ({ mod_transfer_community }: ModTransferCommunityView) =>
mod_transfer_community mod_transfer_community,
) ),
) ),
) )
.concat( .concat(
added.map( added.map(
getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add) getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add),
) ),
) )
.concat( .concat(
banned.map( banned.map(
getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban) getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban),
) ),
) )
.concat( .concat(
admin_purged_persons.map( admin_purged_persons.map(
getModlogActionMapper( getModlogActionMapper(
"AdminPurgePerson", "AdminPurgePerson",
({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person,
) ),
) ),
) )
.concat( .concat(
admin_purged_communities.map( admin_purged_communities.map(
getModlogActionMapper( getModlogActionMapper(
"AdminPurgeCommunity", "AdminPurgeCommunity",
({ admin_purge_community }: AdminPurgeCommunityView) => ({ admin_purge_community }: AdminPurgeCommunityView) =>
admin_purge_community admin_purge_community,
) ),
) ),
) )
.concat( .concat(
admin_purged_posts.map( admin_purged_posts.map(
getModlogActionMapper( getModlogActionMapper(
"AdminPurgePost", "AdminPurgePost",
({ admin_purge_post }: AdminPurgePostView) => admin_purge_post ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post,
) ),
) ),
) )
.concat( .concat(
admin_purged_comments.map( admin_purged_comments.map(
getModlogActionMapper( getModlogActionMapper(
"AdminPurgeComment", "AdminPurgeComment",
({ admin_purge_comment }: AdminPurgeCommentView) => ({ admin_purge_comment }: AdminPurgeCommentView) =>
admin_purge_comment admin_purge_comment,
) ),
) ),
); );
// Sort them by time // Sort them by time
@ -312,6 +316,7 @@ function renderModlogType({ type_, view }: ModlogType) {
const { const {
mod_feature_post: { featured, is_featured_community }, mod_feature_post: { featured, is_featured_community },
post: { id, name }, post: { id, name },
community,
} = view as ModFeaturePostView; } = view as ModFeaturePostView;
return ( return (
@ -320,7 +325,12 @@ function renderModlogType({ type_, view }: ModlogType) {
<span> <span>
Post <Link to={`/post/${id}`}>{name}</Link> Post <Link to={`/post/${id}`}>{name}</Link>
</span> </span>
<span>{is_featured_community ? " In Community" : " In Local"}</span> <span>
{is_featured_community
? " in community "
: " in Local, from community "}
</span>
<CommunityLink community={community} />
</> </>
); );
} }
@ -354,7 +364,7 @@ function renderModlogType({ type_, view }: ModlogType) {
case "ModRemoveCommunity": { case "ModRemoveCommunity": {
const mrco = view as ModRemoveCommunityView; const mrco = view as ModRemoveCommunityView;
const { const {
mod_remove_community: { reason, expires, removed }, mod_remove_community: { reason, removed },
community, community,
} = mrco; } = mrco;
@ -369,11 +379,6 @@ function renderModlogType({ type_, view }: ModlogType) {
<div>reason: {reason}</div> <div>reason: {reason}</div>
</span> </span>
)} )}
{expires && (
<span>
<div>expires: {formatPastDate(expires)}</div>
</span>
)}
</> </>
); );
} }
@ -532,7 +537,7 @@ function renderModlogType({ type_, view }: ModlogType) {
return ( return (
<> <>
<span>Purged a Post from from </span> <span>Purged a Post from </span>
<CommunityLink community={community} /> <CommunityLink community={community} />
{reason && ( {reason && (
<span> <span>
@ -616,7 +621,7 @@ async function createNewOptions({
if (id) { if (id) {
const selectedUser = oldOptions.find( const selectedUser = oldOptions.find(
({ value }) => value === id.toString() ({ value }) => value === id.toString(),
); );
if (selectedUser) { if (selectedUser) {
@ -628,7 +633,7 @@ async function createNewOptions({
newOptions.push( newOptions.push(
...(await fetchUsers(text)) ...(await fetchUsers(text))
.slice(0, Number(fetchLimit)) .slice(0, Number(fetchLimit))
.map<Choice>(personToChoice) .map<Choice>(personToChoice),
); );
} }
@ -642,8 +647,8 @@ export class Modlog extends Component<
private isoData = setIsoData<ModlogData>(this.context); private isoData = setIsoData<ModlogData>(this.context);
state: ModlogState = { state: ModlogState = {
res: { state: "empty" }, res: EMPTY_REQUEST,
communityRes: { state: "empty" }, communityRes: EMPTY_REQUEST,
loadingModSearch: false, loadingModSearch: false,
loadingUserSearch: false, loadingUserSearch: false,
userSearchOptions: [], userSearchOptions: [],
@ -652,7 +657,7 @@ export class Modlog extends Component<
constructor( constructor(
props: RouteComponentProps<{ communityId?: string }>, props: RouteComponentProps<{ communityId?: string }>,
context: any context: any,
) { ) {
super(props, context); super(props, context);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
@ -692,7 +697,7 @@ export class Modlog extends Component<
get combined() { get combined() {
const res = this.state.res; const res = this.state.res;
const combined = res.state == "success" ? buildCombined(res.data) : []; const combined = res.state === "success" ? buildCombined(res.data) : [];
return ( return (
<tbody> <tbody>
@ -717,7 +722,7 @@ export class Modlog extends Component<
get amAdminOrMod(): boolean { get amAdminOrMod(): boolean {
const amMod_ = const amMod_ =
this.state.communityRes.state == "success" && this.state.communityRes.state === "success" &&
amMod(this.state.communityRes.data.moderators); amMod(this.state.communityRes.data.moderators);
return amAdmin() || amMod_; return amAdmin() || amMod_;
} }
@ -725,7 +730,7 @@ export class Modlog extends Component<
modOrAdminText(person?: Person): string { modOrAdminText(person?: Person): string {
return person && return person &&
this.isoData.site_res.admins.some( this.isoData.site_res.admins.some(
({ person: { id } }) => id === person.id ({ person: { id } }) => id === person.id,
) )
? I18NextService.i18n.t("admin") ? I18NextService.i18n.t("admin")
: I18NextService.i18n.t("mod"); : I18NextService.i18n.t("mod");
@ -854,7 +859,11 @@ export class Modlog extends Component<
</thead> </thead>
{this.combined} {this.combined}
</table> </table>
<Paginator page={page} onChange={this.handlePageChange} /> <Paginator
page={page}
onChange={this.handlePageChange}
nextDisabled={false}
/>
</div> </div>
); );
} }
@ -933,20 +942,19 @@ export class Modlog extends Component<
this.props.history.push( this.props.history.push(
`/modlog${communityId ? `/${communityId}` : ""}${getQueryString( `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
queryParams queryParams,
)}` )}`,
); );
await this.refetch(); await this.refetch();
} }
async refetch() { async refetch() {
const auth = myAuth();
const { actionType, page, modId, userId } = getModlogQueryParams(); const { actionType, page, modId, userId } = getModlogQueryParams();
const { communityId: urlCommunityId } = this.props.match.params; const { communityId: urlCommunityId } = this.props.match.params;
const communityId = getIdFromString(urlCommunityId); const communityId = getIdFromString(urlCommunityId);
this.setState({ res: { state: "loading" } }); this.setState({ res: LOADING_REQUEST });
this.setState({ this.setState({
res: await HttpService.client.getModlog({ res: await HttpService.client.getModlog({
community_id: communityId, community_id: communityId,
@ -958,16 +966,14 @@ export class Modlog extends Component<
.hide_modlog_mod_names .hide_modlog_mod_names
? modId ?? undefined ? modId ?? undefined
: undefined, : undefined,
auth,
}), }),
}); });
if (communityId) { if (communityId) {
this.setState({ communityRes: { state: "loading" } }); this.setState({ communityRes: LOADING_REQUEST });
this.setState({ this.setState({
communityRes: await HttpService.client.getCommunity({ communityRes: await HttpService.client.getCommunity({
id: communityId, id: communityId,
auth,
}), }),
}); });
} }
@ -977,7 +983,6 @@ export class Modlog extends Component<
client, client,
path, path,
query: { modId: urlModId, page, userId: urlUserId, actionType }, query: { modId: urlModId, page, userId: urlUserId, actionType },
auth,
site, site,
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> { }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
@ -994,43 +999,33 @@ export class Modlog extends Component<
type_: getActionFromString(actionType), type_: getActionFromString(actionType),
mod_person_id: modId, mod_person_id: modId,
other_person_id: userId, other_person_id: userId,
auth,
}; };
let communityResponse: RequestState<GetCommunityResponse> = { let communityResponse: RequestState<GetCommunityResponse> = EMPTY_REQUEST;
state: "empty",
};
if (communityId) { if (communityId) {
const communityForm: GetCommunity = { const communityForm: GetCommunity = {
id: communityId, id: communityId,
auth,
}; };
communityResponse = await client.getCommunity(communityForm); communityResponse = await client.getCommunity(communityForm);
} }
let modUserResponse: RequestState<GetPersonDetailsResponse> = { let modUserResponse: RequestState<GetPersonDetailsResponse> = EMPTY_REQUEST;
state: "empty",
};
if (modId) { if (modId) {
const getPersonForm: GetPersonDetails = { const getPersonForm: GetPersonDetails = {
person_id: modId, person_id: modId,
auth,
}; };
modUserResponse = await client.getPersonDetails(getPersonForm); modUserResponse = await client.getPersonDetails(getPersonForm);
} }
let userResponse: RequestState<GetPersonDetailsResponse> = { let userResponse: RequestState<GetPersonDetailsResponse> = EMPTY_REQUEST;
state: "empty",
};
if (userId) { if (userId) {
const getPersonForm: GetPersonDetails = { const getPersonForm: GetPersonDetails = {
person_id: userId, person_id: userId,
auth,
}; };
userResponse = await client.getPersonDetails(getPersonForm); userResponse = await client.getPersonDetails(getPersonForm);

View file

@ -7,7 +7,6 @@ import {
enableDownvotes, enableDownvotes,
getCommentParentId, getCommentParentId,
myAuth, myAuth,
myAuthRequired,
setIsoData, setIsoData,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
@ -63,7 +62,14 @@ import {
import { fetchLimit, relTags } from "../../config"; import { fetchLimit, relTags } from "../../config";
import { CommentViewType, InitialFetchRequest } from "../../interfaces"; import { CommentViewType, InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService, UserService } from "../../services"; import { FirstLoadService, I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import { UnreadCounterService } from "../../services";
import {
EMPTY_REQUEST,
EmptyRequestState,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { CommentSortSelect } from "../common/comment-sort-select"; import { CommentSortSelect } from "../common/comment-sort-select";
@ -125,10 +131,10 @@ export class Inbox extends Component<any, InboxState> {
sort: "New", sort: "New",
page: 1, page: 1,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
repliesRes: { state: "empty" }, repliesRes: EMPTY_REQUEST,
mentionsRes: { state: "empty" }, mentionsRes: EMPTY_REQUEST,
messagesRes: { state: "empty" }, messagesRes: EMPTY_REQUEST,
markAllAsReadRes: { state: "empty" }, markAllAsReadRes: EMPTY_REQUEST,
finished: new Map(), finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
@ -188,20 +194,20 @@ export class Inbox extends Component<any, InboxState> {
const mui = UserService.Instance.myUserInfo; const mui = UserService.Instance.myUserInfo;
return mui return mui
? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t( ? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t(
"inbox" "inbox",
)} - ${this.state.siteRes.site_view.site.name}` )} - ${this.state.siteRes.site_view.site.name}`
: ""; : "";
} }
get hasUnreads(): boolean { get hasUnreads(): boolean {
if (this.state.unreadOrAll == UnreadOrAll.Unread) { if (this.state.unreadOrAll === UnreadOrAll.Unread) {
const { repliesRes, mentionsRes, messagesRes } = this.state; const { repliesRes, mentionsRes, messagesRes } = this.state;
const replyCount = const replyCount =
repliesRes.state == "success" ? repliesRes.data.replies.length : 0; repliesRes.state === "success" ? repliesRes.data.replies.length : 0;
const mentionCount = const mentionCount =
mentionsRes.state == "success" ? mentionsRes.data.mentions.length : 0; mentionsRes.state === "success" ? mentionsRes.data.mentions.length : 0;
const messageCount = const messageCount =
messagesRes.state == "success" messagesRes.state === "success"
? messagesRes.data.private_messages.length ? messagesRes.data.private_messages.length
: 0; : 0;
@ -242,11 +248,11 @@ export class Inbox extends Component<any, InboxState> {
className="btn btn-secondary mb-2 mb-sm-3" className="btn btn-secondary mb-2 mb-sm-3"
onClick={linkEvent(this, this.handleMarkAllAsRead)} onClick={linkEvent(this, this.handleMarkAllAsRead)}
> >
{this.state.markAllAsReadRes.state == "loading" ? ( {this.state.markAllAsReadRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
capitalizeFirstLetter( capitalizeFirstLetter(
I18NextService.i18n.t("mark_all_as_read") I18NextService.i18n.t("mark_all_as_read"),
) )
)} )}
</button> </button>
@ -256,6 +262,7 @@ export class Inbox extends Component<any, InboxState> {
<Paginator <Paginator
page={this.state.page} page={this.state.page}
onChange={this.handlePageChange} onChange={this.handlePageChange}
nextDisabled={false}
/> />
</div> </div>
</div> </div>
@ -445,22 +452,22 @@ export class Inbox extends Component<any, InboxState> {
buildCombined(): ReplyType[] { buildCombined(): ReplyType[] {
const replies: ReplyType[] = const replies: ReplyType[] =
this.state.repliesRes.state == "success" this.state.repliesRes.state === "success"
? this.state.repliesRes.data.replies.map(this.replyToReplyType) ? this.state.repliesRes.data.replies.map(this.replyToReplyType)
: []; : [];
const mentions: ReplyType[] = const mentions: ReplyType[] =
this.state.mentionsRes.state == "success" this.state.mentionsRes.state === "success"
? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType) ? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType)
: []; : [];
const messages: ReplyType[] = const messages: ReplyType[] =
this.state.messagesRes.state == "success" this.state.messagesRes.state === "success"
? this.state.messagesRes.data.private_messages.map( ? this.state.messagesRes.data.private_messages.map(
this.messageToReplyType this.messageToReplyType,
) )
: []; : [];
return [...replies, ...mentions, ...messages].sort((a, b) => return [...replies, ...mentions, ...messages].sort((a, b) =>
b.published.localeCompare(a.published) b.published.localeCompare(a.published),
); );
} }
@ -559,9 +566,9 @@ export class Inbox extends Component<any, InboxState> {
all() { all() {
if ( if (
this.state.repliesRes.state == "loading" || this.state.repliesRes.state === "loading" ||
this.state.mentionsRes.state == "loading" || this.state.mentionsRes.state === "loading" ||
this.state.messagesRes.state == "loading" this.state.messagesRes.state === "loading"
) { ) {
return ( return (
<h1 className="h4"> <h1 className="h4">
@ -718,78 +725,77 @@ export class Inbox extends Component<any, InboxState> {
static async fetchInitialData({ static async fetchInitialData({
client, client,
auth,
}: InitialFetchRequest): Promise<InboxData> { }: InitialFetchRequest): Promise<InboxData> {
const sort: CommentSortType = "New"; const sort: CommentSortType = "New";
const empty: EmptyRequestState = EMPTY_REQUEST;
return { let inboxData: InboxData = {
mentionsRes: auth mentionsRes: empty,
? await client.getPersonMentions({ messagesRes: empty,
sort, repliesRes: empty,
unread_only: true,
page: 1,
limit: fetchLimit,
auth,
})
: { state: "empty" },
messagesRes: auth
? await client.getPrivateMessages({
unread_only: true,
page: 1,
limit: fetchLimit,
auth,
})
: { state: "empty" },
repliesRes: auth
? await client.getReplies({
sort,
unread_only: true,
page: 1,
limit: fetchLimit,
auth,
})
: { state: "empty" },
}; };
if (myAuth()) {
const [mentionsRes, messagesRes, repliesRes] = await Promise.all([
client.getPersonMentions({
sort,
unread_only: true,
page: 1,
limit: fetchLimit,
}),
client.getPrivateMessages({
unread_only: true,
page: 1,
limit: fetchLimit,
}),
client.getReplies({
sort,
unread_only: true,
page: 1,
limit: fetchLimit,
}),
]);
inboxData = { mentionsRes, messagesRes, repliesRes };
}
return inboxData;
} }
async refetch() { async refetch() {
const sort = this.state.sort; const sort = this.state.sort;
const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread; const unread_only = this.state.unreadOrAll === UnreadOrAll.Unread;
const page = this.state.page; const page = this.state.page;
const limit = fetchLimit; const limit = fetchLimit;
const auth = myAuthRequired();
this.setState({ repliesRes: { state: "loading" } }); this.setState({ repliesRes: LOADING_REQUEST });
this.setState({ this.setState({
repliesRes: await HttpService.client.getReplies({ repliesRes: await HttpService.client.getReplies({
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
auth,
}), }),
}); });
this.setState({ mentionsRes: { state: "loading" } }); this.setState({ mentionsRes: LOADING_REQUEST });
this.setState({ this.setState({
mentionsRes: await HttpService.client.getPersonMentions({ mentionsRes: await HttpService.client.getPersonMentions({
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
auth,
}), }),
}); });
this.setState({ messagesRes: { state: "loading" } }); this.setState({ messagesRes: LOADING_REQUEST });
this.setState({ this.setState({
messagesRes: await HttpService.client.getPrivateMessages({ messagesRes: await HttpService.client.getPrivateMessages({
unread_only, unread_only,
page, page,
limit, limit,
auth,
}), }),
}); });
UnreadCounterService.Instance.updateInboxCounts();
} }
async handleSortChange(val: CommentSortType) { async handleSortChange(val: CommentSortType) {
@ -798,19 +804,17 @@ export class Inbox extends Component<any, InboxState> {
} }
async handleMarkAllAsRead(i: Inbox) { async handleMarkAllAsRead(i: Inbox) {
i.setState({ markAllAsReadRes: { state: "loading" } }); i.setState({ markAllAsReadRes: LOADING_REQUEST });
i.setState({ i.setState({
markAllAsReadRes: await HttpService.client.markAllAsRead({ markAllAsReadRes: await HttpService.client.markAllAsRead(),
auth: myAuthRequired(),
}),
}); });
if (i.state.markAllAsReadRes.state == "success") { if (i.state.markAllAsReadRes.state === "success") {
i.setState({ i.setState({
repliesRes: { state: "empty" }, repliesRes: EMPTY_REQUEST,
mentionsRes: { state: "empty" }, mentionsRes: EMPTY_REQUEST,
messagesRes: { state: "empty" }, messagesRes: EMPTY_REQUEST,
}); });
} }
} }
@ -837,7 +841,7 @@ export class Inbox extends Component<any, InboxState> {
async handleBlockPerson(form: BlockPerson) { async handleBlockPerson(form: BlockPerson) {
const blockPersonRes = await HttpService.client.blockPerson(form); const blockPersonRes = await HttpService.client.blockPerson(form);
if (blockPersonRes.state == "success") { if (blockPersonRes.state === "success") {
updatePersonBlock(blockPersonRes.data); updatePersonBlock(blockPersonRes.data);
} }
} }
@ -868,7 +872,7 @@ export class Inbox extends Component<any, InboxState> {
async handleDeleteComment(form: DeleteComment) { async handleDeleteComment(form: DeleteComment) {
const res = await HttpService.client.deleteComment(form); const res = await HttpService.client.deleteComment(form);
if (res.state == "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("deleted")); toast(I18NextService.i18n.t("deleted"));
this.findAndUpdateComment(res); this.findAndUpdateComment(res);
} }
@ -876,7 +880,7 @@ export class Inbox extends Component<any, InboxState> {
async handleRemoveComment(form: RemoveComment) { async handleRemoveComment(form: RemoveComment) {
const res = await HttpService.client.removeComment(form); const res = await HttpService.client.removeComment(form);
if (res.state == "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("remove_comment")); toast(I18NextService.i18n.t("remove_comment"));
this.findAndUpdateComment(res); this.findAndUpdateComment(res);
} }
@ -917,12 +921,20 @@ export class Inbox extends Component<any, InboxState> {
async handleCommentReplyRead(form: MarkCommentReplyAsRead) { async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
const res = await HttpService.client.markCommentReplyAsRead(form); const res = await HttpService.client.markCommentReplyAsRead(form);
this.findAndUpdateCommentReply(res); if (this.state.unreadOrAll === UnreadOrAll.All) {
this.findAndUpdateCommentReply(res);
} else {
await this.refetch();
}
} }
async handlePersonMentionRead(form: MarkPersonMentionAsRead) { async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
const res = await HttpService.client.markPersonMentionAsRead(form); const res = await HttpService.client.markPersonMentionAsRead(form);
this.findAndUpdateMention(res); if (this.state.unreadOrAll === UnreadOrAll.All) {
this.findAndUpdateMention(res);
} else {
await this.refetch();
}
} }
async handleBanFromCommunity(form: BanFromCommunity) { async handleBanFromCommunity(form: BanFromCommunity) {
@ -947,7 +959,11 @@ export class Inbox extends Component<any, InboxState> {
async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) { async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
const res = await HttpService.client.markPrivateMessageAsRead(form); const res = await HttpService.client.markPrivateMessageAsRead(form);
this.findAndUpdateMessage(res); if (this.state.unreadOrAll === UnreadOrAll.All) {
this.findAndUpdateMessage(res);
} else {
await this.refetch();
}
} }
async handleMessageReport(form: CreatePrivateMessageReport) { async handleMessageReport(form: CreatePrivateMessageReport) {
@ -958,9 +974,9 @@ export class Inbox extends Component<any, InboxState> {
async handleCreateMessage(form: CreatePrivateMessage) { async handleCreateMessage(form: CreatePrivateMessage) {
const res = await HttpService.client.createPrivateMessage(form); const res = await HttpService.client.createPrivateMessage(form);
this.setState(s => { this.setState(s => {
if (s.messagesRes.state == "success" && res.state == "success") { if (s.messagesRes.state === "success" && res.state === "success") {
s.messagesRes.data.private_messages.unshift( s.messagesRes.data.private_messages.unshift(
res.data.private_message_view res.data.private_message_view,
); );
} }
@ -973,7 +989,7 @@ export class Inbox extends Component<any, InboxState> {
if (s.messagesRes.state === "success" && res.state === "success") { if (s.messagesRes.state === "success" && res.state === "success") {
s.messagesRes.data.private_messages = editPrivateMessage( s.messagesRes.data.private_messages = editPrivateMessage(
res.data.private_message_view, res.data.private_message_view,
s.messagesRes.data.private_messages s.messagesRes.data.private_messages,
); );
} }
return s; return s;
@ -982,20 +998,20 @@ export class Inbox extends Component<any, InboxState> {
updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) { updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.repliesRes.state == "success") { if (s.repliesRes.state === "success") {
s.repliesRes.data.replies s.repliesRes.data.replies
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
if (s.mentionsRes.state == "success") { if (s.mentionsRes.state === "success") {
s.mentionsRes.data.mentions s.mentionsRes.data.mentions
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
return s; return s;
@ -1005,16 +1021,16 @@ export class Inbox extends Component<any, InboxState> {
updateBan(banRes: RequestState<BanPersonResponse>) { updateBan(banRes: RequestState<BanPersonResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.repliesRes.state == "success") { if (s.repliesRes.state === "success") {
s.repliesRes.data.replies s.repliesRes.data.replies
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
if (s.mentionsRes.state == "success") { if (s.mentionsRes.state === "success") {
s.mentionsRes.data.mentions s.mentionsRes.data.mentions
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
} }
return s; return s;
@ -1023,40 +1039,40 @@ export class Inbox extends Component<any, InboxState> {
} }
purgeItem(purgeRes: RequestState<PurgeItemResponse>) { purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
if (purgeRes.state == "success") { if (purgeRes.state === "success") {
toast(I18NextService.i18n.t("purge_success")); toast(I18NextService.i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} }
reportToast( reportToast(
res: RequestState<PrivateMessageReportResponse | CommentReportResponse> res: RequestState<PrivateMessageReportResponse | CommentReportResponse>,
) { ) {
if (res.state == "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} }
} }
// A weird case, since you have only replies and mentions, not comment responses // A weird case, since you have only replies and mentions, not comment responses
findAndUpdateComment(res: RequestState<CommentResponse>) { findAndUpdateComment(res: RequestState<CommentResponse>) {
if (res.state == "success") { if (res.state === "success") {
this.setState(s => { this.setState(s => {
if (s.repliesRes.state == "success") { if (s.repliesRes.state === "success") {
s.repliesRes.data.replies = editWith( s.repliesRes.data.replies = editWith(
res.data.comment_view, res.data.comment_view,
s.repliesRes.data.replies s.repliesRes.data.replies,
); );
} }
if (s.mentionsRes.state == "success") { if (s.mentionsRes.state === "success") {
s.mentionsRes.data.mentions = editWith( s.mentionsRes.data.mentions = editWith(
res.data.comment_view, res.data.comment_view,
s.mentionsRes.data.mentions s.mentionsRes.data.mentions,
); );
} }
// Set finished for the parent // Set finished for the parent
s.finished.set( s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0, getCommentParentId(res.data.comment_view.comment) ?? 0,
true true,
); );
return s; return s;
}); });
@ -1065,10 +1081,10 @@ export class Inbox extends Component<any, InboxState> {
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) { findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
this.setState(s => { this.setState(s => {
if (s.repliesRes.state == "success" && res.state == "success") { if (s.repliesRes.state === "success" && res.state === "success") {
s.repliesRes.data.replies = editCommentReply( s.repliesRes.data.replies = editCommentReply(
res.data.comment_reply_view, res.data.comment_reply_view,
s.repliesRes.data.replies s.repliesRes.data.replies,
); );
} }
return s; return s;
@ -1077,10 +1093,10 @@ export class Inbox extends Component<any, InboxState> {
findAndUpdateMention(res: RequestState<PersonMentionResponse>) { findAndUpdateMention(res: RequestState<PersonMentionResponse>) {
this.setState(s => { this.setState(s => {
if (s.mentionsRes.state == "success" && res.state == "success") { if (s.mentionsRes.state === "success" && res.state === "success") {
s.mentionsRes.data.mentions = editMention( s.mentionsRes.data.mentions = editMention(
res.data.person_mention_view, res.data.person_mention_view,
s.mentionsRes.data.mentions s.mentionsRes.data.mentions,
); );
} }
return s; return s;

View file

@ -1,11 +1,16 @@
import { myAuth, setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client"; import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
import { HttpService, I18NextService, UserService } from "../../services"; import { HttpService, I18NextService, UserService } from "../../services";
import { RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
interface State { interface State {
passwordChangeRes: RequestState<LoginResponse>; passwordChangeRes: RequestState<LoginResponse>;
@ -21,7 +26,7 @@ export class PasswordChange extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
state: State = { state: State = {
passwordChangeRes: { state: "empty" }, passwordChangeRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
form: { form: {
token: this.props.match.params.token, token: this.props.match.params.token,
@ -60,42 +65,28 @@ export class PasswordChange extends Component<any, State> {
passwordChangeForm() { passwordChangeForm() {
return ( return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}> <form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
<div className="mb-3 row"> <div className="mb-3">
<label className="col-sm-2 col-form-label" htmlFor="new-password"> <PasswordInput
{I18NextService.i18n.t("new_password")} id="new-password"
</label> value={this.state.form.password}
<div className="col-sm-10"> onInput={linkEvent(this, this.handlePasswordChange)}
<input showStrength
id="new-password" label={I18NextService.i18n.t("new_password")}
type="password" isNew
value={this.state.form.password} />
onInput={linkEvent(this, this.handlePasswordChange)}
className="form-control"
required
maxLength={60}
/>
</div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3">
<label className="col-sm-2 col-form-label" htmlFor="verify-password"> <PasswordInput
{I18NextService.i18n.t("verify_password")} id="password"
</label> value={this.state.form.password_verify}
<div className="col-sm-10"> onInput={linkEvent(this, this.handleVerifyPasswordChange)}
<input label={I18NextService.i18n.t("verify_password")}
id="verify-password" />
type="password"
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
className="form-control"
required
maxLength={60}
/>
</div>
</div> </div>
<div className="mb-3 row"> <div className="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button type="submit" className="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.passwordChangeRes.state == "loading" ? ( {this.state.passwordChangeRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
capitalizeFirstLetter(I18NextService.i18n.t("save")) capitalizeFirstLetter(I18NextService.i18n.t("save"))
@ -119,7 +110,7 @@ export class PasswordChange extends Component<any, State> {
async handlePasswordChangeSubmit(i: PasswordChange, event: any) { async handlePasswordChangeSubmit(i: PasswordChange, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ passwordChangeRes: { state: "loading" } }); i.setState({ passwordChangeRes: LOADING_REQUEST });
const password = i.state.form.password; const password = i.state.form.password;
const password_verify = i.state.form.password_verify; const password_verify = i.state.form.password_verify;
@ -139,7 +130,7 @@ export class PasswordChange extends Component<any, State> {
res: data, res: data,
}); });
const site = await HttpService.client.getSite({ auth: myAuth() }); const site = await HttpService.client.getSite();
if (site.state === "success") { if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user; UserService.Instance.myUserInfo = site.data.my_user;
} }

View file

@ -25,6 +25,7 @@ import {
LockPost, LockPost,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
MarkPostAsRead,
PersonView, PersonView,
PostView, PostView,
PurgeComment, PurgeComment,
@ -84,6 +85,7 @@ interface PersonDetailsProps {
onSavePost(form: SavePost): void; onSavePost(form: SavePost): void;
onFeaturePost(form: FeaturePost): void; onFeaturePost(form: FeaturePost): void;
onPurgePost(form: PurgePost): void; onPurgePost(form: PurgePost): void;
onMarkPostAsRead(form: MarkPostAsRead): void;
} }
enum ItemEnum { enum ItemEnum {
@ -113,7 +115,16 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<div className="person-details"> <div className="person-details">
{this.viewSelector(this.props.view)} {this.viewSelector(this.props.view)}
<Paginator page={this.props.page} onChange={this.handlePageChange} /> <Paginator
page={this.props.page}
onChange={this.handlePageChange}
nextDisabled={
(this.props.view === PersonDetailsView.Comments &&
this.props.limit > this.props.personRes.comments.length) ||
(this.props.view === PersonDetailsView.Posts &&
this.props.limit > this.props.personRes.posts.length)
}
/>
</div> </div>
); );
} }
@ -200,6 +211,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
onAddModToCommunity={this.props.onAddModToCommunity} onAddModToCommunity={this.props.onAddModToCommunity}
onAddAdmin={this.props.onAddAdmin} onAddAdmin={this.props.onAddAdmin}
onTransferCommunity={this.props.onTransferCommunity} onTransferCommunity={this.props.onTransferCommunity}
onMarkPostAsRead={this.props.onMarkPostAsRead}
/> />
); );
} }
@ -252,7 +264,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
admins={this.props.admins} admins={this.props.admins}
finished={this.props.finished} finished={this.props.finished}
noIndent isTopLevel
showCommunity showCommunity
showContext showContext
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
@ -311,6 +323,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
onAddModToCommunity={this.props.onAddModToCommunity} onAddModToCommunity={this.props.onAddModToCommunity}
onAddAdmin={this.props.onAddAdmin} onAddAdmin={this.props.onAddAdmin}
onTransferCommunity={this.props.onTransferCommunity} onTransferCommunity={this.props.onTransferCommunity}
onMarkPostAsRead={this.props.onMarkPostAsRead}
/> />
<hr className="my-3" /> <hr className="my-3" />
</> </>

View file

@ -57,7 +57,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
{ {
"text-muted": this.props.muted, "text-muted": this.props.muted,
"text-info": !this.props.muted, "text-info": !this.props.muted,
} },
)} )}
to={link} to={link}
> >

View file

@ -5,8 +5,6 @@ import {
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getCommentParentId, getCommentParentId,
myAuth,
myAuthRequired,
setIsoData, setIsoData,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
@ -60,6 +58,7 @@ import {
LockPost, LockPost,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
MarkPostAsRead,
PersonView, PersonView,
PostResponse, PostResponse,
PurgeComment, PurgeComment,
@ -77,7 +76,12 @@ import { fetchLimit, relTags } from "../../config";
import { InitialFetchRequest, PersonDetailsView } from "../../interfaces"; import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
import { mdToHtml } from "../../markdown"; import { mdToHtml } from "../../markdown";
import { FirstLoadService, I18NextService, UserService } from "../../services"; import { FirstLoadService, I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
@ -132,7 +136,7 @@ function getViewFromProps(view?: string): PersonDetailsView {
const getCommunitiesListing = ( const getCommunitiesListing = (
translationKey: NoOptionI18nKeys, translationKey: NoOptionI18nKeys,
communityViews?: { community: Community }[] communityViews?: { community: Community }[],
) => ) =>
communityViews && communityViews &&
communityViews.length > 0 && ( communityViews.length > 0 && (
@ -156,13 +160,23 @@ const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
const Follows = () => const Follows = () =>
getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows); getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
function isPersonBlocked(personRes: RequestState<GetPersonDetailsResponse>) {
return (
(personRes.state === "success" &&
UserService.Instance.myUserInfo?.person_blocks.some(
({ target: { id } }) => id === personRes.data.person_view.person.id,
)) ??
false
);
}
export class Profile extends Component< export class Profile extends Component<
RouteComponentProps<{ username: string }>, RouteComponentProps<{ username: string }>,
ProfileState ProfileState
> { > {
private isoData = setIsoData<ProfileData>(this.context); private isoData = setIsoData<ProfileData>(this.context);
state: ProfileState = { state: ProfileState = {
personRes: { state: "empty" }, personRes: EMPTY_REQUEST,
personBlocked: false, personBlocked: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showBanDialog: false, showBanDialog: false,
@ -208,13 +222,16 @@ export class Profile extends Component<
this.handlePurgePost = this.handlePurgePost.bind(this); this.handlePurgePost = this.handlePurgePost.bind(this);
this.handleFeaturePost = this.handleFeaturePost.bind(this); this.handleFeaturePost = this.handleFeaturePost.bind(this);
this.handleModBanSubmit = this.handleModBanSubmit.bind(this); this.handleModBanSubmit = this.handleModBanSubmit.bind(this);
this.handleMarkPostAsRead = this.handleMarkPostAsRead.bind(this);
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const personRes = this.isoData.routeData.personResponse;
this.state = { this.state = {
...this.state, ...this.state,
personRes: this.isoData.routeData.personResponse, personRes,
isIsomorphic: true, isIsomorphic: true,
personBlocked: isPersonBlocked(personRes),
}; };
} }
} }
@ -233,19 +250,19 @@ export class Profile extends Component<
async fetchUserData() { async fetchUserData() {
const { page, sort, view } = getProfileQueryParams(); const { page, sort, view } = getProfileQueryParams();
this.setState({ personRes: { state: "loading" } }); this.setState({ personRes: LOADING_REQUEST });
const personRes = await HttpService.client.getPersonDetails({
username: this.props.match.params.username,
sort,
saved_only: view === PersonDetailsView.Saved,
page,
limit: fetchLimit,
});
this.setState({ this.setState({
personRes: await HttpService.client.getPersonDetails({ personRes,
username: this.props.match.params.username, personBlocked: isPersonBlocked(personRes),
sort,
saved_only: view === PersonDetailsView.Saved,
page,
limit: fetchLimit,
auth: myAuth(),
}),
}); });
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
this.setPersonBlock();
} }
get amCurrentUser() { get amCurrentUser() {
@ -259,24 +276,10 @@ export class Profile extends Component<
} }
} }
setPersonBlock() {
const mui = UserService.Instance.myUserInfo;
const res = this.state.personRes;
if (mui && res.state === "success") {
this.setState({
personBlocked: mui.person_blocks.some(
({ target: { id } }) => id === res.data.person_view.person.id
),
});
}
}
static async fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
query: { page, sort, view: urlView }, query: { page, sort, view: urlView },
auth,
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> { }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
@ -289,7 +292,6 @@ export class Profile extends Component<
saved_only: view === PersonDetailsView.Saved, saved_only: view === PersonDetailsView.Saved,
page: getPageFromString(page), page: getPageFromString(page),
limit: fetchLimit, limit: fetchLimit,
auth,
}; };
return { return {
@ -300,7 +302,7 @@ export class Profile extends Component<
get documentTitle(): string { get documentTitle(): string {
const siteName = this.state.siteRes.site_view.site.name; const siteName = this.state.siteRes.site_view.site.name;
const res = this.state.personRes; const res = this.state.personRes;
return res.state == "success" return res.state === "success"
? `@${res.data.person_view.person.name} - ${siteName}` ? `@${res.data.person_view.person.name} - ${siteName}`
: siteName; : siteName;
} }
@ -324,6 +326,7 @@ export class Profile extends Component<
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
canonicalPath={personRes.person_view.person.actor_id}
description={personRes.person_view.person.bio} description={personRes.person_view.person.bio}
image={personRes.person_view.person.avatar} image={personRes.person_view.person.avatar}
/> />
@ -375,6 +378,7 @@ export class Profile extends Component<
onSavePost={this.handleSavePost} onSavePost={this.handleSavePost}
onPurgePost={this.handlePurgePost} onPurgePost={this.handlePurgePost}
onFeaturePost={this.handleFeaturePost} onFeaturePost={this.handleFeaturePost}
onMarkPostAsRead={this.handleMarkPostAsRead}
/> />
</div> </div>
@ -495,7 +499,7 @@ export class Profile extends Component<
classNames="ms-1" classNames="ms-1"
isBanned={isBanned(pv.person)} isBanned={isBanned(pv.person)}
isDeleted={pv.person.deleted} isDeleted={pv.person.deleted}
isAdmin={pv.person.admin} isAdmin={isAdmin(pv.person.id, admins)}
isBot={pv.person.bot_account} isBot={pv.person.bot_account}
/> />
</li> </li>
@ -529,7 +533,7 @@ export class Profile extends Component<
} }
onClick={linkEvent( onClick={linkEvent(
pv.person.id, pv.person.id,
this.handleUnblockPerson this.handleUnblockPerson,
)} )}
> >
{I18NextService.i18n.t("unblock_user")} {I18NextService.i18n.t("unblock_user")}
@ -541,7 +545,7 @@ export class Profile extends Component<
} }
onClick={linkEvent( onClick={linkEvent(
pv.person.id, pv.person.id,
this.handleBlockPerson this.handleBlockPerson,
)} )}
> >
{I18NextService.i18n.t("block_user")} {I18NextService.i18n.t("block_user")}
@ -763,7 +767,7 @@ export class Profile extends Component<
const personRes = i.state.personRes; const personRes = i.state.personRes;
if (personRes.state == "success") { if (personRes.state === "success") {
const person = personRes.data.person_view.person; const person = personRes.data.person_view.person;
const ban = !person.banned; const ban = !person.banned;
@ -778,7 +782,6 @@ export class Profile extends Component<
remove_data: removeData, remove_data: removeData,
reason: banReason, reason: banReason,
expires: futureDaysToUnixTime(banExpireDays), expires: futureDaysToUnixTime(banExpireDays),
auth: myAuthRequired(),
}); });
// TODO // TODO
this.updateBan(res); this.updateBan(res);
@ -790,10 +793,10 @@ export class Profile extends Component<
const res = await HttpService.client.blockPerson({ const res = await HttpService.client.blockPerson({
person_id: recipientId, person_id: recipientId,
block, block,
auth: myAuthRequired(),
}); });
if (res.state == "success") { if (res.state === "success") {
updatePersonBlock(res.data); updatePersonBlock(res.data);
this.setState({ personBlocked: res.data.blocked });
} }
} }
@ -829,6 +832,7 @@ export class Profile extends Component<
const blockPersonRes = await HttpService.client.blockPerson(form); const blockPersonRes = await HttpService.client.blockPerson(form);
if (blockPersonRes.state === "success") { if (blockPersonRes.state === "success") {
updatePersonBlock(blockPersonRes.data); updatePersonBlock(blockPersonRes.data);
this.setState({ personBlocked: blockPersonRes.data.blocked });
} }
} }
@ -841,7 +845,7 @@ export class Profile extends Component<
async handleEditComment(form: EditComment) { async handleEditComment(form: EditComment) {
const editCommentRes = await HttpService.client.editComment(form); const editCommentRes = await HttpService.client.editComment(form);
this.findAndUpdateComment(editCommentRes); this.findAndUpdateCommentEdit(editCommentRes);
return editCommentRes; return editCommentRes;
} }
@ -923,7 +927,7 @@ export class Profile extends Component<
async handleAddAdmin(form: AddAdmin) { async handleAddAdmin(form: AddAdmin) {
const addAdminRes = await HttpService.client.addAdmin(form); const addAdminRes = await HttpService.client.addAdmin(form);
if (addAdminRes.state == "success") { if (addAdminRes.state === "success") {
this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s)); this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
} }
} }
@ -943,6 +947,11 @@ export class Profile extends Component<
await HttpService.client.markPersonMentionAsRead(form); await HttpService.client.markPersonMentionAsRead(form);
} }
async handleMarkPostAsRead(form: MarkPostAsRead) {
const res = await HttpService.client.markPostAsRead(form);
this.findAndUpdatePost(res);
}
async handleBanFromCommunity(form: BanFromCommunity) { async handleBanFromCommunity(form: BanFromCommunity) {
const banRes = await HttpService.client.banFromCommunity(form); const banRes = await HttpService.client.banFromCommunity(form);
this.updateBanFromCommunity(banRes); this.updateBanFromCommunity(banRes);
@ -957,17 +966,17 @@ export class Profile extends Component<
// Maybe not necessary // Maybe not necessary
if (banRes.state === "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success") { if (s.personRes.state === "success") {
s.personRes.data.posts s.personRes.data.posts
.filter(c => c.creator.id === banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
s.personRes.data.comments s.personRes.data.comments
.filter(c => c.creator.id === banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach( .forEach(
c => (c.creator_banned_from_community = banRes.data.banned) c => (c.creator_banned_from_community = banRes.data.banned),
); );
} }
return s; return s;
@ -977,14 +986,14 @@ export class Profile extends Component<
updateBan(banRes: RequestState<BanPersonResponse>) { updateBan(banRes: RequestState<BanPersonResponse>) {
// Maybe not necessary // Maybe not necessary
if (banRes.state == "success") { if (banRes.state === "success") {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success") { if (s.personRes.state === "success") {
s.personRes.data.posts s.personRes.data.posts
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
s.personRes.data.comments s.personRes.data.comments
.filter(c => c.creator.id == banRes.data.person_view.person.id) .filter(c => c.creator.id === banRes.data.person_view.person.id)
.forEach(c => (c.creator.banned = banRes.data.banned)); .forEach(c => (c.creator.banned = banRes.data.banned));
s.personRes.data.person_view.person.banned = banRes.data.banned; s.personRes.data.person_view.person.banned = banRes.data.banned;
} }
@ -994,18 +1003,18 @@ export class Profile extends Component<
} }
purgeItem(purgeRes: RequestState<PurgeItemResponse>) { purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
if (purgeRes.state == "success") { if (purgeRes.state === "success") {
toast(I18NextService.i18n.t("purge_success")); toast(I18NextService.i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} }
findAndUpdateComment(res: RequestState<CommentResponse>) { findAndUpdateCommentEdit(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success" && res.state == "success") { if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.comments = editComment( s.personRes.data.comments = editComment(
res.data.comment_view, res.data.comment_view,
s.personRes.data.comments s.personRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true); s.finished.set(res.data.comment_view.comment.id, true);
} }
@ -1013,14 +1022,26 @@ export class Profile extends Component<
}); });
} }
findAndUpdateComment(res: RequestState<CommentResponse>) {
this.setState(s => {
if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.comments = editComment(
res.data.comment_view,
s.personRes.data.comments,
);
}
return s;
});
}
createAndUpdateComments(res: RequestState<CommentResponse>) { createAndUpdateComments(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success" && res.state == "success") { if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.comments.unshift(res.data.comment_view); s.personRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent // Set finished for the parent
s.finished.set( s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0, getCommentParentId(res.data.comment_view.comment) ?? 0,
true true,
); );
} }
return s; return s;
@ -1029,10 +1050,10 @@ export class Profile extends Component<
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) { findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success" && res.state == "success") { if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.comments = editWith( s.personRes.data.comments = editWith(
res.data.comment_reply_view, res.data.comment_reply_view,
s.personRes.data.comments s.personRes.data.comments,
); );
} }
return s; return s;
@ -1041,10 +1062,10 @@ export class Profile extends Component<
findAndUpdatePost(res: RequestState<PostResponse>) { findAndUpdatePost(res: RequestState<PostResponse>) {
this.setState(s => { this.setState(s => {
if (s.personRes.state == "success" && res.state == "success") { if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.posts = editPost( s.personRes.data.posts = editPost(
res.data.post_view, res.data.post_view,
s.personRes.data.posts s.personRes.data.posts,
); );
} }
return s; return s;

View file

@ -1,8 +1,4 @@
import { import { editRegistrationApplication, myAuth, setIsoData } from "@utils/app";
editRegistrationApplication,
myAuthRequired,
setIsoData,
} from "@utils/app";
import { randomStr } from "@utils/helpers"; import { randomStr } from "@utils/helpers";
import { RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import classNames from "classnames"; import classNames from "classnames";
@ -16,12 +12,18 @@ import {
import { fetchLimit } from "../../config"; import { fetchLimit } from "../../config";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService, UserService } from "../../services"; import { FirstLoadService, I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { RegistrationApplication } from "../common/registration-application"; import { RegistrationApplication } from "../common/registration-application";
import { UnreadCounterService } from "../../services";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -46,7 +48,7 @@ export class RegistrationApplications extends Component<
> { > {
private isoData = setIsoData<RegistrationApplicationsData>(this.context); private isoData = setIsoData<RegistrationApplicationsData>(this.context);
state: RegistrationApplicationsState = { state: RegistrationApplicationsState = {
appsRes: { state: "empty" }, appsRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
page: 1, page: 1,
@ -80,7 +82,7 @@ export class RegistrationApplications extends Component<
const mui = UserService.Instance.myUserInfo; const mui = UserService.Instance.myUserInfo;
return mui return mui
? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t( ? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t(
"registration_applications" "registration_applications",
)} - ${this.state.siteRes.site_view.site.name}` )} - ${this.state.siteRes.site_view.site.name}`
: ""; : "";
} }
@ -110,6 +112,7 @@ export class RegistrationApplications extends Component<
<Paginator <Paginator
page={this.state.page} page={this.state.page}
onChange={this.handlePageChange} onChange={this.handlePageChange}
nextDisabled={fetchLimit > apps.length}
/> />
</div> </div>
</div> </div>
@ -204,46 +207,47 @@ export class RegistrationApplications extends Component<
} }
static async fetchInitialData({ static async fetchInitialData({
auth,
client, client,
}: InitialFetchRequest): Promise<RegistrationApplicationsData> { }: InitialFetchRequest): Promise<RegistrationApplicationsData> {
return { return {
listRegistrationApplicationsResponse: auth listRegistrationApplicationsResponse: myAuth()
? await client.listRegistrationApplications({ ? await client.listRegistrationApplications({
unread_only: true, unread_only: true,
page: 1, page: 1,
limit: fetchLimit, limit: fetchLimit,
auth: auth as string,
}) })
: { state: "empty" }, : EMPTY_REQUEST,
}; };
} }
async refetch() { async refetch() {
const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread; const unread_only = this.state.unreadOrAll === UnreadOrAll.Unread;
this.setState({ this.setState({
appsRes: { state: "loading" }, appsRes: LOADING_REQUEST,
}); });
this.setState({ this.setState({
appsRes: await HttpService.client.listRegistrationApplications({ appsRes: await HttpService.client.listRegistrationApplications({
unread_only: unread_only, unread_only: unread_only,
page: this.state.page, page: this.state.page,
limit: fetchLimit, limit: fetchLimit,
auth: myAuthRequired(),
}), }),
}); });
} }
async handleApproveApplication(form: ApproveRegistrationApplication) { async handleApproveApplication(form: ApproveRegistrationApplication) {
const approveRes = await HttpService.client.approveRegistrationApplication( const approveRes = await HttpService.client.approveRegistrationApplication(
form form,
); );
this.setState(s => { this.setState(s => {
if (s.appsRes.state == "success" && approveRes.state == "success") { if (s.appsRes.state === "success" && approveRes.state === "success") {
s.appsRes.data.registration_applications = editRegistrationApplication( s.appsRes.data.registration_applications = editRegistrationApplication(
approveRes.data.registration_application, approveRes.data.registration_application,
s.appsRes.data.registration_applications s.appsRes.data.registration_applications,
); );
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateApplications();
}
} }
return s; return s;
}); });

View file

@ -2,7 +2,6 @@ import {
editCommentReport, editCommentReport,
editPostReport, editPostReport,
editPrivateMessageReport, editPrivateMessageReport,
myAuthRequired,
setIsoData, setIsoData,
} from "@utils/app"; } from "@utils/app";
import { randomStr } from "@utils/helpers"; import { randomStr } from "@utils/helpers";
@ -36,13 +35,18 @@ import {
I18NextService, I18NextService,
UserService, UserService,
} from "../../services"; } from "../../services";
import { RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { CommentReport } from "../comment/comment-report"; import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { PostReport } from "../post/post-report"; import { PostReport } from "../post/post-report";
import { PrivateMessageReport } from "../private_message/private-message-report"; import { PrivateMessageReport } from "../private_message/private-message-report";
import { UnreadCounterService } from "../../services";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -89,9 +93,9 @@ interface ReportsState {
export class Reports extends Component<any, ReportsState> { export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData<ReportsData>(this.context); private isoData = setIsoData<ReportsData>(this.context);
state: ReportsState = { state: ReportsState = {
commentReportsRes: { state: "empty" }, commentReportsRes: EMPTY_REQUEST,
postReportsRes: { state: "empty" }, postReportsRes: EMPTY_REQUEST,
messageReportsRes: { state: "empty" }, messageReportsRes: EMPTY_REQUEST,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
page: 1, page: 1,
@ -140,7 +144,7 @@ export class Reports extends Component<any, ReportsState> {
const mui = UserService.Instance.myUserInfo; const mui = UserService.Instance.myUserInfo;
return mui return mui
? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t( ? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t(
"reports" "reports",
)} - ${this.state.siteRes.site_view.site.name}` )} - ${this.state.siteRes.site_view.site.name}`
: ""; : "";
} }
@ -160,6 +164,16 @@ export class Reports extends Component<any, ReportsState> {
<Paginator <Paginator
page={this.state.page} page={this.state.page}
onChange={this.handlePageChange} onChange={this.handlePageChange}
nextDisabled={
(this.state.messageType === MessageType.All &&
fetchLimit > this.buildCombined.length) ||
(this.state.messageType === MessageType.CommentReport &&
fetchLimit > this.commentReports.length) ||
(this.state.messageType === MessageType.PostReport &&
fetchLimit > this.postReports.length) ||
(this.state.messageType === MessageType.PrivateMessageReport &&
fetchLimit > this.privateMessageReports.length)
}
/> />
</div> </div>
</div> </div>
@ -352,25 +366,25 @@ export class Reports extends Component<any, ReportsState> {
get buildCombined(): ItemType[] { get buildCombined(): ItemType[] {
const commentRes = this.state.commentReportsRes; const commentRes = this.state.commentReportsRes;
const comments = const comments =
commentRes.state == "success" commentRes.state === "success"
? commentRes.data.comment_reports.map(this.commentReportToItemType) ? commentRes.data.comment_reports.map(this.commentReportToItemType)
: []; : [];
const postRes = this.state.postReportsRes; const postRes = this.state.postReportsRes;
const posts = const posts =
postRes.state == "success" postRes.state === "success"
? postRes.data.post_reports.map(this.postReportToItemType) ? postRes.data.post_reports.map(this.postReportToItemType)
: []; : [];
const pmRes = this.state.messageReportsRes; const pmRes = this.state.messageReportsRes;
const privateMessages = const privateMessages =
pmRes.state == "success" pmRes.state === "success"
? pmRes.data.private_message_reports.map( ? pmRes.data.private_message_reports.map(
this.privateMessageReportToItemType this.privateMessageReportToItemType,
) )
: []; : [];
return [...comments, ...posts, ...privateMessages].sort((a, b) => return [...comments, ...posts, ...privateMessages].sort((a, b) =>
b.published.localeCompare(a.published) b.published.localeCompare(a.published),
); );
} }
@ -521,7 +535,6 @@ export class Reports extends Component<any, ReportsState> {
} }
static async fetchInitialData({ static async fetchInitialData({
auth,
client, client,
}: InitialFetchRequest): Promise<ReportsData> { }: InitialFetchRequest): Promise<ReportsData> {
const unresolved_only = true; const unresolved_only = true;
@ -532,20 +545,18 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth: auth as string,
}; };
const postReportsForm: ListPostReports = { const postReportsForm: ListPostReports = {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth: auth as string,
}; };
const data: ReportsData = { const data: ReportsData = {
commentReportsRes: await client.listCommentReports(commentReportsForm), commentReportsRes: await client.listCommentReports(commentReportsForm),
postReportsRes: await client.listPostReports(postReportsForm), postReportsRes: await client.listPostReports(postReportsForm),
messageReportsRes: { state: "empty" }, messageReportsRes: EMPTY_REQUEST,
}; };
if (amAdmin()) { if (amAdmin()) {
@ -553,11 +564,10 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth: auth as string,
}; };
data.messageReportsRes = await client.listPrivateMessageReports( data.messageReportsRes = await client.listPrivateMessageReports(
privateMessageReportsForm privateMessageReportsForm,
); );
} }
@ -565,15 +575,14 @@ export class Reports extends Component<any, ReportsState> {
} }
async refetch() { async refetch() {
const unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread; const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
const page = this.state.page; const page = this.state.page;
const limit = fetchLimit; const limit = fetchLimit;
const auth = myAuthRequired();
this.setState({ this.setState({
commentReportsRes: { state: "loading" }, commentReportsRes: LOADING_REQUEST,
postReportsRes: { state: "loading" }, postReportsRes: LOADING_REQUEST,
messageReportsRes: { state: "loading" }, messageReportsRes: LOADING_REQUEST,
}); });
const form: const form:
@ -583,7 +592,6 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth,
}; };
this.setState({ this.setState({
@ -594,7 +602,7 @@ export class Reports extends Component<any, ReportsState> {
if (amAdmin()) { if (amAdmin()) {
this.setState({ this.setState({
messageReportsRes: await HttpService.client.listPrivateMessageReports( messageReportsRes: await HttpService.client.listPrivateMessageReports(
form form,
), ),
}); });
} }
@ -603,24 +611,36 @@ export class Reports extends Component<any, ReportsState> {
async handleResolveCommentReport(form: ResolveCommentReport) { async handleResolveCommentReport(form: ResolveCommentReport) {
const res = await HttpService.client.resolveCommentReport(form); const res = await HttpService.client.resolveCommentReport(form);
this.findAndUpdateCommentReport(res); this.findAndUpdateCommentReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
} }
async handleResolvePostReport(form: ResolvePostReport) { async handleResolvePostReport(form: ResolvePostReport) {
const res = await HttpService.client.resolvePostReport(form); const res = await HttpService.client.resolvePostReport(form);
this.findAndUpdatePostReport(res); this.findAndUpdatePostReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
} }
async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) { async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) {
const res = await HttpService.client.resolvePrivateMessageReport(form); const res = await HttpService.client.resolvePrivateMessageReport(form);
this.findAndUpdatePrivateMessageReport(res); this.findAndUpdatePrivateMessageReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
} }
findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) { findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentReportsRes.state == "success" && res.state == "success") { if (s.commentReportsRes.state === "success" && res.state === "success") {
s.commentReportsRes.data.comment_reports = editCommentReport( s.commentReportsRes.data.comment_reports = editCommentReport(
res.data.comment_report_view, res.data.comment_report_view,
s.commentReportsRes.data.comment_reports s.commentReportsRes.data.comment_reports,
); );
} }
return s; return s;
@ -629,10 +649,10 @@ export class Reports extends Component<any, ReportsState> {
findAndUpdatePostReport(res: RequestState<PostReportResponse>) { findAndUpdatePostReport(res: RequestState<PostReportResponse>) {
this.setState(s => { this.setState(s => {
if (s.postReportsRes.state == "success" && res.state == "success") { if (s.postReportsRes.state === "success" && res.state === "success") {
s.postReportsRes.data.post_reports = editPostReport( s.postReportsRes.data.post_reports = editPostReport(
res.data.post_report_view, res.data.post_report_view,
s.postReportsRes.data.post_reports s.postReportsRes.data.post_reports,
); );
} }
return s; return s;
@ -640,14 +660,14 @@ export class Reports extends Component<any, ReportsState> {
} }
findAndUpdatePrivateMessageReport( findAndUpdatePrivateMessageReport(
res: RequestState<PrivateMessageReportResponse> res: RequestState<PrivateMessageReportResponse>,
) { ) {
this.setState(s => { this.setState(s => {
if (s.messageReportsRes.state == "success" && res.state == "success") { if (s.messageReportsRes.state === "success" && res.state === "success") {
s.messageReportsRes.data.private_message_reports = s.messageReportsRes.data.private_message_reports =
editPrivateMessageReport( editPrivateMessageReport(
res.data.private_message_report_view, res.data.private_message_report_view,
s.messageReportsRes.data.private_message_reports s.messageReportsRes.data.private_message_reports,
); );
} }
return s; return s;

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,12 @@ import { setIsoData } from "@utils/app";
import { Component } from "inferno"; import { Component } from "inferno";
import { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client"; import { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client";
import { I18NextService } from "../../services"; import { I18NextService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -16,7 +21,7 @@ export class VerifyEmail extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
state: State = { state: State = {
verifyRes: { state: "empty" }, verifyRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
@ -26,7 +31,7 @@ export class VerifyEmail extends Component<any, State> {
async verify() { async verify() {
this.setState({ this.setState({
verifyRes: { state: "loading" }, verifyRes: LOADING_REQUEST,
}); });
this.setState({ this.setState({
@ -35,7 +40,7 @@ export class VerifyEmail extends Component<any, State> {
}), }),
}); });
if (this.state.verifyRes.state == "success") { if (this.state.verifyRes.state === "success") {
toast(I18NextService.i18n.t("email_verified")); toast(I18NextService.i18n.t("email_verified"));
this.props.history.push("/login"); this.props.history.push("/login");
} }
@ -61,7 +66,7 @@ export class VerifyEmail extends Component<any, State> {
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h1 className="h4 mb-4">{I18NextService.i18n.t("verify_email")}</h1> <h1 className="h4 mb-4">{I18NextService.i18n.t("verify_email")}</h1>
{this.state.verifyRes.state == "loading" && ( {this.state.verifyRes.state === "loading" && (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>

View file

@ -1,4 +1,4 @@
import { enableDownvotes, enableNsfw, myAuth, setIsoData } from "@utils/app"; import { enableDownvotes, enableNsfw, setIsoData } from "@utils/app";
import { getIdFromString, getQueryParams } from "@utils/helpers"; import { getIdFromString, getQueryParams } from "@utils/helpers";
import type { QueryParams } from "@utils/types"; import type { QueryParams } from "@utils/types";
import { Choice, RouteDataResponse } from "@utils/types"; import { Choice, RouteDataResponse } from "@utils/types";
@ -14,6 +14,7 @@ import {
import { InitialFetchRequest, PostFormParams } from "../../interfaces"; import { InitialFetchRequest, PostFormParams } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services"; import { FirstLoadService, I18NextService } from "../../services";
import { import {
EMPTY_REQUEST,
HttpService, HttpService,
RequestState, RequestState,
WrappedLemmyHttp, WrappedLemmyHttp,
@ -57,7 +58,7 @@ export class CreatePost extends Component<
state: CreatePostState = { state: CreatePostState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
loading: true, loading: true,
initialCommunitiesRes: { state: "empty" }, initialCommunitiesRes: EMPTY_REQUEST,
isIsomorphic: false, isIsomorphic: false,
}; };
@ -96,12 +97,10 @@ export class CreatePost extends Component<
async fetchCommunity() { async fetchCommunity() {
const { communityId } = getCreatePostQueryParams(); const { communityId } = getCreatePostQueryParams();
const auth = myAuth();
if (communityId) { if (communityId) {
const res = await HttpService.client.getCommunity({ const res = await HttpService.client.getCommunity({
id: communityId, id: communityId,
auth,
}); });
if (res.state === "success") { if (res.state === "success") {
this.setState({ this.setState({
@ -121,7 +120,7 @@ export class CreatePost extends Component<
const { communityId } = getCreatePostQueryParams(); const { communityId } = getCreatePostQueryParams();
const initialCommunitiesRes = await fetchCommunitiesForOptions( const initialCommunitiesRes = await fetchCommunitiesForOptions(
HttpService.client HttpService.client,
); );
this.setState({ this.setState({
@ -239,18 +238,16 @@ export class CreatePost extends Component<
static async fetchInitialData({ static async fetchInitialData({
client, client,
query: { communityId }, query: { communityId },
auth,
}: InitialFetchRequest< }: InitialFetchRequest<
QueryParams<CreatePostProps> QueryParams<CreatePostProps>
>): Promise<CreatePostData> { >): Promise<CreatePostData> {
const data: CreatePostData = { const data: CreatePostData = {
initialCommunitiesRes: await fetchCommunitiesForOptions(client), initialCommunitiesRes: await fetchCommunitiesForOptions(client),
communityResponse: { state: "empty" }, communityResponse: EMPTY_REQUEST,
}; };
if (communityId) { if (communityId) {
const form: GetCommunity = { const form: GetCommunity = {
auth,
id: getIdFromString(communityId), id: getIdFromString(communityId),
}; };

View file

@ -1,9 +1,4 @@
import { import { communityToChoice, fetchCommunities } from "@utils/app";
communityToChoice,
fetchCommunities,
myAuth,
myAuthRequired,
} from "@utils/app";
import { import {
capitalizeFirstLetter, capitalizeFirstLetter,
debounce, debounce,
@ -15,6 +10,7 @@ import { isImage } from "@utils/media";
import { Choice } from "@utils/types"; import { Choice } from "@utils/types";
import autosize from "autosize"; import autosize from "autosize";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import { import {
CommunityView, CommunityView,
CreatePost, CreatePost,
@ -33,13 +29,17 @@ import {
} from "../../config"; } from "../../config";
import { PostFormParams } from "../../interfaces"; import { PostFormParams } from "../../interfaces";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService"; import {
EMPTY_REQUEST,
HttpService,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { LanguageSelect } from "../common/language-select"; import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt";
import { SearchableSelect } from "../common/searchable-select"; import { SearchableSelect } from "../common/searchable-select";
import { PostListings } from "./post-listings"; import { PostListings } from "./post-listings";
@ -89,7 +89,6 @@ function handlePostSubmit(i: PostForm, event: any) {
i.setState(s => ((s.form.url = undefined), s)); i.setState(s => ((s.form.url = undefined), s));
} }
i.setState({ loading: true, submitted: true }); i.setState({ loading: true, submitted: true });
const auth = myAuthRequired();
const pForm = i.state.form; const pForm = i.state.form;
const pv = i.props.post_view; const pv = i.props.post_view;
@ -102,7 +101,6 @@ function handlePostSubmit(i: PostForm, event: any) {
nsfw: pForm.nsfw, nsfw: pForm.nsfw,
post_id: pv.post.id, post_id: pv.post.id,
language_id: pForm.language_id, language_id: pForm.language_id,
auth,
}); });
} else if (pForm.name && pForm.community_id) { } else if (pForm.name && pForm.community_id) {
i.props.onCreate?.({ i.props.onCreate?.({
@ -113,7 +111,6 @@ function handlePostSubmit(i: PostForm, event: any) {
nsfw: pForm.nsfw, nsfw: pForm.nsfw,
language_id: pForm.language_id, language_id: pForm.language_id,
honeypot: pForm.honeypot, honeypot: pForm.honeypot,
auth,
}); });
} }
} }
@ -122,9 +119,9 @@ function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
const sTitle = d.suggestedTitle; const sTitle = d.suggestedTitle;
if (sTitle) { if (sTitle) {
d.i.setState( d.i.setState(
s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s) s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s),
); );
d.i.setState({ suggestedPostsRes: { state: "empty" } }); d.i.setState({ suggestedPostsRes: EMPTY_REQUEST });
setTimeout(() => { setTimeout(() => {
const textarea: any = document.getElementById("post-title"); const textarea: any = document.getElementById("post-title");
autosize.update(textarea); autosize.update(textarea);
@ -223,8 +220,8 @@ function handleImageDelete(i: PostForm) {
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
state: PostFormState = { state: PostFormState = {
suggestedPostsRes: { state: "empty" }, suggestedPostsRes: EMPTY_REQUEST,
metadataRes: { state: "empty" }, metadataRes: EMPTY_REQUEST,
form: {}, form: {},
loading: false, loading: false,
imageLoading: false, imageLoading: false,
@ -271,9 +268,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
({ community: { id, title } }) => ({ ({ community: { id, title } }) => ({
label: title, label: title,
value: id.toString(), value: id.toString(),
}) }),
) ?? [] ) ?? []
).filter(option => option.value !== selectedCommunityChoice.value) ).filter(option => option.value !== selectedCommunityChoice.value),
), ),
}; };
} else { } else {
@ -284,7 +281,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
({ community: { id, title } }) => ({ ({ community: { id, title } }) => ({
label: title, label: title,
value: id.toString(), value: id.toString(),
}) }),
) ?? [], ) ?? [],
}; };
} }
@ -310,16 +307,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & PostFormProps> nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>,
): void { ): void {
if (this.props != nextProps) { if (this.props !== nextProps) {
this.setState( this.setState(
s => ( s => (
(s.form.community_id = getIdFromString( (s.form.community_id = getIdFromString(
nextProps.selectedCommunityChoice?.value nextProps.selectedCommunityChoice?.value,
)), )),
s s
) ),
); );
} }
} }
@ -332,7 +329,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
return ( return (
<form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}> <form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}>
<NavigationPrompt <Prompt
message={I18NextService.i18n.t("block_leaving")}
when={ when={
!!( !!(
this.state.form.name || this.state.form.name ||
@ -341,6 +339,32 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
) && !this.state.submitted ) && !this.state.submitted
} }
/> />
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="post-title">
{I18NextService.i18n.t("title")}
</label>
<div className="col-sm-10">
<textarea
value={this.state.form.name}
id="post-title"
onInput={linkEvent(this, handlePostNameChange)}
className={`form-control ${
!validTitle(this.state.form.name) && "is-invalid"
}`}
required
rows={1}
minLength={3}
maxLength={MAX_POST_TITLE_LENGTH}
/>
{!validTitle(this.state.form.name) && (
<div className="invalid-feedback">
{I18NextService.i18n.t("invalid_post_title")}
</div>
)}
{this.renderSuggestedPosts()}
</div>
</div>
<div className="mb-3 row"> <div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="post-url"> <label className="col-sm-2 col-form-label" htmlFor="post-url">
{I18NextService.i18n.t("url")} {I18NextService.i18n.t("url")}
@ -366,7 +390,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</a> </a>
<a <a
href={`${ghostArchiveUrl}/search?term=${encodeURIComponent( href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
url url,
)}`} )}`}
className="me-2 d-inline-block float-right text-muted small fw-bold" className="me-2 d-inline-block float-right text-muted small fw-bold"
rel={relTags} rel={relTags}
@ -375,7 +399,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</a> </a>
<a <a
href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent( href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
url url,
)}`} )}`}
className="me-2 d-inline-block float-right text-muted small fw-bold" className="me-2 d-inline-block float-right text-muted small fw-bold"
rel={relTags} rel={relTags}
@ -446,37 +470,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
onAddModToCommunity={() => {}} onAddModToCommunity={() => {}}
onAddAdmin={() => {}} onAddAdmin={() => {}}
onTransferCommunity={() => {}} onTransferCommunity={() => {}}
onMarkPostAsRead={() => {}}
/> />
</> </>
)} )}
</div> </div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="post-title">
{I18NextService.i18n.t("title")}
</label>
<div className="col-sm-10">
<textarea
value={this.state.form.name}
id="post-title"
onInput={linkEvent(this, handlePostNameChange)}
className={`form-control ${
!validTitle(this.state.form.name) && "is-invalid"
}`}
required
rows={1}
minLength={3}
maxLength={MAX_POST_TITLE_LENGTH}
/>
{!validTitle(this.state.form.name) && (
<div className="invalid-feedback">
{I18NextService.i18n.t("invalid_post_title")}
</div>
)}
{this.renderSuggestedPosts()}
</div>
</div>
<div className="mb-3 row"> <div className="mb-3 row">
<label className="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{I18NextService.i18n.t("body")} {I18NextService.i18n.t("body")}
@ -580,8 +579,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
case "loading": case "loading":
return <Spinner />; return <Spinner />;
case "success": { case "success": {
const suggestedTitle = this.state.metadataRes.data.metadata.title; // Clean up the title of any extra whitespace and replace &nbsp; with a space
const suggestedTitle = this.state.metadataRes.data.metadata.title
?.trim()
.replace(/\s+/g, " ");
return ( return (
suggestedTitle && ( suggestedTitle && (
<button <button
@ -589,7 +590,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
className="mt-1 small border-0 bg-transparent p-0 d-block text-muted fw-bold pointer" className="mt-1 small border-0 bg-transparent p-0 d-block text-muted fw-bold pointer"
onClick={linkEvent( onClick={linkEvent(
{ i: this, suggestedTitle }, { i: this, suggestedTitle },
copySuggestedTitle copySuggestedTitle,
)} )}
> >
{I18NextService.i18n.t("copy_suggested_title", { title: "" })}{" "} {I18NextService.i18n.t("copy_suggested_title", { title: "" })}{" "}
@ -640,6 +641,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
onAddModToCommunity={() => {}} onAddModToCommunity={() => {}}
onAddAdmin={() => {}} onAddAdmin={() => {}}
onTransferCommunity={() => {}} onTransferCommunity={() => {}}
onMarkPostAsRead={() => {}}
/> />
</> </>
) )
@ -651,7 +653,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
async fetchPageTitle() { async fetchPageTitle() {
const url = this.state.form.url; const url = this.state.form.url;
if (url && validURL(url)) { if (url && validURL(url)) {
this.setState({ metadataRes: { state: "loading" } }); this.setState({ metadataRes: LOADING_REQUEST });
this.setState({ this.setState({
metadataRes: await HttpService.client.getSiteMetadata({ url }), metadataRes: await HttpService.client.getSiteMetadata({ url }),
}); });
@ -661,7 +663,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
async fetchSimilarPosts() { async fetchSimilarPosts() {
const q = this.state.form.name; const q = this.state.form.name;
if (q && q !== "") { if (q && q !== "") {
this.setState({ suggestedPostsRes: { state: "loading" } }); this.setState({ suggestedPostsRes: LOADING_REQUEST });
this.setState({ this.setState({
suggestedPostsRes: await HttpService.client.search({ suggestedPostsRes: await HttpService.client.search({
q, q,
@ -671,7 +673,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
community_id: this.state.form.community_id, community_id: this.state.form.community_id,
page: 1, page: 1,
limit: trendingFetchLimit, limit: trendingFetchLimit,
auth: myAuth(),
}), }),
}); });
} }

Some files were not shown because too many files have changed in this diff Show more