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

View file

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

View file

@ -1,6 +1,6 @@
pipeline:
steps:
fetch_git_submodules:
image: node:alpine
image: node:20-alpine
commands:
- apk add git
- git submodule init
@ -8,17 +8,17 @@ pipeline:
# - git fetch --tags
yarn:
image: node:alpine
image: node:20-alpine
commands:
- yarn
yarn_lint:
image: node:alpine
image: node:20-alpine
commands:
- yarn lint
yarn_build_dev:
image: node:alpine
image: node:20-alpine
commands:
- yarn build:dev
@ -29,7 +29,7 @@ pipeline:
repo: dessalines/lemmy-ui
dockerfile: Dockerfile
platforms: linux/amd64
auto_tag: true
tag: ${CI_COMMIT_TAG}
when:
event: tag
@ -43,3 +43,19 @@ pipeline:
tag: dev
when:
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 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.*'
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/node_modules /app/node_modules
RUN chown -R node:node /app
USER node
EXPOSE 1234
WORKDIR /app
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
WORKDIR /usr/src/app
@ -28,7 +28,7 @@ RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
RUN yarn --prefer-offline
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/node_modules /app/node_modules

View file

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

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

View file

@ -1,6 +1,6 @@
{
"name": "lemmy-ui",
"version": "0.18.1-rc.11",
"version": "0.19.0-rc.3",
"description": "An isomorphic UI for lemmy",
"repository": "https://github.com/LemmyNet/lemmy-ui",
"license": "AGPL-3.0",
@ -15,6 +15,7 @@
"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}\"",
"prepare": "husky install",
"postinstall": "husky install",
"themes:build": "sass 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",
@ -34,23 +35,24 @@
]
},
"dependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/plugin-transform-typescript": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.5",
"@babel/plugin-transform-runtime": "^7.21.5",
"@babel/plugin-transform-typescript": "^7.21.5",
"@babel/preset-env": "7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5",
"@emoji-mart/data": "^1.1.0",
"@shortcm/qr-image": "^9.0.2",
"autosize": "^6.0.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
"bootstrap": "^5.2.3",
"bootstrap": "^5.3.1",
"check-password-strength": "^2.0.7",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
"cookie": "^0.5.0",
"copy-webpack-plugin": "^11.0.0",
"cross-fetch": "^3.1.5",
"cross-fetch": "^4.0.0",
"css-loader": "^6.7.3",
"date-fns": "^2.30.0",
"emoji-mart": "^5.4.0",
@ -58,22 +60,24 @@
"express": "~4.18.2",
"history": "^5.3.0",
"html-to-text": "^9.0.5",
"i18next": "^22.4.15",
"inferno": "^8.1.1",
"inferno-create-element": "^8.1.1",
"husky": "^8.0.3",
"i18next": "^23.3.0",
"inferno": "^8.2.2",
"inferno-create-element": "^8.2.2",
"inferno-helmet": "^5.2.1",
"inferno-hydrate": "^8.1.1",
"inferno-hydrate": "^8.2.2",
"inferno-i18next-dess": "0.0.2",
"inferno-router": "^8.1.1",
"inferno-server": "^8.1.1",
"inferno-router": "^8.2.2",
"inferno-server": "^8.2.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.merge": "^4.6.2",
"markdown-it": "^13.0.1",
"markdown-it-bidi": "^0.1.0",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.2",
"markdown-it-footnote": "^3.0.3",
"markdown-it-highlightjs": "^4.0.1",
"markdown-it-html5-embed": "^1.0.0",
"markdown-it-ruby": "^0.1.1",
"markdown-it-sub": "^1.0.0",
@ -81,21 +85,23 @@
"mini-css-extract-plugin": "^2.7.5",
"register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0",
"sanitize-html": "^2.10.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"rxjs": "^7.8.1",
"sanitize-html": "^2.11.0",
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"serialize-javascript": "^6.0.1",
"service-worker-webpack": "^1.0.0",
"sharp": "^0.32.1",
"sharp": "^0.32.4",
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0",
"tributejs": "^5.1.3",
"webpack": "5.82.1",
"webpack-cli": "^5.1.1",
"webpack": "5.88.2",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
},
"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/bootstrap": "^5.2.6",
"@types/cookie": "^0.5.1",
@ -103,33 +109,32 @@
"@types/html-to-text": "^9.0.0",
"@types/lodash.isequal": "^4.5.6",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-container": "^2.0.5",
"@types/node": "^20.1.2",
"@types/markdown-it-container": "^2.0.6",
"@types/node": "^20.4.5",
"@types/path-browserify": "^1.0.0",
"@types/sanitize-html": "^2.9.0",
"@types/serialize-javascript": "^5.0.1",
"@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"eslint": "^8.40.0",
"@types/toastify-js": "^1.12.0",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"eslint": "^8.45.0",
"eslint-plugin-inferno": "^7.32.2",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"eslint-plugin-prettier": "^5.0.0",
"import-sort-style-module": "^6.0.0",
"lint-staged": "^13.2.2",
"prettier": "^2.8.8",
"lint-staged": "^13.2.3",
"prettier": "^3.0.0",
"prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-packagejson": "^2.4.5",
"rimraf": "^5.0.0",
"sortpack": "^2.3.4",
"style-loader": "^3.3.2",
"terser": "^5.17.3",
"typescript": "^5.0.4",
"terser": "^5.19.2",
"typescript": "^5.1.6",
"typescript-language-server": "^3.3.2",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-dev-server": "4.15.0"
"webpack-dev-server": "4.15.1"
},
"packageManager": "yarn@1.22.19",
"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;
}
.img-blur {
.img-blur-thumb {
filter: blur(10px);
-webkit-filter: blur(10px);
-moz-filter: blur(10px);
@ -259,6 +259,18 @@ hr {
-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 {
max-height: 90vh;
}
@ -436,3 +448,7 @@ br.big {
.skip-link:focus {
top: 0;
}
.totp-link {
width: fit-content;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,8 +23,11 @@ option:disabled {
}
.form-control::placeholder {
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;
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;
}
.input-group-text {

View file

@ -74,8 +74,9 @@
--bs-font-sans-serif: "Lucida Console", Monaco, monospace;
--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-root-font-size: 93.75%;
--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-line-height: 1.5;
--bs-body-color: #495057;
@ -184,6 +185,9 @@
box-sizing: border-box;
}
:root {
font-size: var(--bs-root-font-size);
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
@ -220,47 +224,47 @@ h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {
}
h1, .h1 {
font-size: calc(1.34375rem + 1.125vw);
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1, .h1 {
font-size: 2.1875rem;
font-size: 2.5rem;
}
}
h2, .h2 {
font-size: calc(1.3rem + 0.6vw);
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2, .h2 {
font-size: 1.75rem;
font-size: 2rem;
}
}
h3, .h3 {
font-size: calc(1.278125rem + 0.3375vw);
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3, .h3 {
font-size: 1.53125rem;
font-size: 1.75rem;
}
}
h4, .h4 {
font-size: calc(1.25625rem + 0.075vw);
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4, .h4 {
font-size: 1.3125rem;
font-size: 1.5rem;
}
}
h5, .h5 {
font-size: 1.09375rem;
font-size: 1.25rem;
}
h6, .h6 {
font-size: 0.875rem;
font-size: 1rem;
}
p {
@ -585,7 +589,7 @@ progress {
}
.lead {
font-size: 1.09375rem;
font-size: 1.25rem;
font-weight: 300;
}
@ -679,7 +683,7 @@ progress {
.blockquote {
margin-bottom: 1rem;
font-size: 1.09375rem;
font-size: 1.25rem;
}
.blockquote > :last-child {
margin-bottom: 0;
@ -2087,13 +2091,13 @@ progress {
.col-form-label-lg {
padding-top: 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 {
padding-top: 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 {
@ -2106,7 +2110,7 @@ progress {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
@ -2199,7 +2203,7 @@ progress {
.form-control-sm {
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
padding: 0.25rem 0.5rem;
font-size: 0.765625rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
.form-control-sm::file-selector-button {
@ -2211,7 +2215,7 @@ progress {
.form-control-lg {
min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
padding: 0.5rem 1rem;
font-size: 1.09375rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
.form-control-lg::file-selector-button {
@ -2258,7 +2262,7 @@ textarea.form-control-lg {
display: block;
width: 100%;
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
font-size: 0.875rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
@ -2299,7 +2303,7 @@ textarea.form-control-lg {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
font-size: 0.765625rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
@ -2307,7 +2311,7 @@ textarea.form-control-lg {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
font-size: 1.09375rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
@ -2317,7 +2321,7 @@ textarea.form-control-lg {
.form-check {
display: block;
min-height: 1.3125rem;
min-height: 1.5rem;
padding-left: 1.5em;
margin-bottom: 0.125rem;
}
@ -2653,7 +2657,7 @@ textarea.form-control-lg {
display: flex;
align-items: center;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
@ -2669,7 +2673,7 @@ textarea.form-control-lg {
.input-group-lg > .input-group-text,
.input-group-lg > .btn {
padding: 0.5rem 1rem;
font-size: 1.09375rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
@ -2678,7 +2682,7 @@ textarea.form-control-lg {
.input-group-sm > .input-group-text,
.input-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.765625rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
@ -2728,7 +2732,7 @@ textarea.form-control-lg {
max-width: 100%;
padding: 0.25rem 0.5rem;
margin-top: 0.1rem;
font-size: 0.765625rem;
font-size: 0.875rem;
color: #fff;
background-color: var(--bs-success);
border-radius: var(--bs-border-radius);
@ -2818,7 +2822,7 @@ textarea.form-control-lg {
max-width: 100%;
padding: 0.25rem 0.5rem;
margin-top: 0.1rem;
font-size: 0.765625rem;
font-size: 0.875rem;
color: #fff;
background-color: var(--bs-danger);
border-radius: var(--bs-border-radius);
@ -2896,7 +2900,7 @@ textarea.form-control-lg {
--bs-btn-padding-x: 0.75rem;
--bs-btn-padding-y: 0.375rem;
--bs-btn-font-family: ;
--bs-btn-font-size: 0.875rem;
--bs-btn-font-size: 1rem;
--bs-btn-font-weight: 400;
--bs-btn-line-height: 1.5;
--bs-btn-color: var(--bs-body-color);
@ -3272,14 +3276,14 @@ textarea.form-control-lg {
.btn-lg, .btn-group-lg > .btn {
--bs-btn-padding-y: 0.5rem;
--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);
}
.btn-sm, .btn-group-sm > .btn {
--bs-btn-padding-y: 0.25rem;
--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);
}
@ -3352,7 +3356,7 @@ textarea.form-control-lg {
--bs-dropdown-padding-x: 0;
--bs-dropdown-padding-y: 0.5rem;
--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-bg: var(--bs-body-bg);
--bs-dropdown-border-color: var(--bs-border-color-translucent);
@ -3614,7 +3618,7 @@ textarea.form-control-lg {
display: block;
padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);
margin-bottom: 0;
font-size: 0.765625rem;
font-size: 0.875rem;
color: var(--bs-dropdown-header-color);
white-space: nowrap;
}
@ -3899,15 +3903,15 @@ textarea.form-control-lg {
--bs-navbar-hover-color: rgba(255, 64, 186, 0.7);
--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-brand-padding-y: 0.3359375rem;
--bs-navbar-brand-padding-y: 0.3125rem;
--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-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);
--bs-navbar-nav-link-padding-x: 0.5rem;
--bs-navbar-toggler-padding-y: 0.25rem;
--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-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);
--bs-navbar-toggler-border-radius: var(--bs-border-radius);
@ -4544,7 +4548,7 @@ textarea.form-control-lg {
align-items: center;
width: 100%;
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);
text-align: left;
background-color: var(--bs-accordion-btn-bg);
@ -4688,7 +4692,7 @@ textarea.form-control-lg {
.pagination {
--bs-pagination-padding-x: 0.75rem;
--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-bg: var(--bs-body-bg);
--bs-pagination-border-width: var(--bs-border-width);
@ -4768,14 +4772,14 @@ textarea.form-control-lg {
.pagination-lg {
--bs-pagination-padding-x: 1.5rem;
--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);
}
.pagination-sm {
--bs-pagination-padding-x: 0.5rem;
--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);
}
@ -4910,7 +4914,7 @@ textarea.form-control-lg {
.progress,
.progress-stacked {
--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-border-radius: var(--bs-border-radius);
--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-y: 0.25rem;
--bs-tooltip-margin: ;
--bs-tooltip-font-size: 0.765625rem;
--bs-tooltip-font-size: 0.875rem;
--bs-tooltip-color: var(--bs-body-bg);
--bs-tooltip-bg: var(--bs-emphasis-color);
--bs-tooltip-border-radius: var(--bs-border-radius);
@ -5815,7 +5819,7 @@ textarea.form-control-lg {
.popover {
--bs-popover-zindex: 1070;
--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-border-width: var(--bs-border-width);
--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-header-padding-x: 1rem;
--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-bg: var(--bs-secondary-bg);
--bs-popover-body-padding-x: 1rem;
@ -8295,27 +8299,27 @@ textarea.form-control-lg {
}
.fs-1 {
font-size: calc(1.34375rem + 1.125vw) !important;
font-size: calc(1.375rem + 1.5vw) !important;
}
.fs-2 {
font-size: calc(1.3rem + 0.6vw) !important;
font-size: calc(1.325rem + 0.9vw) !important;
}
.fs-3 {
font-size: calc(1.278125rem + 0.3375vw) !important;
font-size: calc(1.3rem + 0.6vw) !important;
}
.fs-4 {
font-size: calc(1.25625rem + 0.075vw) !important;
font-size: calc(1.275rem + 0.3vw) !important;
}
.fs-5 {
font-size: 1.09375rem !important;
font-size: 1.25rem !important;
}
.fs-6 {
font-size: 0.875rem !important;
font-size: 1rem !important;
}
.fst-italic {
@ -11858,16 +11862,16 @@ textarea.form-control-lg {
}
@media (min-width: 1200px) {
.fs-1 {
font-size: 2.1875rem !important;
font-size: 2.5rem !important;
}
.fs-2 {
font-size: 1.75rem !important;
font-size: 2rem !important;
}
.fs-3 {
font-size: 1.53125rem !important;
font-size: 1.75rem !important;
}
.fs-4 {
font-size: 1.3125rem !important;
font-size: 1.5rem !important;
}
}
@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
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024"
height="1024"
viewBox="0 0 1024 1024"
version="1.1"
id="svg8"
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">
version="1.1">
<g transform="translate(0,-26.066658)">
<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"
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" />
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" />
<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"
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"
id="path969"
inkscape:connector-curvature="0"
sodipodi:nodetypes="szszs"
inkscape:label="head"
sodipodi:insensitive="true" />
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" />
<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"
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" />
<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"
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" />
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" />
<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"
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" />
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" />
<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"
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" />
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" />
</g>
</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">
<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 id="icon-alert-triangle" viewBox="0 0 24 24">
<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>
<symbol id="icon-alert-triangle" viewBox="0 -960 960 960">
<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 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>
@ -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.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</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>
</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 { hydrate } from "inferno-hydrate";
import { Router } from "inferno-router";
import { BrowserRouter } from "inferno-router";
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/dropdown";
import "bootstrap/js/dist/modal";
async function startClient() {
initializeSite(window.isoData.site_res);
@ -13,9 +14,9 @@ async function startClient() {
await setupDateFns();
const wrapper = (
<Router history={HistoryService.history}>
<BrowserRouter>
<App user={UserService.Instance.myUserInfo} />
</Router>
</BrowserRouter>
);
const root = document.getElementById("root");

View file

@ -6,7 +6,7 @@ import fetch from "cross-fetch";
import type { Request, Response } from "express";
import { StaticRouter, matchPath } from "inferno-router";
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 {
InitialFetchRequest,
@ -26,18 +26,19 @@ export default async (req: Request, res: Response) => {
try {
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 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;
// Get site data first
@ -46,19 +47,18 @@ export default async (req: Request, res: Response) => {
let site: GetSiteResponse | undefined = undefined;
let routeData: RouteData = {};
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(
"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;
auth = undefined;
try_site = await client.getSite(getSiteForm);
client.setHeaders({});
try_site = await client.getSite();
}
if (!auth && isAuthPath(path)) {
return res.redirect("/login");
return res.redirect(`/login?prev=${encodeURIComponent(url)}`);
}
if (try_site.state === "success") {
@ -72,7 +72,6 @@ export default async (req: Request, res: Response) => {
if (site && activeRoute?.fetchInitialData) {
const initialFetchReq: InitialFetchRequest = {
client,
auth,
path,
query,
site,
@ -90,7 +89,7 @@ export default async (req: Request, res: Response) => {
}
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;
// 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);
res.send(await createSsrHtml(root, isoData));
res.send(await createSsrHtml(root, isoData, res.locals.cspNonce));
} catch (err) {
// If an error is caught here, the error page couldn't even be rendered
console.error(err);
res.statusCode = 500;
return res.send(
process.env.NODE_ENV === "development" ? err.message : "Server error"
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()) {
const headers = setForwardedHeaders(req.headers);
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") {
manifest = await generateManifestJson(site.data);

View file

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

View file

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

View file

@ -8,7 +8,7 @@ export default async ({ res }: { res: Response }) => {
path.resolve(
`./dist/service-worker${
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 ThemesListHandler from "./handlers/themes-list-handler";
import { setCacheControl, setDefaultCsp } from "./middleware";
import CodeThemeHandler from "./handlers/code-theme-handler";
const server = express();
@ -20,17 +21,26 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"]
server.use(express.json());
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(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);
@ -38,6 +48,7 @@ server.get("/robots.txt", RobotsHandler);
server.get("/service-worker.js", ServiceWorkerHandler);
server.get("/manifest.webmanifest", ManifestHandler);
server.get("/css/themes/:name", ThemeHandler);
server.get("/css/code-themes/:name", CodeThemeHandler);
server.get("/css/themelist", ThemesListHandler);
server.get("/*", CatchAllHandler);

View file

@ -1,3 +1,4 @@
import * as crypto from "crypto";
import type { NextFunction, Request, Response } from "express";
import { hasJwtCookie } from "./utils/has-jwt-cookie";
@ -8,9 +9,20 @@ export function setDefaultCsp({
res: Response;
next: NextFunction;
}) {
res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
res.setHeader(
"Content-Security-Policy",
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:`
`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();
@ -25,7 +37,7 @@ export function setDefaultCsp({
export function setCacheControl(
req: Request,
res: Response,
next: NextFunction
next: NextFunction,
) {
if (process.env.NODE_ENV !== "production") {
return next();
@ -43,7 +55,7 @@ export function setCacheControl(
if (hasJwtCookie(req)) {
caching = "private";
} 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 sharp from "sharp";
import { favIconPngUrl, favIconUrl } from "../../shared/config";
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
import { IsoDataOptionalSite } from "../../shared/interfaces";
import { buildThemeList } from "./build-themes-list";
import { fetchIconPng } from "./fetch-icon-png";
@ -14,7 +14,8 @@ let appleTouchIcon: string | undefined = undefined;
export async function createSsrHtml(
root: string,
isoData: IsoDataOptionalSite
isoData: IsoDataOptionalSite,
cspNonce: string,
) {
const site = isoData.site_res;
@ -22,10 +23,16 @@ export async function createSsrHtml(
(await buildThemeList())[0]
}.css" />`;
const customHtmlHeaderScriptTag = new RegExp("<script", "g");
const customHtmlHeaderWithNonce = customHtmlHeader.replace(
customHtmlHeaderScriptTag,
`<script nonce="${cspNonce}"`,
);
if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${await sharp(
await fetchIconPng(site.site_view.site.icon)
await fetchIconPng(site.site_view.site.icon),
)
.resize(180, 180)
.extend({
@ -45,28 +52,28 @@ export async function createSsrHtml(
process.env["LEMMY_UI_DEBUG"] === "true"
? renderToString(
<>
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
</>
<script
nonce={cspNonce}
src="//cdn.jsdelivr.net/npm/eruda"
></script>
<script nonce={cspNonce}>eruda.init();</script>
</>,
)
: "";
const helmet = Helmet.renderStatic();
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
return `
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
<script>window.isoData = ${serialize(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script>
<script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>
<!-- A remote debugging utility for mobile -->
${erudaStr}
<!-- Custom injected script -->
${customHtmlHeader}
${customHtmlHeaderWithNonce}
${helmet.title.toString()}
${helmet.meta.toString()}

View file

@ -1,4 +1,3 @@
import { getHttpBaseExternal } from "@utils/env";
import { readFile } from "fs/promises";
import { GetSiteResponse } from "lemmy-js-client";
import path from "path";
@ -11,7 +10,7 @@ const defaultLogoPathDirectory = path.join(
process.cwd(),
"dist",
"assets",
"icons"
"icons",
);
export default async function ({
@ -21,15 +20,13 @@ export default async function ({
local_site: { community_creation_admin_only },
},
}: GetSiteResponse) {
const url = getHttpBaseExternal();
const icon = site.icon ? await fetchIconPng(site.icon) : null;
return {
name: site.name,
description: site.description ?? "A link aggregator for the fediverse",
start_url: url,
scope: url,
start_url: "/",
scope: "/",
display: "standalone",
id: "/",
background_color: "#222222",
@ -37,7 +34,7 @@ export default async function ({
icons: await Promise.all(
iconSizes.map(async size => {
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"));
if (icon) {
@ -54,7 +51,7 @@ export default async function ({
src: `data:image/png;base64,${src}`,
purpose: "any maskable",
};
})
}),
),
shortcuts: [
{
@ -76,7 +73,8 @@ export default async function ({
description: "Create a post.",
},
].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",
@ -85,7 +83,7 @@ export default async function ({
description: "Create a community",
},
]
: []
: [],
),
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 { Component, RefObject, createRef, linkEvent } from "inferno";
import { Provider } from "inferno-i18next-dess";
@ -14,6 +14,8 @@ import { Footer } from "./footer";
import { Navbar } from "./navbar";
import "./styles.scss";
import { Theme } from "./theme";
import AnonymousGuard from "../common/anonymous-guard";
import { CodeTheme } from "./code-theme";
interface AppProps {
user?: MyUserInfo;
@ -54,7 +56,10 @@ export class App extends Component<AppProps, any> {
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
</button>
{siteView && (
<Theme defaultTheme={siteView.local_site.default_theme} />
<>
<Theme defaultTheme={siteView.local_site.default_theme} />
<CodeTheme />
</>
)}
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
@ -75,9 +80,13 @@ export class App extends Component<AppProps, any> {
<div tabIndex={-1}>
{RouteComponent &&
(isAuthPath(path ?? "") ? (
<AuthGuard>
<AuthGuard {...routeProps}>
<RouteComponent {...routeProps} />
</AuthGuard>
) : isAnonymousPath(path ?? "") ? (
<AnonymousGuard>
<RouteComponent {...routeProps} />
</AnonymousGuard>
) : (
<RouteComponent {...routeProps} />
))}
@ -86,7 +95,7 @@ export class App extends Component<AppProps, any> {
);
}}
/>
)
),
)}
<Route component={ErrorPage} />
</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 { removeAuthParam } from "@utils/helpers";
import { Component } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
@ -56,11 +55,7 @@ export class ErrorPage extends Component<any, any> {
</>
)}
{errorPageData?.error && (
<T
i18nKey="error_code_message"
parent="p"
interpolation={{ error: removeAuthParam(errorPageData.error) }}
>
<T i18nKey="error_code_message" parent="p">
#<strong className="text-danger">#</strong>#
</T>
)}

View file

@ -1,31 +1,30 @@
import { myAuth, showAvatars } from "@utils/app";
import { showAvatars } from "@utils/app";
import { isBrowser } from "@utils/browser";
import { numToSI, poll } from "@utils/helpers";
import { numToSI } from "@utils/helpers";
import { amAdmin, canCreateCommunity } from "@utils/roles";
import { Component, createRef, linkEvent } from "inferno";
import { NavLink } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { donateLemmyUrl } from "../../config";
import {
GetReportCountResponse,
GetSiteResponse,
GetUnreadCountResponse,
GetUnreadRegistrationApplicationCountResponse,
} from "lemmy-js-client";
import { donateLemmyUrl, updateUnreadCountsInterval } from "../../config";
import { I18NextService, UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
I18NextService,
UserService,
UnreadCounterService,
} from "../../services";
import { toast } from "../../toast";
import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image";
import { Subscription } from "rxjs";
interface NavbarProps {
siteRes?: GetSiteResponse;
}
interface NavbarState {
unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
unreadReportCountRes: RequestState<GetReportCountResponse>;
unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
onSiteBanner?(url: string): any;
unreadInboxCount: number;
unreadReportCount: number;
unreadApplicationCount: number;
}
function handleCollapseClick(i: Navbar) {
@ -44,13 +43,17 @@ function handleLogOut(i: Navbar) {
}
export class Navbar extends Component<NavbarProps, NavbarState> {
state: NavbarState = {
unreadInboxCountRes: { state: "empty" },
unreadReportCountRes: { state: "empty" },
unreadApplicationCountRes: { state: "empty" },
};
collapseButtonRef = createRef<HTMLButtonElement>();
mobileMenuRef = createRef<HTMLDivElement>();
unreadInboxCountSubscription: Subscription;
unreadReportCountSubscription: Subscription;
unreadApplicationCountSubscription: Subscription;
state: NavbarState = {
unreadInboxCount: 0,
unreadReportCount: 0,
unreadApplicationCount: 0,
};
constructor(props: any, context: any) {
super(props, context);
@ -63,7 +66,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
if (isBrowser()) {
// On the first load, check the unreads
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();
document.addEventListener("mouseup", this.handleOutsideMenuClick);
@ -72,6 +86,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
componentWillUnmount() {
document.removeEventListener("mouseup", this.handleOutsideMenuClick);
this.unreadInboxCountSubscription.unsubscribe();
this.unreadReportCountSubscription.unsubscribe();
this.unreadApplicationCountSubscription.unsubscribe();
}
// TODO class active corresponding to current pages
@ -103,34 +120,34 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
to="/inbox"
className="p-1 nav-link border-0 nav-messages"
title={I18NextService.i18n.t("unread_messages", {
count: Number(this.state.unreadApplicationCountRes.state),
formattedCount: numToSI(this.unreadInboxCount),
count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.state.unreadInboxCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="bell" />
{this.unreadInboxCount > 0 && (
{this.state.unreadInboxCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadInboxCount)}
{numToSI(this.state.unreadInboxCount)}
</span>
)}
</NavLink>
</li>
{this.moderatesSomething && (
{UserService.Instance.moderatesSomething && (
<li className="nav-item nav-item-icon">
<NavLink
to="/reports"
className="p-1 nav-link border-0"
title={I18NextService.i18n.t("unread_reports", {
count: Number(this.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount),
count: Number(this.state.unreadReportCount),
formattedCount: numToSI(this.state.unreadReportCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="shield" />
{this.unreadReportCount > 0 && (
{this.state.unreadReportCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadReportCount)}
{numToSI(this.state.unreadReportCount)}
</span>
)}
</NavLink>
@ -144,16 +161,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
title={I18NextService.i18n.t(
"unread_registration_applications",
{
count: Number(this.unreadApplicationCount),
formattedCount: numToSI(this.unreadApplicationCount),
}
count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI(
this.state.unreadApplicationCount,
),
},
)}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="clipboard" />
{this.unreadApplicationCount > 0 && (
{this.state.unreadApplicationCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadApplicationCount)}
{numToSI(this.state.unreadApplicationCount)}
</span>
)}
</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"
to="/inbox"
title={I18NextService.i18n.t("unread_messages", {
count: Number(this.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount),
count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.state.unreadInboxCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="bell" />
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
{I18NextService.i18n.t("unread_messages", {
count: Number(this.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount),
count: Number(this.state.unreadInboxCount),
formattedCount: numToSI(this.state.unreadInboxCount),
})}
</span>
{this.unreadInboxCount > 0 && (
{this.state.unreadInboxCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadInboxCount)}
{numToSI(this.state.unreadInboxCount)}
</span>
)}
</NavLink>
</li>
{this.moderatesSomething && (
{UserService.Instance.moderatesSomething && (
<li id="navModeration" className="nav-item">
<NavLink
className="nav-link d-inline-flex align-items-center d-md-inline-block"
to="/reports"
title={I18NextService.i18n.t("unread_reports", {
count: Number(this.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount),
count: Number(this.state.unreadReportCount),
formattedCount: numToSI(this.state.unreadReportCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="shield" />
<span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
{I18NextService.i18n.t("unread_reports", {
count: Number(this.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount),
count: Number(this.state.unreadReportCount),
formattedCount: numToSI(
this.state.unreadReportCount,
),
})}
</span>
{this.unreadReportCount > 0 && (
{this.state.unreadReportCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadReportCount)}
{numToSI(this.state.unreadReportCount)}
</span>
)}
</NavLink>
@ -321,11 +342,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
title={I18NextService.i18n.t(
"unread_registration_applications",
{
count: Number(this.unreadApplicationCount),
count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI(
this.unreadApplicationCount
this.state.unreadApplicationCount,
),
}
},
)}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
@ -334,16 +355,16 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{I18NextService.i18n.t(
"unread_registration_applications",
{
count: Number(this.unreadApplicationCount),
count: Number(this.state.unreadApplicationCount),
formattedCount: numToSI(
this.unreadApplicationCount
this.state.unreadApplicationCount,
),
}
},
)}
</span>
{this.unreadApplicationCount > 0 && (
{this.state.unreadApplicationCount > 0 && (
<span className="mx-1 badge text-bg-light">
{numToSI(this.unreadApplicationCount)}
{numToSI(this.state.unreadApplicationCount)}
</span>
)}
</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() {
return this.context.router.history.location.pathname;
}

View file

@ -21,7 +21,10 @@ export class Theme extends Component<Props> {
/>
</Helmet>
);
} else if (this.props.defaultTheme != "browser") {
} else if (
this.props.defaultTheme !== "browser" &&
this.props.defaultTheme !== "browser-compact"
) {
return (
<Helmet>
<link
@ -31,6 +34,25 @@ export class Theme extends Component<Props> {
/>
</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 {
return (
<Helmet>

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
@ -11,6 +10,7 @@ import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node";
import { EMPTY_REQUEST } from "../../services/HttpService";
interface CommentReportProps {
report: CommentReportView;
@ -33,9 +33,9 @@ export class CommentReport extends Component<
}
componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>,
): void {
if (this.props != nextProps) {
if (this.props !== nextProps) {
this.setState({ loading: false });
}
}
@ -44,7 +44,7 @@ export class CommentReport extends Component<
const r = this.props.report;
const comment = r.comment;
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 )
@ -98,8 +98,8 @@ export class CommentReport extends Component<
onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
onCreateComment={() => Promise.resolve({ state: "empty" })}
onEditComment={() => Promise.resolve({ state: "empty" })}
onCreateComment={() => Promise.resolve(EMPTY_REQUEST)}
onEditComment={() => Promise.resolve(EMPTY_REQUEST)}
/>
<div>
{I18NextService.i18n.t("reporter")}:{" "}
@ -149,7 +149,6 @@ export class CommentReport extends Component<
i.props.onResolveReport({
report_id: i.props.report.comment_report.id,
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 { Redirect } from "inferno-router";
import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { UserService } from "../../services";
import { Spinner } from "./icon";
function AuthGuard(props: { children?: InfernoNode }) {
if (!UserService.Instance.myUserInfo) {
return <Redirect to="/login" />;
} else {
return props.children;
interface AuthGuardState {
hasRedirected: boolean;
}
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 = (
counts: CommunityAggregates | SiteAggregates
counts: CommunityAggregates | SiteAggregates,
): counts is CommunityAggregates => {
return "subscribers" in counts;
};
const isSiteAggregates = (
counts: CommunityAggregates | SiteAggregates
counts: CommunityAggregates | SiteAggregates,
): counts is SiteAggregates => {
return "communities" in counts;
};
@ -34,7 +34,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
}
},
)}
>
{I18NextService.i18n.t("number_of_users", {
@ -50,7 +50,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
}
},
)}
>
{I18NextService.i18n.t("number_of_users", {
@ -66,7 +66,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
}
},
)}
>
{I18NextService.i18n.t("number_of_users", {
@ -82,7 +82,7 @@ export const Badges = ({ counts, communityId }: BadgesProps) => {
{
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
}
},
)}
>
{I18NextService.i18n.t("number_of_users", {

View file

@ -48,6 +48,9 @@ export class CommentSortSelect extends Component<
{I18NextService.i18n.t("sort_type")}
</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={"New"}>{I18NextService.i18n.t("new")}</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";
interface EmojiMartProps {
@ -7,21 +7,24 @@ interface EmojiMartProps {
}
export class EmojiMart extends Component<EmojiMartProps> {
div: RefObject<HTMLDivElement>;
constructor(props: any, context: any) {
super(props, context);
this.div = createRef();
this.handleEmojiClick = this.handleEmojiClick.bind(this);
}
componentDidMount() {
const div: any = document.getElementById("emoji-picker");
if (div) {
div.appendChild(
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)
);
}
this.div.current?.appendChild(
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions) as any,
);
}
render() {
return <div id="emoji-picker"></div>;
return <div id="emoji-picker" ref={this.div} />;
}
handleEmojiClick(e: any) {

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ export class ListingTypeSelect extends Component<
}
static getDerivedStateFromProps(
props: ListingTypeSelectProps
props: ListingTypeSelectProps,
): ListingTypeSelectState {
return {
type_: props.type_,
@ -107,6 +107,27 @@ export class ListingTypeSelect extends Component<
>
{I18NextService.i18n.t("all")}
</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>
);
}

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 autosize from "autosize";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import { Language } from "lemmy-js-client";
import {
concurrentImageUpload,
@ -19,9 +20,8 @@ import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiPicker } from "./emoji-picker";
import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select";
import NavigationPrompt from "./navigation-prompt";
import ProgressBar from "./progress-bar";
import validUrl from "@utils/helpers/valid-url";
interface MarkdownTextAreaProps {
/**
* Initial content inside the textarea
@ -49,7 +49,7 @@ interface MarkdownTextAreaProps {
hideNavigationWarnings?: boolean;
onContentChange?(val: string): void;
onReplyCancel?(): void;
onSubmit?(content: string, formId: string, languageId?: number): void;
onSubmit?(content: string, languageId?: number): void;
allLanguages: Language[]; // TODO should probably be nullable
siteLanguages: number[]; // TODO same
}
@ -89,6 +89,7 @@ export class MarkdownTextArea extends Component<
super(props, context);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.handleEmoji = this.handleEmoji.bind(this);
if (isBrowser()) {
this.tribute = setupTribute();
@ -138,18 +139,14 @@ export class MarkdownTextArea extends Component<
render() {
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 (
<form
className="markdown-textarea"
id={this.formId}
onSubmit={linkEvent(this, this.handleSubmit)}
>
<NavigationPrompt
<Prompt
message={I18NextService.i18n.t("block_leaving")}
when={
!this.props.hideNavigationWarnings &&
!!this.state.content &&
@ -167,9 +164,7 @@ export class MarkdownTextArea extends Component<
{this.getFormatButton("bold", this.handleInsertBold)}
{this.getFormatButton("italic", this.handleInsertItalic)}
{this.getFormatButton("link", this.handleInsertLink)}
<EmojiPicker
onEmojiClick={e => this.handleEmoji(this, e)}
></EmojiPicker>
<EmojiPicker onEmojiClick={this.handleEmoji}></EmojiPicker>
<label
htmlFor={`file-upload-${this.id}`}
className={classNames("mb-0", {
@ -206,7 +201,7 @@ export class MarkdownTextArea extends Component<
{this.getFormatButton("header", this.handleInsertHeader)}
{this.getFormatButton(
"strikethrough",
this.handleInsertStrikethrough
this.handleInsertStrikethrough,
)}
{this.getFormatButton("quote", this.handleInsertQuote)}
{this.getFormatButton("list", this.handleInsertList)}
@ -214,7 +209,7 @@ export class MarkdownTextArea extends Component<
{this.getFormatButton("subscript", this.handleInsertSubscript)}
{this.getFormatButton(
"superscript",
this.handleInsertSuperscript
this.handleInsertSuperscript,
)}
{this.getFormatButton("spoiler", this.handleInsertSpoiler)}
<a
@ -234,11 +229,11 @@ export class MarkdownTextArea extends Component<
"form-control border-0 rounded-top-0 rounded-bottom",
{
"d-none": this.state.previewMode,
}
},
)}
value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
onPaste={linkEvent(this, this.handlePaste)}
onKeyDown={linkEvent(this, this.handleKeyBinds)}
required
disabled={this.isDisabled}
@ -263,7 +258,7 @@ export class MarkdownTextArea extends Component<
value={this.state.imageUploadStatus.uploaded}
max={this.state.imageUploadStatus.total}
text={
I18NextService.i18n.t("pictures_uploded_progess", {
I18NextService.i18n.t("pictures_uploaded_progess", {
uploaded: this.state.imageUploadStatus.uploaded,
total: this.state.imageUploadStatus.total,
}) ?? undefined
@ -333,7 +328,7 @@ export class MarkdownTextArea extends Component<
getFormatButton(
type: NoOptionI18nKeys,
handleClick: (i: MarkdownTextArea, event: any) => void
handleClick: (i: MarkdownTextArea, event: any) => void,
) {
let iconType: string;
@ -363,26 +358,65 @@ export class MarkdownTextArea extends Component<
);
}
handleEmoji(i: MarkdownTextArea, e: any) {
handleEmoji(e: any) {
let value = e.native;
if (value == null) {
if (!value) {
const emoji = customEmojisLookup.get(e.id)?.custom_emoji;
if (emoji) {
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`;
value = `![${emoji.alt_text}](${emoji.image_url} "emoji ${emoji.shortcode}")`;
}
}
i.setState({
content: `${i.state.content ?? ""} ${value} `,
this.setState({
content: `${this.state.content ?? ""} ${value} `,
});
i.contentChange();
const textarea: any = document.getElementById(i.id);
this.contentChange();
const textarea: any = document.getElementById(this.id);
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];
if (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),
formattedCount: numToSI(maxUploadImages),
}),
"danger"
"danger",
);
} else {
i.setState({
@ -429,7 +463,7 @@ export class MarkdownTextArea extends Component<
uploaded: (imageUploadStatus?.uploaded ?? 0) + 1,
},
}));
})
}),
);
} catch (e) {
errorOccurred = true;
@ -481,7 +515,7 @@ export class MarkdownTextArea extends Component<
// Keybind handler
// Keybinds inspired by github comment area
handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
if (event.ctrlKey || event.metaKey) {
if (platform.isMac() ? event.metaKey : event.ctrlKey) {
switch (event.key) {
case "k": {
i.handleInsertLink(i, event);
@ -539,7 +573,7 @@ export class MarkdownTextArea extends Component<
event.preventDefault();
if (i.state.content) {
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({
content: `${content?.substring(
0,
start
start,
)}[${selectedText}]()${content?.substring(end)}`,
});
textarea.focus();
@ -589,7 +623,7 @@ export class MarkdownTextArea extends Component<
simpleSurroundBeforeAfter(
beforeChars: string,
afterChars: string,
emptyChars = "___"
emptyChars = "___",
) {
const content = this.state.content ?? "";
if (!this.state.content) {
@ -604,7 +638,7 @@ export class MarkdownTextArea extends Component<
this.setState({
content: `${content?.substring(
0,
start
start,
)}${beforeChars}${selectedText}${afterChars}${content?.substring(end)}`,
});
} else {
@ -619,12 +653,12 @@ export class MarkdownTextArea extends Component<
if (start !== end) {
textarea.setSelectionRange(
start + beforeChars.length,
end + afterChars.length
end + afterChars.length,
);
} else {
textarea.setSelectionRange(
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() {
const updated = this.props.updated;
let line = `${capitalizeFirstLetter(
I18NextService.i18n.t("created")
I18NextService.i18n.t("created"),
)}: ${formatDate(this.props.published)}`;
if (updated) {
line += `\n\n\n${capitalizeFirstLetter(
I18NextService.i18n.t("modified")
I18NextService.i18n.t("modified"),
)} ${formatDate(updated)}`;
}
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 {
page: number;
onChange(val: number): any;
nextDisabled: boolean;
}
export class Paginator extends Component<PaginatorProps, any> {
@ -15,7 +16,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<div className="paginator my-2">
<button
className="btn btn-secondary me-2"
disabled={this.props.page == 1}
disabled={this.props.page === 1}
onClick={linkEvent(this, this.handlePrev)}
>
{I18NextService.i18n.t("prev")}
@ -23,6 +24,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<button
className="btn btn-secondary"
onClick={linkEvent(this, this.handleNext)}
disabled={this.props.nextDisabled || false}
>
{I18NextService.i18n.t("next")}
</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 { Component } from "inferno";
import { UserService } from "../../services";
const iconThumbnailSize = 96;
const thumbnailSize = 256;
@ -13,6 +15,7 @@ interface PictrsImageProps {
nsfw?: boolean;
iconOverlay?: boolean;
pushup?: boolean;
cardTop?: boolean;
}
export class PictrsImage extends Component<PictrsImageProps, any> {
@ -21,28 +24,40 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
}
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 (
<picture>
<source srcSet={this.src("webp")} type="image/webp" />
<source srcSet={this.props.src} />
<source srcSet={src} />
<source srcSet={this.src("jpg")} type="image/jpeg" />
<img
src={this.props.src}
src={src}
alt={this.alt()}
title={this.alt()}
loading="lazy"
className={classNames("overflow-hidden pictrs-image", {
"img-fluid": !this.props.icon && !this.props.iconOverlay,
banner: this.props.banner,
"img-fluid": !(icon || iconOverlay),
banner,
"thumbnail rounded object-fit-cover":
this.props.thumbnail && !this.props.icon && !this.props.banner,
"img-expanded slight-radius":
!this.props.thumbnail && !this.props.icon,
"img-blur": this.props.thumbnail && this.props.nsfw,
"object-fit-cover img-icon me-1": this.props.icon,
thumbnail && !(icon || banner),
"img-expanded slight-radius": !(thumbnail || icon),
"img-blur": thumbnail && nsfw,
"object-fit-cover img-icon me-1": icon,
"img-blur-icon": icon && blur_image,
"img-blur-thumb": thumbnail && blur_image,
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
this.props.iconOverlay,
"avatar-pushup": this.props.pushup,
iconOverlay,
"avatar-pushup": pushup,
"card-img-top": cardTop,
})}
/>
</picture>
@ -56,7 +71,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
const split = this.props.src.split("/pictrs/image/");
// If theres not multiple, then its not a pictrs image
if (split.length == 1) {
if (split.length === 1) {
return this.props.src;
}

View file

@ -1,4 +1,3 @@
import { myAuthRequired } from "@utils/app";
import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
@ -42,9 +41,9 @@ export class RegistrationApplication extends Component<
componentWillReceiveProps(
nextProps: Readonly<
{ children?: InfernoNode } & RegistrationApplicationProps
>
>,
): void {
if (this.props != nextProps) {
if (this.props !== nextProps) {
this.setState({
denyExpanded: false,
approveLoading: false,
@ -149,7 +148,6 @@ export class RegistrationApplication extends Component<
i.props.onApproveApplication({
id: i.props.application.registration_application.id,
approve: true,
auth: myAuthRequired(),
});
}
@ -160,7 +158,6 @@ export class RegistrationApplication extends Component<
id: i.props.application.registration_application.id,
approve: false,
deny_reason: i.state.denyReason,
auth: myAuthRequired(),
});
} else {
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";
import { I18NextService } from "../../services";
import { Icon, Spinner } from "./icon";
import { LoadingEllipses } from "./loading-ellipses";
interface SearchableSelectProps {
id: string;
@ -22,7 +23,6 @@ interface SearchableSelectProps {
interface SearchableSelectState {
selectedIndex: number;
searchText: string;
loadingEllipses: string;
}
function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
@ -70,12 +70,10 @@ export class SearchableSelect extends Component<
> {
searchInputRef: RefObject<HTMLInputElement> = createRef();
toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
state: SearchableSelectState = {
selectedIndex: 0,
searchText: "",
loadingEllipses: "...",
};
constructor(props: SearchableSelectProps, context: any) {
@ -83,7 +81,7 @@ export class SearchableSelect extends Component<
if (props.value) {
let selectedIndex = props.options.findIndex(
({ value }) => value === props.value?.toString()
({ value }) => value === props.value?.toString(),
);
if (selectedIndex < 0) {
@ -99,7 +97,7 @@ export class SearchableSelect extends Component<
render() {
const { id, options, onSearch, loading } = this.props;
const { searchText, selectedIndex, loadingEllipses } = this.state;
const { searchText, selectedIndex } = this.state;
return (
<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)}
ref={this.toggleButtonRef}
>
{loading
? `${I18NextService.i18n.t("loading")}${loadingEllipses}`
: options[selectedIndex].label}
{loading ? (
<>
{I18NextService.i18n.t("loading")}
<LoadingEllipses />
</>
) : (
options[selectedIndex].label
)}
</button>
<div className="modlog-choices-font-size dropdown-menu w-100 p-2">
<div className="input-group">
@ -140,7 +143,7 @@ export class SearchableSelect extends Component<
(onSearch || searchText.length === 0
? options
: options.filter(({ label }) =>
label.toLowerCase().includes(searchText.toLowerCase())
label.toLowerCase().includes(searchText.toLowerCase()),
)
).map((option, index) => (
<button
@ -180,24 +183,4 @@ export class SearchableSelect extends Component<
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"}>
{I18NextService.i18n.t("active")}
</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={"Old"}>{I18NextService.i18n.t("old")}</option>
{!this.props.hideMostComments && [
@ -79,6 +85,15 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<option value={"TopMonth"}>
{I18NextService.i18n.t("top_month")}
</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={"TopAll"}>{I18NextService.i18n.t("top_all")}</option>
</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
className={classNames(
"row d-inline-flex gx-1",
this.props.classNames
this.props.classNames,
)}
>
{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 classNames from "classnames";
import { Component, linkEvent } from "inferno";
@ -53,7 +53,6 @@ const handleUpvote = (i: VoteButtons) => {
i.props.onVote({
comment_id: i.props.id,
score: newVote(VoteType.Upvote, i.props.my_vote),
auth: myAuthRequired(),
});
break;
case VoteContentType.Post:
@ -61,7 +60,6 @@ const handleUpvote = (i: VoteButtons) => {
i.props.onVote({
post_id: i.props.id,
score: newVote(VoteType.Upvote, i.props.my_vote),
auth: myAuthRequired(),
});
}
};
@ -73,7 +71,6 @@ const handleDownvote = (i: VoteButtons) => {
i.props.onVote({
comment_id: i.props.id,
score: newVote(VoteType.Downvote, i.props.my_vote),
auth: myAuthRequired(),
});
break;
case VoteContentType.Post:
@ -81,7 +78,6 @@ const handleDownvote = (i: VoteButtons) => {
i.props.onVote({
post_id: i.props.id,
score: newVote(VoteType.Downvote, i.props.my_vote),
auth: myAuthRequired(),
});
}
};
@ -193,7 +189,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
<button
type="button"
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)}
data-tippy-content={I18NextService.i18n.t("upvote")}
@ -220,7 +216,7 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
<button
type="button"
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)}
data-tippy-content={I18NextService.i18n.t("downvote")}

View file

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

View file

@ -1,6 +1,6 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter, randomStr } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import {
CommunityView,
CreateCommunity,
@ -12,7 +12,6 @@ import { Icon, Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt";
interface CommunityFormProps {
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"
onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}
>
<NavigationPrompt
<Prompt
message={I18NextService.i18n.t("block_leaving")}
when={
!this.props.loading &&
!!(
@ -230,7 +230,7 @@ export class CommunityForm extends Component<
checked={this.state.form.posting_restricted_to_mods}
onChange={linkEvent(
this,
this.handleCommunityPostingRestrictedToMods
this.handleCommunityPostingRestrictedToMods,
)}
/>
</div>
@ -278,7 +278,6 @@ export class CommunityForm extends Component<
event.preventDefault();
i.setState({ submitted: true });
const cForm = i.state.form;
const auth = myAuthRequired();
const cv = i.props.community_view;
@ -292,7 +291,6 @@ export class CommunityForm extends Component<
nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages,
auth,
});
} else {
if (cForm.title && cForm.name) {
@ -305,7 +303,6 @@ export class CommunityForm extends Component<
nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages,
auth,
});
}
}
@ -329,7 +326,7 @@ export class CommunityForm extends Component<
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
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() {
const community = this.props.community;
let name_: string, title: string, link: string;
const local = community.local == null ? true : community.local;
let title: string, link: string;
const local = community.local === null ? true : community.local;
const domain = hostname(community.actor_id);
if (local) {
name_ = community.name;
title = community.title;
link = `/c/${community.name}`;
} else {
const domain = hostname(community.actor_id);
name_ = `${community.name}@${domain}`;
const name_ = `${community.name}@${domain}`;
title = `${community.title}@${domain}`;
link = !this.props.realLink ? `/c/${name_}` : community.actor_id;
}
const apubName = `!${name_}`;
const apubName = `!${community.name}@${domain}`;
const displayName = this.props.useApubName ? apubName : title;
return !this.props.realLink ? (
<Link
@ -58,12 +57,14 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
avatarAndName(displayName: string) {
const icon = this.props.community.icon;
const nsfw = this.props.community.nsfw;
return (
<>
{!this.props.hideAvatar &&
!this.props.community.removed &&
showAvatars() &&
icon && <PictrsImage src={icon} icon />}
icon && <PictrsImage src={icon} icon nsfw={nsfw} />}
<span className="overflow-wrap-anywhere">{displayName}</span>
</>
);

View file

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

View file

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

View file

@ -1,9 +1,4 @@
import {
fetchThemeList,
myAuthRequired,
setIsoData,
showLocal,
} from "@utils/app";
import { fetchThemeList, setIsoData, showLocal } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers";
import { RouteDataResponse } from "@utils/types";
import classNames from "classnames";
@ -21,7 +16,12 @@ import {
import { InitialFetchRequest } from "../../interfaces";
import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
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 { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@ -55,9 +55,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
siteRes: this.isoData.site_res,
banned: [],
currentTab: "site",
bannedRes: { state: "empty" },
instancesRes: { state: "empty" },
leaveAdminTeamRes: { state: "empty" },
bannedRes: EMPTY_REQUEST,
instancesRes: EMPTY_REQUEST,
leaveAdminTeamRes: EMPTY_REQUEST,
loading: false,
themeList: [],
isIsomorphic: false,
@ -85,16 +85,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
}
static async fetchInitialData({
auth,
client,
}: InitialFetchRequest): Promise<AdminSettingsData> {
return {
bannedRes: await client.getBannedPersons({
auth: auth as string,
}),
instancesRes: await client.getFederatedInstances({
auth: auth as string,
}),
bannedRes: await client.getBannedPersons(),
instancesRes: await client.getFederatedInstances(),
};
}
@ -150,15 +145,26 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
loading={this.state.loading}
/>
</div>
<div className="col-12 col-md-6">
{this.admins()}
<hr />
{this.bannedUsers()}
</div>
<div className="col-12 col-md-6">{this.admins()}</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",
label: "Rate Limiting",
@ -230,16 +236,14 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
async fetchData() {
this.setState({
bannedRes: { state: "loading" },
instancesRes: { state: "loading" },
bannedRes: LOADING_REQUEST,
instancesRes: LOADING_REQUEST,
themeList: [],
});
const auth = myAuthRequired();
const [bannedRes, instancesRes, themeList] = await Promise.all([
HttpService.client.getBannedPersons({ auth }),
HttpService.client.getFederatedInstances({ auth }),
HttpService.client.getBannedPersons(),
HttpService.client.getFederatedInstances(),
fetchThemeList(),
]);
@ -274,7 +278,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
onClick={linkEvent(this, this.handleLeaveAdminTeam)}
className="btn btn-danger mb-2"
>
{this.state.leaveAdminTeamRes.state == "loading" ? (
{this.state.leaveAdminTeamRes.state === "loading" ? (
<Spinner />
) : (
I18NextService.i18n.t("leave_admin_team")
@ -295,7 +299,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
const bans = this.state.bannedRes.data.banned;
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">
{bans.map(banned => (
<li key={banned.person.id} className="list-inline-item">
@ -334,11 +338,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
}
async handleLeaveAdminTeam(i: AdminSettings) {
i.setState({ leaveAdminTeamRes: { state: "loading" } });
i.setState({ leaveAdminTeamRes: LOADING_REQUEST });
this.setState({
leaveAdminTeamRes: await HttpService.client.leaveAdmin({
auth: myAuthRequired(),
}),
leaveAdminTeamRes: await HttpService.client.leaveAdmin(),
});
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 { Component, linkEvent } from "inferno";
import {
@ -11,7 +11,6 @@ import { customEmojisLookup } from "../../markdown";
import { HttpService, I18NextService } from "../../services";
import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
@ -66,17 +65,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
this.handlePageChange = this.handlePageChange.bind(this);
this.handleEmojiClick = this.handleEmojiClick.bind(this);
}
get documentTitle(): string {
return I18NextService.i18n.t("custom_emojis");
}
render() {
return (
<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>
{customEmojisLookup.size > 0 && (
<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 +
this.itemsPerPage
)
this.itemsPerPage,
),
)
.map((cv, index) => (
<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"
htmlFor={`file-uploader-${index}`}
data-tippy-content={I18NextService.i18n.t(
"upload_image"
"upload_image",
)}
>
{capitalizeFirstLetter(
I18NextService.i18n.t("upload")
I18NextService.i18n.t("upload"),
)}
<input
name={`file-uploader-${index}`}
@ -153,7 +144,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
className="d-none"
onChange={linkEvent(
{ form: this, index: index },
this.handleImageUpload
this.handleImageUpload,
)}
/>
</label>
@ -168,7 +159,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.shortcode}
onInput={linkEvent(
{ form: this, index: index },
this.handleEmojiShortCodeChange
this.handleEmojiShortCodeChange,
)}
/>
</td>
@ -180,7 +171,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.category}
onInput={linkEvent(
{ form: this, index: index },
this.handleEmojiCategoryChange
this.handleEmojiCategoryChange,
)}
/>
</td>
@ -192,7 +183,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.image_url}
onInput={linkEvent(
{ form: this, index: index, overrideValue: null },
this.handleEmojiImageUrlChange
this.handleEmojiImageUrlChange,
)}
/>
</td>
@ -204,7 +195,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.alt_text}
onInput={linkEvent(
{ form: this, index: index },
this.handleEmojiAltTextChange
this.handleEmojiAltTextChange,
)}
/>
</td>
@ -216,7 +207,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
value={cv.keywords}
onInput={linkEvent(
{ form: this, index: index },
this.handleEmojiKeywordChange
this.handleEmojiKeywordChange,
)}
/>
</td>
@ -231,7 +222,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
}
onClick={linkEvent(
{ i: this, cv: cv },
this.handleEditEmojiClick
this.handleEditEmojiClick,
)}
data-tippy-content={I18NextService.i18n.t("save")}
aria-label={I18NextService.i18n.t("save")}
@ -241,7 +232,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
<Spinner />
) : (
capitalizeFirstLetter(
I18NextService.i18n.t("save")
I18NextService.i18n.t("save"),
)
)}
</button>
@ -250,7 +241,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
{ i: this, index: index, cv: cv },
this.handleDeleteEmojiClick
this.handleDeleteEmojiClick,
)}
data-tippy-content={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")}
</button>
<Paginator page={this.state.page} onChange={this.handlePageChange} />
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
nextDisabled={false}
/>
</div>
</div>
);
@ -290,8 +285,8 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
cv.shortcode.length > 0;
const noDuplicateShortCodes =
this.state.customEmojis.filter(
x => x.shortcode == cv.shortcode && x.id != cv.id
).length == 0;
x => x.shortcode === cv.shortcode && x.id !== cv.id,
).length === 0;
return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
}
@ -308,7 +303,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
const view = customEmojisLookup.get(e.id);
if (view) {
const page = this.state.customEmojis.find(
x => x.id == view.custom_emoji.id
x => x.id === view.custom_emoji.id,
)?.page;
if (page) {
this.setState({ page: page });
@ -319,7 +314,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiCategoryChange(
props: { form: EmojiForm; index: number },
event: any
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
@ -335,7 +330,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiShortCodeChange(
props: { form: EmojiForm; index: number },
event: any
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
@ -355,7 +350,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
index,
overrideValue,
}: { form: EmojiForm; index: number; overrideValue: string | null },
event: any
event: any,
) {
form.setState(prevState => {
const custom_emojis = [...form.state.customEmojis];
@ -376,7 +371,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
changed: true,
loading: false,
}
: ce
: ce,
),
};
});
@ -384,7 +379,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiAltTextChange(
props: { form: EmojiForm; index: number },
event: any
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
@ -400,7 +395,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleEmojiKeywordChange(
props: { form: EmojiForm; index: number },
event: any
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
@ -420,10 +415,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
cv: CustomEmojiViewForm;
}) {
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({
id: d.cv.id,
auth: myAuthRequired(),
});
} else {
const custom_emojis = [...d.i.state.customEmojis];
@ -444,7 +438,6 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
image_url: d.cv.image_url,
alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
auth: myAuthRequired(),
});
} else {
d.i.props.onCreate({
@ -453,7 +446,6 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
image_url: d.cv.image_url,
alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
auth: myAuthRequired(),
});
}
}
@ -485,7 +477,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleImageUpload(
{ form, index }: { form: EmojiForm; index: number },
event: any
event: any,
) {
let file: any;
if (event.target) {
@ -498,7 +490,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
form.setState(prevState => ({
...prevState,
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);
form.handleEmojiImageUrlChange(
{ form: form, index: index, overrideValue: res.data.url as string },
event
event,
);
} else if (res.data.msg === "too_large") {
toast(I18NextService.i18n.t("upload_too_large"), "danger");

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter, validEmail } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import { GetSiteResponse } from "lemmy-js-client";
import { HttpService, I18NextService, UserService } from "../../services";
import { HttpService, I18NextService } from "../../services";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@ -30,15 +30,9 @@ export class LoginReset extends Component<any, State> {
super(props, context);
}
componentDidMount() {
if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/");
}
}
get documentTitle(): string {
return `${capitalizeFirstLetter(
I18NextService.i18n.t("forgot_password")
I18NextService.i18n.t("forgot_password"),
)} - ${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 });
if (res.state == "success") {
if (res.state === "success") {
toast(I18NextService.i18n.t("reset_password_mail_sent"));
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 { getQueryParams } from "@utils/helpers";
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 { 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 { HtmlTags } from "../common/html-tags";
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 {
loginRes: RequestState<LoginResponse>;
form: {
username_or_email?: string;
password?: string;
totp_2fa_token?: string;
username_or_email: string;
password: string;
};
showTotp: boolean;
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);
state: State = {
loginRes: { state: "empty" },
form: {},
showTotp: false,
loginRes: EMPTY_REQUEST,
form: {
username_or_email: "",
password: "",
},
siteRes: this.isoData.site_res,
show2faModal: false,
};
constructor(props: any, context: any) {
super(props, context);
}
componentDidMount() {
// Navigate to home if already logged in
if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/");
}
this.handleSubmitTotp = this.handleSubmitTotp.bind(this);
}
get documentTitle(): string {
@ -48,7 +134,7 @@ export class Login extends Component<any, State> {
}
get isLemmyMl(): boolean {
return isBrowser() && window.location.hostname == "lemmy.ml";
return isBrowser() && window.location.hostname === "lemmy.ml";
}
render() {
@ -58,6 +144,12 @@ export class Login extends Component<any, State> {
title={this.documentTitle}
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="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</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() {
return (
<div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<form onSubmit={linkEvent(this, handleLoginSubmit)}>
<h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1>
<div className="mb-3 row">
<label
@ -83,62 +193,26 @@ export class Login extends Component<any, State> {
className="form-control"
id="login-email-or-username"
value={this.state.form.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)}
onInput={linkEvent(this, handleLoginUsernameChange)}
autoComplete="email"
required
minLength={3}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="login-password">
{I18NextService.i18n.t("password")}
</label>
<div className="col-sm-10">
<input
type="password"
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 className="mb-3">
<PasswordInput
id="login-password"
value={this.state.form.password}
onInput={linkEvent(this, handleLoginPasswordChange)}
label={I18NextService.i18n.t("password")}
showForgotLink
/>
</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="col-sm-10">
<button type="submit" className="btn btn-secondary">
{this.state.loginRes.state == "loading" ? (
{this.state.loginRes.state === "loading" ? (
<Spinner />
) : (
I18NextService.i18n.t("login")
@ -150,64 +224,4 @@ export class Login extends Component<any, State> {
</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 classNames from "classnames";
import { Component, FormEventHandler, linkEvent } from "inferno";
@ -88,7 +87,7 @@ function RateLimits({
function handleRateLimitChange(
{ rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm },
event: any
event: any,
) {
ctx.setState(prev => ({
...prev,
@ -101,7 +100,7 @@ function handleRateLimitChange(
function handlePerSecondChange(
{ rateLimitType, ctx }: { rateLimitType: string; ctx: RateLimitsForm },
event: any
event: any,
) {
ctx.setState(prev => ({
...prev,
@ -114,15 +113,12 @@ function handlePerSecondChange(
function submitRateLimitForm(i: RateLimitsForm, event: any) {
event.preventDefault();
const auth = myAuthRequired();
const form: EditSite = Object.entries(i.state.form).reduce(
(acc, [key, val]) => {
acc[`rate_limit_${key}`] = val;
return acc;
},
{
auth,
}
{},
);
i.props.onSaveSite(form);
@ -159,11 +155,11 @@ export default class RateLimitsForm extends Component<
})}
handleRateLimit={linkEvent(
{ rateLimitType, ctx: this },
handleRateLimitChange
handleRateLimitChange,
)}
handleRateLimitPerSecond={linkEvent(
{ rateLimitType, ctx: this },
handlePerSecondChange
handlePerSecondChange,
)}
rateLimitValue={this.state.form[rateLimitType]}
rateLimitPerSecondValue={

View file

@ -8,8 +8,14 @@ import {
Register,
} from "lemmy-js-client";
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 PasswordInput from "../common/password-input";
import { SiteForm } from "./site-form";
interface State {
@ -34,7 +40,7 @@ export class Setup extends Component<any, State> {
private isoData = setIsoData(this.context);
state: State = {
registerRes: { state: "empty" },
registerRes: EMPTY_REQUEST,
themeList: [],
form: {
show_nsfw: true,
@ -121,46 +127,28 @@ export class Setup extends Component<any, State> {
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="password">
{I18NextService.i18n.t("password")}
</label>
<div className="col-sm-10">
<input
type="password"
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 className="mb-3">
<PasswordInput
id="password"
value={this.state.form.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
label={I18NextService.i18n.t("password")}
isNew
/>
</div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="verify-password">
{I18NextService.i18n.t("verify_password")}
</label>
<div className="col-sm-10">
<input
type="password"
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 className="mb-3">
<PasswordInput
id="verify-password"
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
label={I18NextService.i18n.t("verify_password")}
isNew
/>
</div>
<div className="mb-3 row">
<div className="col-sm-10">
<button type="submit" className="btn btn-secondary">
{this.state.registerRes.state == "loading" ? (
{this.state.registerRes.state === "loading" ? (
<Spinner />
) : (
I18NextService.i18n.t("sign_up")
@ -174,7 +162,7 @@ export class Setup extends Component<any, State> {
async handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault();
i.setState({ registerRes: { state: "loading" } });
i.setState({ registerRes: LOADING_REQUEST });
const {
username,
password_verify,
@ -203,7 +191,7 @@ export class Setup extends Component<any, State> {
registerRes: await HttpService.client.register(form),
});
if (i.state.registerRes.state == "success") {
if (i.state.registerRes.state === "success") {
const data = i.state.registerRes.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 { validEmail } from "@utils/helpers";
import { Options, passwordStrength } from "check-password-strength";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
@ -15,38 +13,17 @@ import {
import { joinLemmyUrl } from "../../config";
import { mdToHtml } from "../../markdown";
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 { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
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,
},
];
import PasswordInput from "../common/password-input";
interface State {
registerRes: RequestState<LoginResponse>;
@ -71,8 +48,8 @@ export class Signup extends Component<any, State> {
private audio?: HTMLAudioElement;
state: State = {
registerRes: { state: "empty" },
captchaRes: { state: "empty" },
registerRes: EMPTY_REQUEST,
captchaRes: EMPTY_REQUEST,
form: {
show_nsfw: false,
},
@ -93,13 +70,13 @@ export class Signup extends Component<any, State> {
}
async fetchCaptcha() {
this.setState({ captchaRes: { state: "loading" } });
this.setState({ captchaRes: LOADING_REQUEST });
this.setState({
captchaRes: await HttpService.client.getCaptcha({}),
captchaRes: await HttpService.client.getCaptcha(),
});
this.setState(s => {
if (s.captchaRes.state == "success") {
if (s.captchaRes.state === "success") {
s.form.captcha_uuid = s.captchaRes.data.ok?.uuid;
}
return s;
@ -113,12 +90,12 @@ export class Signup extends Component<any, State> {
titleName(siteView: SiteView): string {
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 {
return isBrowser() && window.location.hostname == "lemmy.ml";
return isBrowser() && window.location.hostname === "lemmy.ml";
}
render() {
@ -219,57 +196,28 @@ export class Signup extends Component<any, State> {
</div>
</div>
<div className="mb-3 row">
<label
className="col-sm-2 col-form-label"
htmlFor="register-password"
>
{I18NextService.i18n.t("password")}
</label>
<div className="col-sm-10">
<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 className="mb-3">
<PasswordInput
id="register-password"
value={this.state.form.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
showStrength
label={I18NextService.i18n.t("password")}
isNew
/>
</div>
<div className="mb-3 row">
<label
className="col-sm-2 col-form-label"
htmlFor="register-verify-password"
>
{I18NextService.i18n.t("verify_password")}
</label>
<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 className="mb-3">
<PasswordInput
id="register-verify-password"
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
label={I18NextService.i18n.t("verify_password")}
isNew
/>
</div>
{siteView.local_site.registration_mode == "RequireApplication" && (
{siteView.local_site.registration_mode === "RequireApplication" && (
<>
<div className="mb-3 row">
<div className="offset-sm-2 col-sm-10">
@ -281,7 +229,7 @@ export class Signup extends Component<any, State> {
<div
className="md-div"
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="col-sm-10">
<button type="submit" className="btn btn-secondary">
{this.state.registerRes.state == "loading" ? (
{this.state.registerRes.state === "loading" ? (
<Spinner />
) : (
this.titleName(siteView)
@ -379,7 +327,7 @@ export class Signup extends Component<any, State> {
value={this.state.form.captcha_answer}
onInput={linkEvent(
this,
this.handleRegisterCaptchaAnswerChange
this.handleRegisterCaptchaAnswerChange,
)}
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) {
event.preventDefault();
const {
@ -453,7 +382,7 @@ export class Signup extends Component<any, State> {
username,
} = i.state.form;
if (username && password && password_verify) {
i.setState({ registerRes: { state: "loading" } });
i.setState({ registerRes: LOADING_REQUEST });
const registerRes = await HttpService.client.register({
username,
@ -469,7 +398,7 @@ export class Signup extends Component<any, State> {
switch (registerRes.state) {
case "failed": {
toast(registerRes.msg, "danger");
i.setState({ registerRes: { state: "empty" } });
i.setState({ registerRes: EMPTY_REQUEST });
break;
}
@ -482,7 +411,7 @@ export class Signup extends Component<any, State> {
res: data,
});
const site = await HttpService.client.getSite({ auth: myAuth() });
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
@ -511,7 +440,7 @@ export class Signup extends Component<any, State> {
handleRegisterEmailChange(i: Signup, event: any) {
i.state.form.email = event.target.value;
if (i.state.form.email == "") {
if (i.state.form.email === "") {
i.state.form.email = undefined;
}
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.
// 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;
if (!i.audio) {
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 {
Component,
@ -7,6 +6,7 @@ import {
InfernoNode,
linkEvent,
} from "inferno";
import { Prompt } from "inferno-router";
import {
CreateSite,
EditSite,
@ -21,7 +21,6 @@ import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt";
interface SiteFormProps {
blockedInstances?: Instance[];
@ -85,7 +84,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
captcha_difficulty: ls.captcha_difficulty,
allowed_instances: this.props.allowedInstances?.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"
onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}
>
<NavigationPrompt
<Prompt
message={I18NextService.i18n.t("block_leaving")}
when={
!this.props.loading &&
!siteSetup &&
@ -292,7 +291,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</select>
</div>
</div>
{this.state.siteForm.registration_mode == "RequireApplication" && (
{this.state.siteForm.registration_mode === "RequireApplication" && (
<div className="mb-3 row">
<label className="col-12 col-form-label">
{I18NextService.i18n.t("application_questionnaire")}
@ -318,7 +317,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.community_creation_admin_only}
onChange={linkEvent(
this,
this.handleSiteCommunityCreationAdminOnly
this.handleSiteCommunityCreationAdminOnly,
)}
/>
<label
@ -340,7 +339,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.require_email_verification}
onChange={linkEvent(
this,
this.handleSiteRequireEmailVerification
this.handleSiteRequireEmailVerification,
)}
/>
<label
@ -362,7 +361,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
checked={this.state.siteForm.application_email_admins}
onChange={linkEvent(
this,
this.handleSiteApplicationEmailAdmins
this.handleSiteApplicationEmailAdmins,
)}
/>
<label
@ -410,6 +409,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<option value="browser">
{I18NextService.i18n.t("browser_default")}
</option>
<option value="browser-compact">
{I18NextService.i18n.t("browser_default_compact")}
</option>
{this.props.themeList?.map(theme => (
<option key={theme} value={theme}>
{theme}
@ -627,7 +629,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
}
componentDidUpdate(
prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps>
prevProps: Readonly<{ children?: InfernoNode } & SiteFormProps>,
) {
if (
!(
@ -690,7 +692,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
className="btn btn-sm bg-danger"
onClick={linkEvent(
{ key, instance },
this.handleRemoveInstance
this.handleRemoveInstance,
)}
>
<Icon
@ -718,7 +720,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleInstanceEnterPress(
key: InstanceKey,
event: InfernoKeyboardEvent<HTMLInputElement>
event: InfernoKeyboardEvent<HTMLInputElement>,
) {
if (event.code.toLowerCase() === "enter") {
event.preventDefault();
@ -729,8 +731,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleSaveSiteSubmit(i: SiteForm, event: any) {
event.preventDefault();
const auth = myAuthRequired();
i.setState(s => ((s.siteForm.auth = auth), s));
i.setState({ submitted: true });
const stateSiteForm = i.state.siteForm;
@ -784,7 +784,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
allowed_instances: stateSiteForm.allowed_instances,
blocked_instances: stateSiteForm.blocked_instances,
discussion_languages: stateSiteForm.discussion_languages,
auth,
};
}
@ -859,7 +858,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleDeleteTaglineClick(
i: SiteForm,
index: number,
event: InfernoMouseEvent<HTMLButtonElement>
event: InfernoMouseEvent<HTMLButtonElement>,
) {
event.preventDefault();
const taglines = i.state.siteForm.taglines;
@ -874,7 +873,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleAddTaglineClick(
i: SiteForm,
event: InfernoMouseEvent<HTMLButtonElement>
event: InfernoMouseEvent<HTMLButtonElement>,
) {
event.preventDefault();
if (!i.state.siteForm.taglines) {
@ -965,7 +964,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleSiteActorNameMaxLength(i: SiteForm, event: any) {
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 { PersonView, Site, SiteAggregates } from "lemmy-js-client";
import { mdToHtml } from "../../markdown";
@ -32,10 +33,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
return (
<div className="site-sidebar accordion">
<section id="sidebarInfo" className="card border-secondary mb-3">
<header
className="card-header d-flex align-items-center"
id="sidebarInfoHeader"
>
<header className="card-header" id="sidebarInfoHeader">
{this.siteName()}
{!this.state.collapsed && (
<BannerIconHeader banner={this.props.site.banner} />
@ -54,7 +52,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
siteName() {
return (
<>
<div className={classNames({ "mb-2": !this.state.collapsed })}>
<h5 className="mb-0 d-inline">{this.props.site.name}</h5>
{!this.props.isMobile && (
<button
@ -83,7 +81,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
)}
</button>
)}
</>
</div>
);
}

View file

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

View file

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

View file

@ -7,7 +7,6 @@ import {
enableDownvotes,
getCommentParentId,
myAuth,
myAuthRequired,
setIsoData,
updatePersonBlock,
} from "@utils/app";
@ -63,7 +62,14 @@ import {
import { fetchLimit, relTags } from "../../config";
import { CommentViewType, InitialFetchRequest } from "../../interfaces";
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 { CommentNodes } from "../comment/comment-nodes";
import { CommentSortSelect } from "../common/comment-sort-select";
@ -125,10 +131,10 @@ export class Inbox extends Component<any, InboxState> {
sort: "New",
page: 1,
siteRes: this.isoData.site_res,
repliesRes: { state: "empty" },
mentionsRes: { state: "empty" },
messagesRes: { state: "empty" },
markAllAsReadRes: { state: "empty" },
repliesRes: EMPTY_REQUEST,
mentionsRes: EMPTY_REQUEST,
messagesRes: EMPTY_REQUEST,
markAllAsReadRes: EMPTY_REQUEST,
finished: new Map(),
isIsomorphic: false,
};
@ -188,20 +194,20 @@ export class Inbox extends Component<any, InboxState> {
const mui = UserService.Instance.myUserInfo;
return mui
? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t(
"inbox"
"inbox",
)} - ${this.state.siteRes.site_view.site.name}`
: "";
}
get hasUnreads(): boolean {
if (this.state.unreadOrAll == UnreadOrAll.Unread) {
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
const { repliesRes, mentionsRes, messagesRes } = this.state;
const replyCount =
repliesRes.state == "success" ? repliesRes.data.replies.length : 0;
repliesRes.state === "success" ? repliesRes.data.replies.length : 0;
const mentionCount =
mentionsRes.state == "success" ? mentionsRes.data.mentions.length : 0;
mentionsRes.state === "success" ? mentionsRes.data.mentions.length : 0;
const messageCount =
messagesRes.state == "success"
messagesRes.state === "success"
? messagesRes.data.private_messages.length
: 0;
@ -242,11 +248,11 @@ export class Inbox extends Component<any, InboxState> {
className="btn btn-secondary mb-2 mb-sm-3"
onClick={linkEvent(this, this.handleMarkAllAsRead)}
>
{this.state.markAllAsReadRes.state == "loading" ? (
{this.state.markAllAsReadRes.state === "loading" ? (
<Spinner />
) : (
capitalizeFirstLetter(
I18NextService.i18n.t("mark_all_as_read")
I18NextService.i18n.t("mark_all_as_read"),
)
)}
</button>
@ -256,6 +262,7 @@ export class Inbox extends Component<any, InboxState> {
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
nextDisabled={false}
/>
</div>
</div>
@ -445,22 +452,22 @@ export class Inbox extends Component<any, InboxState> {
buildCombined(): ReplyType[] {
const replies: ReplyType[] =
this.state.repliesRes.state == "success"
this.state.repliesRes.state === "success"
? this.state.repliesRes.data.replies.map(this.replyToReplyType)
: [];
const mentions: ReplyType[] =
this.state.mentionsRes.state == "success"
this.state.mentionsRes.state === "success"
? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType)
: [];
const messages: ReplyType[] =
this.state.messagesRes.state == "success"
this.state.messagesRes.state === "success"
? this.state.messagesRes.data.private_messages.map(
this.messageToReplyType
this.messageToReplyType,
)
: [];
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() {
if (
this.state.repliesRes.state == "loading" ||
this.state.mentionsRes.state == "loading" ||
this.state.messagesRes.state == "loading"
this.state.repliesRes.state === "loading" ||
this.state.mentionsRes.state === "loading" ||
this.state.messagesRes.state === "loading"
) {
return (
<h1 className="h4">
@ -718,78 +725,77 @@ export class Inbox extends Component<any, InboxState> {
static async fetchInitialData({
client,
auth,
}: InitialFetchRequest): Promise<InboxData> {
const sort: CommentSortType = "New";
return {
mentionsRes: auth
? await client.getPersonMentions({
sort,
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" },
const empty: EmptyRequestState = EMPTY_REQUEST;
let inboxData: InboxData = {
mentionsRes: empty,
messagesRes: empty,
repliesRes: 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() {
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 limit = fetchLimit;
const auth = myAuthRequired();
this.setState({ repliesRes: { state: "loading" } });
this.setState({ repliesRes: LOADING_REQUEST });
this.setState({
repliesRes: await HttpService.client.getReplies({
sort,
unread_only,
page,
limit,
auth,
}),
});
this.setState({ mentionsRes: { state: "loading" } });
this.setState({ mentionsRes: LOADING_REQUEST });
this.setState({
mentionsRes: await HttpService.client.getPersonMentions({
sort,
unread_only,
page,
limit,
auth,
}),
});
this.setState({ messagesRes: { state: "loading" } });
this.setState({ messagesRes: LOADING_REQUEST });
this.setState({
messagesRes: await HttpService.client.getPrivateMessages({
unread_only,
page,
limit,
auth,
}),
});
UnreadCounterService.Instance.updateInboxCounts();
}
async handleSortChange(val: CommentSortType) {
@ -798,19 +804,17 @@ export class Inbox extends Component<any, InboxState> {
}
async handleMarkAllAsRead(i: Inbox) {
i.setState({ markAllAsReadRes: { state: "loading" } });
i.setState({ markAllAsReadRes: LOADING_REQUEST });
i.setState({
markAllAsReadRes: await HttpService.client.markAllAsRead({
auth: myAuthRequired(),
}),
markAllAsReadRes: await HttpService.client.markAllAsRead(),
});
if (i.state.markAllAsReadRes.state == "success") {
if (i.state.markAllAsReadRes.state === "success") {
i.setState({
repliesRes: { state: "empty" },
mentionsRes: { state: "empty" },
messagesRes: { state: "empty" },
repliesRes: EMPTY_REQUEST,
mentionsRes: EMPTY_REQUEST,
messagesRes: EMPTY_REQUEST,
});
}
}
@ -837,7 +841,7 @@ export class Inbox extends Component<any, InboxState> {
async handleBlockPerson(form: BlockPerson) {
const blockPersonRes = await HttpService.client.blockPerson(form);
if (blockPersonRes.state == "success") {
if (blockPersonRes.state === "success") {
updatePersonBlock(blockPersonRes.data);
}
}
@ -868,7 +872,7 @@ export class Inbox extends Component<any, InboxState> {
async handleDeleteComment(form: DeleteComment) {
const res = await HttpService.client.deleteComment(form);
if (res.state == "success") {
if (res.state === "success") {
toast(I18NextService.i18n.t("deleted"));
this.findAndUpdateComment(res);
}
@ -876,7 +880,7 @@ export class Inbox extends Component<any, InboxState> {
async handleRemoveComment(form: RemoveComment) {
const res = await HttpService.client.removeComment(form);
if (res.state == "success") {
if (res.state === "success") {
toast(I18NextService.i18n.t("remove_comment"));
this.findAndUpdateComment(res);
}
@ -917,12 +921,20 @@ export class Inbox extends Component<any, InboxState> {
async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
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) {
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) {
@ -947,7 +959,11 @@ export class Inbox extends Component<any, InboxState> {
async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
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) {
@ -958,9 +974,9 @@ export class Inbox extends Component<any, InboxState> {
async handleCreateMessage(form: CreatePrivateMessage) {
const res = await HttpService.client.createPrivateMessage(form);
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(
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") {
s.messagesRes.data.private_messages = editPrivateMessage(
res.data.private_message_view,
s.messagesRes.data.private_messages
s.messagesRes.data.private_messages,
);
}
return s;
@ -982,20 +998,20 @@ export class Inbox extends Component<any, InboxState> {
updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
// Maybe not necessary
if (banRes.state == "success") {
if (banRes.state === "success") {
this.setState(s => {
if (s.repliesRes.state == "success") {
if (s.repliesRes.state === "success") {
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_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
.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_from_community = banRes.data.banned)
c => (c.creator_banned_from_community = banRes.data.banned),
);
}
return s;
@ -1005,16 +1021,16 @@ export class Inbox extends Component<any, InboxState> {
updateBan(banRes: RequestState<BanPersonResponse>) {
// Maybe not necessary
if (banRes.state == "success") {
if (banRes.state === "success") {
this.setState(s => {
if (s.repliesRes.state == "success") {
if (s.repliesRes.state === "success") {
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));
}
if (s.mentionsRes.state == "success") {
if (s.mentionsRes.state === "success") {
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));
}
return s;
@ -1023,40 +1039,40 @@ export class Inbox extends Component<any, InboxState> {
}
purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
if (purgeRes.state == "success") {
if (purgeRes.state === "success") {
toast(I18NextService.i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
reportToast(
res: RequestState<PrivateMessageReportResponse | CommentReportResponse>
res: RequestState<PrivateMessageReportResponse | CommentReportResponse>,
) {
if (res.state == "success") {
if (res.state === "success") {
toast(I18NextService.i18n.t("report_created"));
}
}
// A weird case, since you have only replies and mentions, not comment responses
findAndUpdateComment(res: RequestState<CommentResponse>) {
if (res.state == "success") {
if (res.state === "success") {
this.setState(s => {
if (s.repliesRes.state == "success") {
if (s.repliesRes.state === "success") {
s.repliesRes.data.replies = editWith(
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(
res.data.comment_view,
s.mentionsRes.data.mentions
s.mentionsRes.data.mentions,
);
}
// Set finished for the parent
s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0,
true
true,
);
return s;
});
@ -1065,10 +1081,10 @@ export class Inbox extends Component<any, InboxState> {
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
this.setState(s => {
if (s.repliesRes.state == "success" && res.state == "success") {
if (s.repliesRes.state === "success" && res.state === "success") {
s.repliesRes.data.replies = editCommentReply(
res.data.comment_reply_view,
s.repliesRes.data.replies
s.repliesRes.data.replies,
);
}
return s;
@ -1077,10 +1093,10 @@ export class Inbox extends Component<any, InboxState> {
findAndUpdateMention(res: RequestState<PersonMentionResponse>) {
this.setState(s => {
if (s.mentionsRes.state == "success" && res.state == "success") {
if (s.mentionsRes.state === "success" && res.state === "success") {
s.mentionsRes.data.mentions = editMention(
res.data.person_mention_view,
s.mentionsRes.data.mentions
s.mentionsRes.data.mentions,
);
}
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 { Component, linkEvent } from "inferno";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
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 { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
interface State {
passwordChangeRes: RequestState<LoginResponse>;
@ -21,7 +26,7 @@ export class PasswordChange extends Component<any, State> {
private isoData = setIsoData(this.context);
state: State = {
passwordChangeRes: { state: "empty" },
passwordChangeRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
form: {
token: this.props.match.params.token,
@ -60,42 +65,28 @@ export class PasswordChange extends Component<any, State> {
passwordChangeForm() {
return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="new-password">
{I18NextService.i18n.t("new_password")}
</label>
<div className="col-sm-10">
<input
id="new-password"
type="password"
value={this.state.form.password}
onInput={linkEvent(this, this.handlePasswordChange)}
className="form-control"
required
maxLength={60}
/>
</div>
<div className="mb-3">
<PasswordInput
id="new-password"
value={this.state.form.password}
onInput={linkEvent(this, this.handlePasswordChange)}
showStrength
label={I18NextService.i18n.t("new_password")}
isNew
/>
</div>
<div className="mb-3 row">
<label className="col-sm-2 col-form-label" htmlFor="verify-password">
{I18NextService.i18n.t("verify_password")}
</label>
<div className="col-sm-10">
<input
id="verify-password"
type="password"
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
className="form-control"
required
maxLength={60}
/>
</div>
<div className="mb-3">
<PasswordInput
id="password"
value={this.state.form.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
label={I18NextService.i18n.t("verify_password")}
/>
</div>
<div className="mb-3 row">
<div className="col-sm-10">
<button type="submit" className="btn btn-secondary">
{this.state.passwordChangeRes.state == "loading" ? (
{this.state.passwordChangeRes.state === "loading" ? (
<Spinner />
) : (
capitalizeFirstLetter(I18NextService.i18n.t("save"))
@ -119,7 +110,7 @@ export class PasswordChange extends Component<any, State> {
async handlePasswordChangeSubmit(i: PasswordChange, event: any) {
event.preventDefault();
i.setState({ passwordChangeRes: { state: "loading" } });
i.setState({ passwordChangeRes: LOADING_REQUEST });
const password = i.state.form.password;
const password_verify = i.state.form.password_verify;
@ -139,7 +130,7 @@ export class PasswordChange extends Component<any, State> {
res: data,
});
const site = await HttpService.client.getSite({ auth: myAuth() });
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@ import {
editCommentReport,
editPostReport,
editPrivateMessageReport,
myAuthRequired,
setIsoData,
} from "@utils/app";
import { randomStr } from "@utils/helpers";
@ -36,13 +35,18 @@ import {
I18NextService,
UserService,
} from "../../services";
import { RequestState } from "../../services/HttpService";
import {
EMPTY_REQUEST,
LOADING_REQUEST,
RequestState,
} from "../../services/HttpService";
import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
import { PostReport } from "../post/post-report";
import { PrivateMessageReport } from "../private_message/private-message-report";
import { UnreadCounterService } from "../../services";
enum UnreadOrAll {
Unread,
@ -89,9 +93,9 @@ interface ReportsState {
export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData<ReportsData>(this.context);
state: ReportsState = {
commentReportsRes: { state: "empty" },
postReportsRes: { state: "empty" },
messageReportsRes: { state: "empty" },
commentReportsRes: EMPTY_REQUEST,
postReportsRes: EMPTY_REQUEST,
messageReportsRes: EMPTY_REQUEST,
unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All,
page: 1,
@ -140,7 +144,7 @@ export class Reports extends Component<any, ReportsState> {
const mui = UserService.Instance.myUserInfo;
return mui
? `@${mui.local_user_view.person.name} ${I18NextService.i18n.t(
"reports"
"reports",
)} - ${this.state.siteRes.site_view.site.name}`
: "";
}
@ -160,6 +164,16 @@ export class Reports extends Component<any, ReportsState> {
<Paginator
page={this.state.page}
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>
@ -352,25 +366,25 @@ export class Reports extends Component<any, ReportsState> {
get buildCombined(): ItemType[] {
const commentRes = this.state.commentReportsRes;
const comments =
commentRes.state == "success"
commentRes.state === "success"
? commentRes.data.comment_reports.map(this.commentReportToItemType)
: [];
const postRes = this.state.postReportsRes;
const posts =
postRes.state == "success"
postRes.state === "success"
? postRes.data.post_reports.map(this.postReportToItemType)
: [];
const pmRes = this.state.messageReportsRes;
const privateMessages =
pmRes.state == "success"
pmRes.state === "success"
? pmRes.data.private_message_reports.map(
this.privateMessageReportToItemType
this.privateMessageReportToItemType,
)
: [];
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({
auth,
client,
}: InitialFetchRequest): Promise<ReportsData> {
const unresolved_only = true;
@ -532,20 +545,18 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only,
page,
limit,
auth: auth as string,
};
const postReportsForm: ListPostReports = {
unresolved_only,
page,
limit,
auth: auth as string,
};
const data: ReportsData = {
commentReportsRes: await client.listCommentReports(commentReportsForm),
postReportsRes: await client.listPostReports(postReportsForm),
messageReportsRes: { state: "empty" },
messageReportsRes: EMPTY_REQUEST,
};
if (amAdmin()) {
@ -553,11 +564,10 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only,
page,
limit,
auth: auth as string,
};
data.messageReportsRes = await client.listPrivateMessageReports(
privateMessageReportsForm
privateMessageReportsForm,
);
}
@ -565,15 +575,14 @@ export class Reports extends Component<any, ReportsState> {
}
async refetch() {
const unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
const page = this.state.page;
const limit = fetchLimit;
const auth = myAuthRequired();
this.setState({
commentReportsRes: { state: "loading" },
postReportsRes: { state: "loading" },
messageReportsRes: { state: "loading" },
commentReportsRes: LOADING_REQUEST,
postReportsRes: LOADING_REQUEST,
messageReportsRes: LOADING_REQUEST,
});
const form:
@ -583,7 +592,6 @@ export class Reports extends Component<any, ReportsState> {
unresolved_only,
page,
limit,
auth,
};
this.setState({
@ -594,7 +602,7 @@ export class Reports extends Component<any, ReportsState> {
if (amAdmin()) {
this.setState({
messageReportsRes: await HttpService.client.listPrivateMessageReports(
form
form,
),
});
}
@ -603,24 +611,36 @@ export class Reports extends Component<any, ReportsState> {
async handleResolveCommentReport(form: ResolveCommentReport) {
const res = await HttpService.client.resolveCommentReport(form);
this.findAndUpdateCommentReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
}
async handleResolvePostReport(form: ResolvePostReport) {
const res = await HttpService.client.resolvePostReport(form);
this.findAndUpdatePostReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
}
async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) {
const res = await HttpService.client.resolvePrivateMessageReport(form);
this.findAndUpdatePrivateMessageReport(res);
if (this.state.unreadOrAll === UnreadOrAll.Unread) {
this.refetch();
UnreadCounterService.Instance.updateReports();
}
}
findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) {
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(
res.data.comment_report_view,
s.commentReportsRes.data.comment_reports
s.commentReportsRes.data.comment_reports,
);
}
return s;
@ -629,10 +649,10 @@ export class Reports extends Component<any, ReportsState> {
findAndUpdatePostReport(res: RequestState<PostReportResponse>) {
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(
res.data.post_report_view,
s.postReportsRes.data.post_reports
s.postReportsRes.data.post_reports,
);
}
return s;
@ -640,14 +660,14 @@ export class Reports extends Component<any, ReportsState> {
}
findAndUpdatePrivateMessageReport(
res: RequestState<PrivateMessageReportResponse>
res: RequestState<PrivateMessageReportResponse>,
) {
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 =
editPrivateMessageReport(
res.data.private_message_report_view,
s.messageReportsRes.data.private_message_reports
s.messageReportsRes.data.private_message_reports,
);
}
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 { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client";
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 { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@ -16,7 +21,7 @@ export class VerifyEmail extends Component<any, State> {
private isoData = setIsoData(this.context);
state: State = {
verifyRes: { state: "empty" },
verifyRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
};
@ -26,7 +31,7 @@ export class VerifyEmail extends Component<any, State> {
async verify() {
this.setState({
verifyRes: { state: "loading" },
verifyRes: LOADING_REQUEST,
});
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"));
this.props.history.push("/login");
}
@ -61,7 +66,7 @@ export class VerifyEmail extends Component<any, State> {
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h1 className="h4 mb-4">{I18NextService.i18n.t("verify_email")}</h1>
{this.state.verifyRes.state == "loading" && (
{this.state.verifyRes.state === "loading" && (
<h5>
<Spinner large />
</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 type { QueryParams } from "@utils/types";
import { Choice, RouteDataResponse } from "@utils/types";
@ -14,6 +14,7 @@ import {
import { InitialFetchRequest, PostFormParams } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
import {
EMPTY_REQUEST,
HttpService,
RequestState,
WrappedLemmyHttp,
@ -57,7 +58,7 @@ export class CreatePost extends Component<
state: CreatePostState = {
siteRes: this.isoData.site_res,
loading: true,
initialCommunitiesRes: { state: "empty" },
initialCommunitiesRes: EMPTY_REQUEST,
isIsomorphic: false,
};
@ -96,12 +97,10 @@ export class CreatePost extends Component<
async fetchCommunity() {
const { communityId } = getCreatePostQueryParams();
const auth = myAuth();
if (communityId) {
const res = await HttpService.client.getCommunity({
id: communityId,
auth,
});
if (res.state === "success") {
this.setState({
@ -121,7 +120,7 @@ export class CreatePost extends Component<
const { communityId } = getCreatePostQueryParams();
const initialCommunitiesRes = await fetchCommunitiesForOptions(
HttpService.client
HttpService.client,
);
this.setState({
@ -239,18 +238,16 @@ export class CreatePost extends Component<
static async fetchInitialData({
client,
query: { communityId },
auth,
}: InitialFetchRequest<
QueryParams<CreatePostProps>
>): Promise<CreatePostData> {
const data: CreatePostData = {
initialCommunitiesRes: await fetchCommunitiesForOptions(client),
communityResponse: { state: "empty" },
communityResponse: EMPTY_REQUEST,
};
if (communityId) {
const form: GetCommunity = {
auth,
id: getIdFromString(communityId),
};

View file

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

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