mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge branch 'main' into comment-depth
This commit is contained in:
commit
31e61e7449
94 changed files with 9939 additions and 9699 deletions
|
@ -18,6 +18,7 @@
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
"@typescript-eslint/ban-ts-comment": 0,
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
|
"@typescript-eslint/no-empty-function": 0,
|
||||||
"arrow-body-style": 0,
|
"arrow-body-style": 0,
|
||||||
"curly": 0,
|
"curly": 0,
|
||||||
"eol-last": 0,
|
"eol-last": 0,
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
"no-useless-constructor": 0,
|
"no-useless-constructor": 0,
|
||||||
"no-useless-escape": 0,
|
"no-useless-escape": 0,
|
||||||
"no-var": 0,
|
"no-var": 0,
|
||||||
"prefer-const": 0,
|
"prefer-const": 1,
|
||||||
"prefer-rest-params": 0,
|
"prefer-rest-params": 0,
|
||||||
"quote-props": 0,
|
"quote-props": 0,
|
||||||
"unicorn/filename-case": 0
|
"unicorn/filename-case": 0
|
||||||
|
|
27
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
27
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
@ -1,27 +0,0 @@
|
||||||
---
|
|
||||||
name: "\U0001F41E Bug Report"
|
|
||||||
about: Create a report to help us improve Lemmy
|
|
||||||
title: ""
|
|
||||||
labels: bug
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
Found a bug? Please fill out the sections below. 👍
|
|
||||||
|
|
||||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
|
||||||
|
|
||||||
### Issue Summary
|
|
||||||
|
|
||||||
A summary of the bug.
|
|
||||||
|
|
||||||
### Steps to Reproduce
|
|
||||||
|
|
||||||
1. (for example) I clicked login, and an endless spinner show up.
|
|
||||||
2. I tried to install lemmy via this guide, and I'm getting this error.
|
|
||||||
3. ...
|
|
||||||
|
|
||||||
### Technical details
|
|
||||||
|
|
||||||
- Please post your log: `sudo docker-compose logs > lemmy_log.out`.
|
|
||||||
- What OS are you trying to install lemmy on?
|
|
||||||
- Any browser console errors?
|
|
47
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
name: "\U0001F41E Bug Report"
|
||||||
|
description: Create a report to help us improve lemmy-ui
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug", "triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Found a bug? Please fill out the sections below. 👍
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||||
|
- type: textarea
|
||||||
|
id: summary
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
description: A summary of the bug.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: |
|
||||||
|
Describe the steps to reproduce the bug.
|
||||||
|
The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution.
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: technical
|
||||||
|
attributes:
|
||||||
|
label: Technical Details
|
||||||
|
description: |
|
||||||
|
- Any browser console errors?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: lemmy-ui-version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: Which Lemmy UI version do you use? Displayed in the footer.
|
||||||
|
placeholder: ex. 0.17.4-rc.4
|
||||||
|
validations:
|
||||||
|
required: true
|
43
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
43
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
name: "\U0001F680 Feature request"
|
|
||||||
about: Suggest an idea for improving Lemmy
|
|
||||||
title: ""
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
|
||||||
|
|
||||||
### Is your proposal related to a problem?
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide a clear and concise description of what the problem is.
|
|
||||||
For example, "I'm always frustrated when..."
|
|
||||||
-->
|
|
||||||
|
|
||||||
(Write your answer here.)
|
|
||||||
|
|
||||||
### Describe the solution you'd like
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide a clear and concise description of what you want to happen.
|
|
||||||
-->
|
|
||||||
|
|
||||||
(Describe your proposed solution here.)
|
|
||||||
|
|
||||||
### Describe alternatives you've considered
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Let us know about other solutions you've tried or researched.
|
|
||||||
-->
|
|
||||||
|
|
||||||
(Write your answer here.)
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Is there anything else you can add about the proposal?
|
|
||||||
You might want to link to related issues here, if you haven't already.
|
|
||||||
-->
|
|
||||||
|
|
||||||
(Write your answer here.)
|
|
41
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
name: "\U0001F680 Feature request"
|
||||||
|
description: Suggest an idea for improving Lemmy
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Have a suggestion about Lemmy's UI?
|
||||||
|
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your proposal related to a problem?
|
||||||
|
description: |
|
||||||
|
Provide a clear and concise description of what the problem is.
|
||||||
|
For example, "I'm always frustrated when..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like.
|
||||||
|
description: |
|
||||||
|
Provide a clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered.
|
||||||
|
description: |
|
||||||
|
Let us know about other solutions you've tried or researched.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |
|
||||||
|
Is there anything else you can add about the proposal?
|
||||||
|
You might want to link to related issues here, if you haven't already.
|
9
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
9
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
name: "? Question"
|
|
||||||
about: General questions about Lemmy
|
|
||||||
title: ""
|
|
||||||
labels: question
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
What's the question you have about lemmy?
|
|
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: "? Question"
|
||||||
|
description: General questions about Lemmy
|
||||||
|
title: "Question: "
|
||||||
|
labels: ["question", "triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Have a question about Lemmy's UI?
|
||||||
|
Please check the docs first: https://join-lemmy.org/docs/en/index.html
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: Question
|
||||||
|
description: What's the question you have about Lemmy's UI?
|
||||||
|
validations:
|
||||||
|
required: true
|
9
.github/ISSUE_TEMPLATE/hexbear.md
vendored
9
.github/ISSUE_TEMPLATE/hexbear.md
vendored
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
name: Hexbear
|
|
||||||
about: For hexbear issues
|
|
||||||
title: ""
|
|
||||||
labels: hexbear
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
For hexbear-related issues
|
|
11
.github/ISSUE_TEMPLATE/hexbear.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/hexbear.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
name: "Hexbear"
|
||||||
|
description: For hexbear issues
|
||||||
|
labels: ["hexbear", "triage"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: Question
|
||||||
|
description: What's the question you have about hexbear?
|
||||||
|
validations:
|
||||||
|
required: true
|
|
@ -1,6 +1,6 @@
|
||||||
pipeline:
|
pipeline:
|
||||||
fetch_git_submodules:
|
fetch_git_submodules:
|
||||||
image: node:14-alpine
|
image: node:alpine
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- apk add git
|
||||||
- git submodule init
|
- git submodule init
|
||||||
|
@ -8,93 +8,27 @@ pipeline:
|
||||||
# - git fetch --tags
|
# - git fetch --tags
|
||||||
|
|
||||||
yarn:
|
yarn:
|
||||||
image: node:14-alpine
|
image: node:alpine
|
||||||
commands:
|
commands:
|
||||||
- yarn
|
- yarn
|
||||||
|
|
||||||
yarn_lint:
|
yarn_lint:
|
||||||
image: node:14-alpine
|
image: node:alpine
|
||||||
commands:
|
commands:
|
||||||
- yarn lint
|
- yarn lint
|
||||||
|
|
||||||
yarn_build_dev:
|
yarn_build_dev:
|
||||||
image: node:14-alpine
|
image: node:alpine
|
||||||
commands:
|
commands:
|
||||||
- yarn build:dev
|
- yarn build:dev
|
||||||
|
|
||||||
nightly_build:
|
publish_release_docker:
|
||||||
image: plugins/docker
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
secrets: [docker_username, docker_password]
|
||||||
settings:
|
settings:
|
||||||
dockerfile: Dockerfile
|
|
||||||
repo: dessalines/lemmy-ui
|
repo: dessalines/lemmy-ui
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
tags:
|
|
||||||
- dev
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- cron
|
|
||||||
|
|
||||||
publish_release_docker_image_amd:
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
repo: dessalines/lemmy-ui
|
platforms: linux/amd64
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
auto_tag_suffix: linux-amd64
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
when:
|
|
||||||
event: tag
|
|
||||||
platform: linux/arm64
|
|
||||||
|
|
||||||
publish_release_docker_image_arm:
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
repo: dessalines/lemmy-ui
|
|
||||||
auto_tag: true
|
|
||||||
auto_tag_suffix: linux-arm64
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
when:
|
|
||||||
event: tag
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
publish_release_docker_manifest:
|
|
||||||
image: plugins/manifest
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
target: "dessalines/lemmy-ui:${CI_COMMIT_TAG}"
|
|
||||||
template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH"
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
ignore_missing: true
|
|
||||||
when:
|
|
||||||
event: tag
|
|
||||||
|
|
||||||
publish_latest_release_docker_manifest:
|
|
||||||
image: plugins/manifest
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: docker_username
|
|
||||||
password:
|
|
||||||
from_secret: docker_password
|
|
||||||
target: "dessalines/lemmy-ui:latest"
|
|
||||||
template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH"
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
ignore_missing: true
|
|
||||||
when:
|
when:
|
||||||
event: tag
|
event: tag
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# lemmy-ui
|
# Lemmy-UI
|
||||||
|
|
||||||
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
|
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ The following environment variables can be used to configure lemmy-ui:
|
||||||
| `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. |
|
| `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. |
|
||||||
| `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. |
|
| `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. |
|
||||||
| `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. |
|
| `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. |
|
||||||
| `LEMMY_UI_LEMMY_WS_HOST` | `string` | `0.0.0.0:8536` | An alternate location for lemmy's websocket address. Not usually necessary. |
|
|
||||||
| `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. |
|
| `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. |
|
||||||
| `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. |
|
| `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. |
|
||||||
| `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. |
|
| `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. |
|
||||||
|
|
|
@ -4,7 +4,8 @@ set -e
|
||||||
new_tag="$1"
|
new_tag="$1"
|
||||||
|
|
||||||
# Old deploy
|
# Old deploy
|
||||||
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag
|
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 --push
|
||||||
|
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64
|
||||||
# sudo docker push dessalines/lemmy-ui:$new_tag
|
# sudo docker push dessalines/lemmy-ui:$new_tag
|
||||||
|
|
||||||
# Upgrade version
|
# Upgrade version
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ddf0d3a4dcfba5eddbcdb702db2470b52abb3815
|
Subproject commit f45ddff206adb52ab0ac7555bf14978edac5d2f2
|
23
package.json
23
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.17.1",
|
"version": "0.18.0-beta.6",
|
||||||
"description": "An isomorphic UI for lemmy",
|
"description": "An isomorphic UI for lemmy",
|
||||||
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
|
@ -17,16 +17,9 @@
|
||||||
"start": "yarn build:dev --watch"
|
"start": "yarn build:dev --watch"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js}": [
|
"*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
|
||||||
"prettier --write",
|
"*.{css, scss}": ["prettier --write"],
|
||||||
"eslint --fix"
|
"package.json": ["sortpack"]
|
||||||
],
|
|
||||||
"*.{css, scss}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"package.json": [
|
|
||||||
"sortpack"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||||
|
@ -49,6 +42,7 @@
|
||||||
"emoji-mart": "^5.4.0",
|
"emoji-mart": "^5.4.0",
|
||||||
"emoji-short-name": "^2.0.0",
|
"emoji-short-name": "^2.0.0",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
|
"history": "^5.3.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"i18next": "^22.4.15",
|
"i18next": "^22.4.15",
|
||||||
"inferno": "^8.1.1",
|
"inferno": "^8.1.1",
|
||||||
|
@ -60,7 +54,7 @@
|
||||||
"inferno-server": "^8.1.1",
|
"inferno-server": "^8.1.1",
|
||||||
"isomorphic-cookie": "^1.2.4",
|
"isomorphic-cookie": "^1.2.4",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lemmy-js-client": "0.17.2-rc.17",
|
"lemmy-js-client": "0.17.2-rc.24",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-container": "^3.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
|
@ -73,7 +67,6 @@
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"run-node-webpack-plugin": "^1.3.0",
|
"run-node-webpack-plugin": "^1.3.0",
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"sanitize-html": "^2.10.0",
|
"sanitize-html": "^2.10.0",
|
||||||
"sass": "^1.62.1",
|
"sass": "^1.62.1",
|
||||||
"sass-loader": "^13.2.2",
|
"sass-loader": "^13.2.2",
|
||||||
|
@ -85,8 +78,7 @@
|
||||||
"tributejs": "^5.1.3",
|
"tributejs": "^5.1.3",
|
||||||
"webpack": "5.82.1",
|
"webpack": "5.82.1",
|
||||||
"webpack-cli": "^5.1.1",
|
"webpack-cli": "^5.1.1",
|
||||||
"webpack-node-externals": "^3.0.0",
|
"webpack-node-externals": "^3.0.0"
|
||||||
"websocket-ts": "^1.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.8",
|
"@babel/core": "^7.21.8",
|
||||||
|
@ -120,6 +112,7 @@
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"webpack-dev-server": "4.15.0"
|
"webpack-dev-server": "4.15.0"
|
||||||
},
|
},
|
||||||
|
"packageManager": "yarn@1.22.19",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.9.0"
|
"node": ">=8.9.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -75,6 +75,11 @@
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-div pre {
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.md-div table {
|
.md-div table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -275,6 +280,10 @@ hr {
|
||||||
-ms-filter: blur(10px);
|
-ms-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.img-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.img-expanded {
|
.img-expanded {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,865 +0,0 @@
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
// --------------------------------------------------
|
|
||||||
|
|
||||||
//== Colors
|
|
||||||
//
|
|
||||||
//## Gray and brand colors for use across Bootstrap.
|
|
||||||
|
|
||||||
//// colors from bs-2
|
|
||||||
// Grays
|
|
||||||
// -------------------------
|
|
||||||
$black: #000;
|
|
||||||
$grayDark: #555;
|
|
||||||
$gray: #bbb;
|
|
||||||
$grayLight: #bbb;
|
|
||||||
$white: #fff;
|
|
||||||
|
|
||||||
// Accent colors
|
|
||||||
// -------------------------
|
|
||||||
$blue: #5555ff;
|
|
||||||
$cyan: #55ffff;
|
|
||||||
$cyanDark: #00aaaa;
|
|
||||||
$blueDark: #000084;
|
|
||||||
$green: #55ff55;
|
|
||||||
$greenDark: #00aa00;
|
|
||||||
$magenta: #ff55ff;
|
|
||||||
$magentaDark: #aa00aa;
|
|
||||||
$red: #ff5555;
|
|
||||||
$redDark: #aa0000;
|
|
||||||
$yellow: #fefe54;
|
|
||||||
$brown: #aa5500;
|
|
||||||
$orange: #a85400;
|
|
||||||
$pink: #fe54fe;
|
|
||||||
$purple: #fe5454;
|
|
||||||
|
|
||||||
// end colors
|
|
||||||
|
|
||||||
$gray-base: $gray;
|
|
||||||
$gray-darker: $grayDark;
|
|
||||||
$gray-dark: $grayDark;
|
|
||||||
$gray-light: $grayLight;
|
|
||||||
$gray-lighter: $grayLight;
|
|
||||||
|
|
||||||
$brand-primary: $gray;
|
|
||||||
$brand-primary-bg: $cyanDark;
|
|
||||||
$brand-success: $greenDark;
|
|
||||||
$brand-info: $brown;
|
|
||||||
$brand-warning: $magentaDark;
|
|
||||||
$brand-danger: $redDark;
|
|
||||||
|
|
||||||
//== Scaffolding
|
|
||||||
//
|
|
||||||
//## Settings for some of the most global styles.
|
|
||||||
|
|
||||||
//** Background color for `<body>`.
|
|
||||||
$body-bg: $blueDark;
|
|
||||||
//** Global text color on `<body>`.
|
|
||||||
$text-color: $gray-light;
|
|
||||||
|
|
||||||
//** Global textual link color.
|
|
||||||
$link-color: $brand-primary;
|
|
||||||
//** Link hover color set via `darken()` function.
|
|
||||||
$link-hover-color: $white;
|
|
||||||
//** Link hover decoration.
|
|
||||||
$link-hover-decoration: none;
|
|
||||||
|
|
||||||
//== Typography
|
|
||||||
//
|
|
||||||
//## Font, line-height, and color for body text, headings, and more.
|
|
||||||
|
|
||||||
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
|
||||||
$font-family-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
|
||||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
|
||||||
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
|
||||||
$font-family-base: $font-family-sans-serif;
|
|
||||||
|
|
||||||
$baseWidth: 10px;
|
|
||||||
$font-size-base: 18px;
|
|
||||||
$font-size-large: $font-size-base;
|
|
||||||
$font-size-small: $font-size-base;
|
|
||||||
|
|
||||||
$font-size-h1: $font-size-base;
|
|
||||||
$font-size-h2: $font-size-base;
|
|
||||||
$font-size-h3: $font-size-base;
|
|
||||||
$font-size-h4: $font-size-base;
|
|
||||||
$font-size-h5: $font-size-base;
|
|
||||||
$font-size-h6: $font-size-base;
|
|
||||||
|
|
||||||
//** Unit-less `line-height` for use in components like buttons.
|
|
||||||
$baseLineHeight: 19px;
|
|
||||||
$line-height-base: $baseLineHeight;
|
|
||||||
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
|
|
||||||
$line-height-computed: $line-height-base;
|
|
||||||
|
|
||||||
//** By default, this inherits from the `<body>`.
|
|
||||||
$headings-font-family: inherit;
|
|
||||||
$headings-font-weight: normal;
|
|
||||||
$headings-line-height: $line-height-base;
|
|
||||||
$headings-color: inherit;
|
|
||||||
|
|
||||||
$space: $baseWidth;
|
|
||||||
$halfbaseLineHeight: ($baseLineHeight / 2);
|
|
||||||
$borderWidth: 2px;
|
|
||||||
$baseLineWidth: ($baseLineHeight / 2);
|
|
||||||
$halfSpace: ($baseWidth / 2);
|
|
||||||
$lhsNB: ($baseWidth / 2 + 1);
|
|
||||||
$rhsNB: ($baseWidth / 2 - 1);
|
|
||||||
$lhs: ($lhsNB - ($borderWidth));
|
|
||||||
$rhs: ($rhsNB - ($borderWidth / 2));
|
|
||||||
$tsNB: ($baseLineHeight / 2);
|
|
||||||
$bsNB: $tsNB;
|
|
||||||
$ts: ($tsNB - ($borderWidth / 2));
|
|
||||||
$bs: $ts;
|
|
||||||
$tsMargin: 3px;
|
|
||||||
|
|
||||||
//== Iconography
|
|
||||||
//
|
|
||||||
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
|
|
||||||
|
|
||||||
//** Load fonts from this directory.
|
|
||||||
$icon-font-path: "../fonts/";
|
|
||||||
//** File name for all font files.
|
|
||||||
$icon-font-name: "glyphicons-halflings-regular";
|
|
||||||
//** Element ID within SVG icon file.
|
|
||||||
$icon-font-svg-id: "glyphicons_halflingsregular";
|
|
||||||
|
|
||||||
//== Components
|
|
||||||
//
|
|
||||||
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
|
|
||||||
|
|
||||||
$padding-base-vertical: 0px;
|
|
||||||
$padding-base-horizontal: 0px;
|
|
||||||
|
|
||||||
$padding-large-vertical: 0px;
|
|
||||||
$padding-large-horizontal: $halfSpace;
|
|
||||||
|
|
||||||
$padding-small-vertical: 0px;
|
|
||||||
$padding-small-horizontal: 0px;
|
|
||||||
|
|
||||||
$padding-xs-vertical: 0px;
|
|
||||||
$padding-xs-horizontal: 0px;
|
|
||||||
|
|
||||||
$line-height-large: $baseLineHeight;
|
|
||||||
$line-height-small: $baseLineHeight;
|
|
||||||
|
|
||||||
$border-radius-base: 0;
|
|
||||||
$border-radius-large: 0;
|
|
||||||
$border-radius-small: 0;
|
|
||||||
|
|
||||||
//** Global color for active items (e.g., navs or dropdowns).
|
|
||||||
$component-active-color: $white;
|
|
||||||
//** Global background color for active items (e.g., navs or dropdowns).
|
|
||||||
$component-active-bg: $black;
|
|
||||||
|
|
||||||
//** Width of the `border` for generating carets that indicator dropdowns.
|
|
||||||
$caret-width-base: 4px;
|
|
||||||
//** Carets increase slightly in size for larger components.
|
|
||||||
$caret-width-large: 5px;
|
|
||||||
|
|
||||||
//== Tables
|
|
||||||
//
|
|
||||||
//## Customizes the `.table` component with basic values, each used across all table variations.
|
|
||||||
|
|
||||||
//** Padding for `<th>`s and `<td>`s.
|
|
||||||
$table-cell-padding: $ts $rhs $bs $lhs;
|
|
||||||
//** Padding for cells in `.table-condensed`.
|
|
||||||
$table-condensed-cell-padding: $ts $rhs $bs $lhs;
|
|
||||||
|
|
||||||
//** Default background color used for all tables.
|
|
||||||
$table-bg: transparent;
|
|
||||||
//** Background color used for `.table-striped`.
|
|
||||||
$table-bg-accent: $black;
|
|
||||||
//** Background color used for `.table-hover`.
|
|
||||||
$table-bg-hover: #f5f5f5;
|
|
||||||
$table-bg-active: $table-bg-hover;
|
|
||||||
|
|
||||||
//** Border color for table and cell borders.
|
|
||||||
$table-border-color: $gray;
|
|
||||||
|
|
||||||
//== Buttons
|
|
||||||
//
|
|
||||||
//## For each of Bootstrap's buttons, define text, background and border color.
|
|
||||||
|
|
||||||
$btn-font-weight: normal;
|
|
||||||
|
|
||||||
$btn-default-color: $black;
|
|
||||||
$btn-default-bg: $grayLight;
|
|
||||||
$btn-default-border: $grayLight;
|
|
||||||
|
|
||||||
$btn-primary-color: $black;
|
|
||||||
$btn-primary-bg: $cyanDark;
|
|
||||||
$btn-primary-border: $grayLight;
|
|
||||||
|
|
||||||
$btn-success-color: #fff;
|
|
||||||
$btn-success-bg: $brand-success;
|
|
||||||
$btn-success-border: $btn-success-bg;
|
|
||||||
|
|
||||||
$btn-info-color: #fff;
|
|
||||||
$btn-info-bg: $brand-info;
|
|
||||||
$btn-info-border: $btn-info-bg;
|
|
||||||
|
|
||||||
$btn-warning-color: #fff;
|
|
||||||
$btn-warning-bg: $brand-warning;
|
|
||||||
$btn-warning-border: $btn-warning-bg;
|
|
||||||
|
|
||||||
$btn-danger-color: #fff;
|
|
||||||
$btn-danger-bg: $brand-danger;
|
|
||||||
$btn-danger-border: $btn-danger-bg;
|
|
||||||
|
|
||||||
$btn-link-disabled-color: $gray-light;
|
|
||||||
|
|
||||||
//== Forms
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** `<input>` background color
|
|
||||||
$input-bg: $cyanDark;
|
|
||||||
//** `<input disabled>` background color
|
|
||||||
$input-bg-disabled: $gray-lighter;
|
|
||||||
|
|
||||||
//** Text color for `<input>`s
|
|
||||||
$input-color: $white;
|
|
||||||
//** `<input>` border color
|
|
||||||
$input-border: #ccc;
|
|
||||||
|
|
||||||
// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
|
|
||||||
//** Default `.form-control` border radius
|
|
||||||
// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
|
|
||||||
$input-border-radius: $border-radius-base;
|
|
||||||
//** Large `.form-control` border radius
|
|
||||||
$input-border-radius-large: $border-radius-large;
|
|
||||||
//** Small `.form-control` border radius
|
|
||||||
$input-border-radius-small: $border-radius-small;
|
|
||||||
|
|
||||||
//** Border color for inputs on focus
|
|
||||||
$input-border-focus: $black;
|
|
||||||
|
|
||||||
//** Placeholder text color
|
|
||||||
$input-color-placeholder: $black;
|
|
||||||
|
|
||||||
//** Default `.form-control` height
|
|
||||||
$input-height-base: $line-height-computed;
|
|
||||||
//** Large `.form-control` height
|
|
||||||
$input-height-large: $input-height-base;
|
|
||||||
//** Small `.form-control` height
|
|
||||||
$input-height-small: $input-height-base;
|
|
||||||
|
|
||||||
$legend-color: $gray-dark;
|
|
||||||
$legend-border-color: #e5e5e5;
|
|
||||||
|
|
||||||
//** Background color for textual input addons
|
|
||||||
$input-group-addon-bg: $gray-lighter;
|
|
||||||
//** Border color for textual input addons
|
|
||||||
$input-group-addon-border-color: $input-border;
|
|
||||||
|
|
||||||
//** Disabled cursor for form controls and buttons.
|
|
||||||
$cursor-disabled: not-allowed;
|
|
||||||
|
|
||||||
//== Dropdowns
|
|
||||||
//
|
|
||||||
//## Dropdown menu container and contents.
|
|
||||||
|
|
||||||
//** Background for the dropdown menu.
|
|
||||||
$dropdown-bg: $gray;
|
|
||||||
//** Dropdown menu `border-color`.
|
|
||||||
$dropdown-border: rgb(0, 0, 0);
|
|
||||||
//** Dropdown menu `border-color` **for IE8**.
|
|
||||||
$dropdown-fallback-border: #ccc;
|
|
||||||
//** Divider color for between dropdown items.
|
|
||||||
$dropdown-divider-bg: $black;
|
|
||||||
|
|
||||||
//** Dropdown link text color.
|
|
||||||
$dropdown-link-color: $black;
|
|
||||||
//** Hover color for dropdown links.
|
|
||||||
$dropdown-link-hover-color: $gray;
|
|
||||||
//** Hover background for dropdown links.
|
|
||||||
$dropdown-link-hover-bg: $black;
|
|
||||||
|
|
||||||
//** Active dropdown menu item text color.
|
|
||||||
$dropdown-link-active-color: $component-active-color;
|
|
||||||
//** Active dropdown menu item background color.
|
|
||||||
$dropdown-link-active-bg: $component-active-bg;
|
|
||||||
|
|
||||||
//** Disabled dropdown menu item background color.
|
|
||||||
$dropdown-link-disabled-color: $gray-light;
|
|
||||||
|
|
||||||
//** Text color for headers within dropdown menus.
|
|
||||||
$dropdown-header-color: $black;
|
|
||||||
|
|
||||||
//** Deprecated `$dropdown-caret-color` as of v3.1.0
|
|
||||||
$dropdown-caret-color: #000;
|
|
||||||
|
|
||||||
//-- Z-index master list
|
|
||||||
//
|
|
||||||
// Warning: Avoid customizing these values. They're used for a bird's eye view
|
|
||||||
// of components dependent on the z-axis and are designed to all work together.
|
|
||||||
//
|
|
||||||
// Note: These variables are not generated into the Customizer.
|
|
||||||
|
|
||||||
$zindex-navbar: 1000;
|
|
||||||
$zindex-dropdown: 1000;
|
|
||||||
$zindex-popover: 1060;
|
|
||||||
$zindex-tooltip: 1070;
|
|
||||||
$zindex-navbar-fixed: 1030;
|
|
||||||
$zindex-modal: 1040;
|
|
||||||
|
|
||||||
//== Media queries breakpoints
|
|
||||||
//
|
|
||||||
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
|
|
||||||
|
|
||||||
// Extra small screen / phone
|
|
||||||
//** Deprecated `$screen-xs` as of v3.0.1
|
|
||||||
$screen-xs: 480px;
|
|
||||||
//** Deprecated `$screen-xs-min` as of v3.2.0
|
|
||||||
$screen-xs-min: $screen-xs;
|
|
||||||
//** Deprecated `$screen-phone` as of v3.0.1
|
|
||||||
$screen-phone: $screen-xs-min;
|
|
||||||
|
|
||||||
// Small screen / tablet
|
|
||||||
//** Deprecated `$screen-sm` as of v3.0.1
|
|
||||||
$screen-sm: 768px;
|
|
||||||
$screen-sm-min: $screen-sm;
|
|
||||||
//** Deprecated `$screen-tablet` as of v3.0.1
|
|
||||||
$screen-tablet: $screen-sm-min;
|
|
||||||
|
|
||||||
// Medium screen / desktop
|
|
||||||
//** Deprecated `$screen-md` as of v3.0.1
|
|
||||||
$screen-md: 992px;
|
|
||||||
$screen-md-min: $screen-md;
|
|
||||||
//** Deprecated `$screen-desktop` as of v3.0.1
|
|
||||||
$screen-desktop: $screen-md-min;
|
|
||||||
|
|
||||||
// Large screen / wide desktop
|
|
||||||
//** Deprecated `$screen-lg` as of v3.0.1
|
|
||||||
$screen-lg: 1200px;
|
|
||||||
$screen-lg-min: $screen-lg;
|
|
||||||
//** Deprecated `$screen-lg-desktop` as of v3.0.1
|
|
||||||
$screen-lg-desktop: $screen-lg-min;
|
|
||||||
|
|
||||||
// So media queries don't overlap when required, provide a maximum
|
|
||||||
$screen-xs-max: ($screen-sm-min - 1);
|
|
||||||
$screen-sm-max: ($screen-md-min - 1);
|
|
||||||
$screen-md-max: ($screen-lg-min - 1);
|
|
||||||
|
|
||||||
//== Grid system
|
|
||||||
//
|
|
||||||
//## Define your custom responsive grid.
|
|
||||||
|
|
||||||
//** Number of columns in the grid.
|
|
||||||
$grid-columns: 12;
|
|
||||||
//** Padding between columns. Gets divided in half for the left and right.
|
|
||||||
$grid-gutter-width: ($baseWidth * 2);
|
|
||||||
// Navbar collapse
|
|
||||||
//** Point at which the navbar becomes uncollapsed.
|
|
||||||
$grid-float-breakpoint: $screen-sm-min;
|
|
||||||
//** Point at which the navbar begins collapsing.
|
|
||||||
$grid-float-breakpoint-max: ($grid-float-breakpoint);
|
|
||||||
|
|
||||||
//== Container sizes
|
|
||||||
//
|
|
||||||
//## Define the maximum width of `.container` for different screen sizes.
|
|
||||||
|
|
||||||
// Small screen / tablet
|
|
||||||
$container-tablet: (720px + $grid-gutter-width);
|
|
||||||
//** For `$screen-sm-min` and up.
|
|
||||||
$container-sm: $container-tablet;
|
|
||||||
|
|
||||||
// Medium screen / desktop
|
|
||||||
$container-desktop: (940px + $grid-gutter-width);
|
|
||||||
//** For `$screen-md-min` and up.
|
|
||||||
$container-md: $container-desktop;
|
|
||||||
|
|
||||||
// Large screen / wide desktop
|
|
||||||
$container-large-desktop: (1140px + $grid-gutter-width);
|
|
||||||
//** For `$screen-lg-min` and up.
|
|
||||||
$container-lg: $container-large-desktop;
|
|
||||||
|
|
||||||
//== Navbar
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
// Basics of a navbar
|
|
||||||
$navbar-height: 0px;
|
|
||||||
$navbar-margin-bottom: $line-height-computed;
|
|
||||||
$navbar-border-radius: $border-radius-base;
|
|
||||||
$navbar-padding-horizontal: ($baseWidth * 2);
|
|
||||||
$navbar-padding-vertical: 0;
|
|
||||||
$navbar-collapse-max-height: 340px;
|
|
||||||
|
|
||||||
$navbar-default-color: $black;
|
|
||||||
$navbar-default-bg: $grayLight;
|
|
||||||
$navbar-default-border: $navbar-default-bg;
|
|
||||||
|
|
||||||
// Navbar links
|
|
||||||
$navbar-default-link-color: $black;
|
|
||||||
$navbar-default-link-hover-color: $white;
|
|
||||||
$navbar-default-link-hover-bg: $black;
|
|
||||||
$navbar-default-link-active-color: $white;
|
|
||||||
$navbar-default-link-active-bg: $black;
|
|
||||||
$navbar-default-link-disabled-color: $gray;
|
|
||||||
$navbar-default-link-disabled-bg: transparent;
|
|
||||||
|
|
||||||
// Navbar brand label
|
|
||||||
$navbar-default-brand-color: $navbar-default-link-color;
|
|
||||||
$navbar-default-brand-hover-color: $navbar-default-brand-color;
|
|
||||||
$navbar-default-brand-hover-bg: transparent;
|
|
||||||
|
|
||||||
// Navbar toggle
|
|
||||||
$navbar-default-toggle-hover-bg: #ddd;
|
|
||||||
$navbar-default-toggle-icon-bar-bg: #888;
|
|
||||||
$navbar-default-toggle-border-color: #ddd;
|
|
||||||
|
|
||||||
// Inverted navbar
|
|
||||||
// Reset inverted navbar basics
|
|
||||||
$navbar-inverse-color: $gray;
|
|
||||||
$navbar-inverse-bg: $black;
|
|
||||||
$navbar-inverse-border: $navbar-inverse-bg;
|
|
||||||
|
|
||||||
// Inverted navbar links
|
|
||||||
$navbar-inverse-link-color: $gray-light;
|
|
||||||
$navbar-inverse-link-hover-color: $black;
|
|
||||||
$navbar-inverse-link-hover-bg: $grayLight;
|
|
||||||
$navbar-inverse-link-active-color: $white;
|
|
||||||
$navbar-inverse-link-active-bg: $grayDark;
|
|
||||||
$navbar-inverse-link-disabled-color: $gray;
|
|
||||||
$navbar-inverse-link-disabled-bg: transparent;
|
|
||||||
|
|
||||||
// Inverted navbar brand label
|
|
||||||
$navbar-inverse-brand-color: $navbar-inverse-link-color;
|
|
||||||
$navbar-inverse-brand-hover-color: #fff;
|
|
||||||
$navbar-inverse-brand-hover-bg: transparent;
|
|
||||||
|
|
||||||
// Inverted navbar toggle
|
|
||||||
$navbar-inverse-toggle-hover-bg: $grayLight;
|
|
||||||
$navbar-inverse-toggle-icon-bar-bg: #fff;
|
|
||||||
$navbar-inverse-toggle-border-color: #333;
|
|
||||||
|
|
||||||
//== Navs
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//=== Shared nav styles
|
|
||||||
$nav-link-padding: 0 $baseWidth;
|
|
||||||
$nav-link-hover-bg: $gray-lighter;
|
|
||||||
|
|
||||||
$nav-disabled-link-color: $gray-light;
|
|
||||||
$nav-disabled-link-hover-color: $gray-light;
|
|
||||||
|
|
||||||
//== Tabs
|
|
||||||
$nav-tabs-border-color: #ddd;
|
|
||||||
|
|
||||||
$nav-tabs-link-hover-border-color: $gray-lighter;
|
|
||||||
|
|
||||||
$nav-tabs-active-link-hover-bg: $black;
|
|
||||||
$nav-tabs-active-link-hover-color: $white;
|
|
||||||
|
|
||||||
$nav-tabs-justified-active-link-border-color: $body-bg;
|
|
||||||
|
|
||||||
//== Pills
|
|
||||||
$nav-pills-border-radius: $border-radius-base;
|
|
||||||
$nav-pills-active-link-hover-bg: $component-active-bg;
|
|
||||||
$nav-pills-active-link-hover-color: $component-active-color;
|
|
||||||
|
|
||||||
//== Pagination
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$pagination-color: $black;
|
|
||||||
$pagination-bg: $gray;
|
|
||||||
$pagination-border: #ddd;
|
|
||||||
|
|
||||||
$pagination-hover-color: $link-hover-color;
|
|
||||||
$pagination-hover-bg: $gray-lighter;
|
|
||||||
$pagination-hover-border: #ddd;
|
|
||||||
|
|
||||||
$pagination-active-color: #fff;
|
|
||||||
$pagination-active-bg: $brand-primary;
|
|
||||||
$pagination-active-border: $brand-primary;
|
|
||||||
|
|
||||||
$pagination-disabled-color: $gray-light;
|
|
||||||
$pagination-disabled-bg: #fff;
|
|
||||||
$pagination-disabled-border: #ddd;
|
|
||||||
|
|
||||||
//== Pager
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$pager-bg: $pagination-bg;
|
|
||||||
$pager-border: $pagination-border;
|
|
||||||
$pager-border-radius: 0;
|
|
||||||
|
|
||||||
$pager-hover-bg: $pagination-hover-bg;
|
|
||||||
|
|
||||||
$pager-active-bg: $pagination-active-bg;
|
|
||||||
$pager-active-color: $pagination-active-color;
|
|
||||||
|
|
||||||
$pager-disabled-color: $pagination-disabled-color;
|
|
||||||
|
|
||||||
//== Jumbotron
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$jumbotron-padding: ($ts) ($rhs + $baseWidth) ($bs) ($lhs + $baseWidth);
|
|
||||||
$jumbotron-color: $white;
|
|
||||||
$jumbotron-bg: transparent;
|
|
||||||
$jumbotron-heading-color: inherit;
|
|
||||||
$jumbotron-font-size: $font-size-base;
|
|
||||||
|
|
||||||
//== Form states and alerts
|
|
||||||
//
|
|
||||||
//## Define colors for form feedback states and, by default, alerts.
|
|
||||||
|
|
||||||
$state-success-text: $green;
|
|
||||||
$state-success-bg: $greenDark;
|
|
||||||
$state-success-border: $state-success-bg;
|
|
||||||
|
|
||||||
$state-info-text: $yellow;
|
|
||||||
$state-info-bg: $brown;
|
|
||||||
$state-info-border: $state-info-bg;
|
|
||||||
|
|
||||||
$state-warning-text: $magenta;
|
|
||||||
$state-warning-bg: $magentaDark;
|
|
||||||
$state-warning-border: $state-warning-bg;
|
|
||||||
|
|
||||||
$state-danger-text: $red;
|
|
||||||
$state-danger-bg: $black;
|
|
||||||
$state-danger-border: $state-danger-bg;
|
|
||||||
|
|
||||||
//== Tooltips
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Tooltip max width
|
|
||||||
$tooltip-max-width: ($baseWidth * 25);
|
|
||||||
//** Tooltip text color
|
|
||||||
$tooltip-color: $white;
|
|
||||||
//** Tooltip background color
|
|
||||||
$tooltip-bg: $grayDark;
|
|
||||||
$tooltip-opacity: 1;
|
|
||||||
|
|
||||||
//** Tooltip arrow width
|
|
||||||
$tooltip-arrow-width: 0px;
|
|
||||||
//** Tooltip arrow color
|
|
||||||
$tooltip-arrow-color: $tooltip-bg;
|
|
||||||
|
|
||||||
//== Popovers
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Popover body background color
|
|
||||||
$popover-bg: $gray;
|
|
||||||
//** Popover maximum width
|
|
||||||
$popover-max-width: ($baseWidth * 20);
|
|
||||||
//** Popover border color
|
|
||||||
$popover-border-color: rgb(0, 0, 0);
|
|
||||||
//** Popover fallback border color
|
|
||||||
$popover-fallback-border-color: #ccc;
|
|
||||||
|
|
||||||
//** Popover title background color
|
|
||||||
$popover-title-bg: $greenDark;
|
|
||||||
|
|
||||||
//** Popover arrow width
|
|
||||||
$popover-arrow-width: 10px;
|
|
||||||
//** Popover arrow color
|
|
||||||
$popover-arrow-color: $popover-bg;
|
|
||||||
|
|
||||||
//** Popover outer arrow width
|
|
||||||
$popover-arrow-outer-width: ($popover-arrow-width + 1);
|
|
||||||
//** Popover outer arrow color
|
|
||||||
$popover-arrow-outer-color: $popover-border-color;
|
|
||||||
//** Popover outer arrow fallback color
|
|
||||||
$popover-arrow-outer-fallback-color: $popover-fallback-border-color;
|
|
||||||
|
|
||||||
//== Labels
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Default label background color
|
|
||||||
$label-default-bg: $gray-light;
|
|
||||||
//** Primary label background color
|
|
||||||
$label-primary-bg: $brand-primary-bg;
|
|
||||||
//** Success label background color
|
|
||||||
$label-success-bg: $brand-success;
|
|
||||||
//** Info label background color
|
|
||||||
$label-info-bg: $brand-info;
|
|
||||||
//** Warning label background color
|
|
||||||
$label-warning-bg: $brand-warning;
|
|
||||||
//** Danger label background color
|
|
||||||
$label-danger-bg: $brand-danger;
|
|
||||||
|
|
||||||
//** Default label text color
|
|
||||||
$label-color: #fff;
|
|
||||||
//** Default text color of a linked label
|
|
||||||
$label-link-hover-color: #fff;
|
|
||||||
|
|
||||||
//== Modals
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Padding applied to the modal body
|
|
||||||
$modal-inner-padding: 0 $baseWidth;
|
|
||||||
|
|
||||||
//** Padding applied to the modal title
|
|
||||||
$modal-title-padding: 0 $baseWidth;
|
|
||||||
//** Modal title line-height
|
|
||||||
$modal-title-line-height: $line-height-base;
|
|
||||||
|
|
||||||
//** Background color of modal content area
|
|
||||||
$modal-content-bg: $gray;
|
|
||||||
//** Modal content border color
|
|
||||||
$modal-content-border-color: rgb(0, 0, 0);
|
|
||||||
//** Modal content border color **for IE8**
|
|
||||||
$modal-content-fallback-border-color: #999;
|
|
||||||
|
|
||||||
//** Modal backdrop background color
|
|
||||||
$modal-backdrop-bg: #000;
|
|
||||||
//** Modal backdrop opacity
|
|
||||||
// $modal-backdrop-opacity: @include 5;
|
|
||||||
//** Modal header border color
|
|
||||||
$modal-header-border-color: #e5e5e5;
|
|
||||||
//** Modal footer border color
|
|
||||||
$modal-footer-border-color: $modal-header-border-color;
|
|
||||||
|
|
||||||
$modal-lg: 900px;
|
|
||||||
$modal-md: 600px;
|
|
||||||
$modal-sm: 300px;
|
|
||||||
|
|
||||||
//== Alerts
|
|
||||||
//
|
|
||||||
//## Define alert colors, border radius, and padding.
|
|
||||||
|
|
||||||
$alert-padding: $line-height-base ($baseWidth * 2);
|
|
||||||
$alert-border-radius: $border-radius-base;
|
|
||||||
$alert-link-font-weight: normal;
|
|
||||||
|
|
||||||
$alert-success-bg: $state-success-bg;
|
|
||||||
$alert-success-text: $state-success-text;
|
|
||||||
$alert-success-border: $state-success-border;
|
|
||||||
|
|
||||||
$alert-info-bg: $state-info-bg;
|
|
||||||
$alert-info-text: $state-info-text;
|
|
||||||
$alert-info-border: $state-info-border;
|
|
||||||
|
|
||||||
$alert-warning-bg: $state-warning-bg;
|
|
||||||
$alert-warning-text: $state-warning-text;
|
|
||||||
$alert-warning-border: $state-warning-border;
|
|
||||||
|
|
||||||
$alert-danger-bg: $state-danger-bg;
|
|
||||||
$alert-danger-text: $state-danger-text;
|
|
||||||
$alert-danger-border: $state-danger-border;
|
|
||||||
|
|
||||||
//== Progress bars
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Background color of the whole progress component
|
|
||||||
$progress-bg: $black;
|
|
||||||
//** Progress bar text color
|
|
||||||
$progress-bar-color: $black;
|
|
||||||
//** Variable for setting rounded corners on progress bar.
|
|
||||||
$progress-border-radius: $border-radius-base;
|
|
||||||
|
|
||||||
//** Default progress bar color
|
|
||||||
$progress-bar-bg: $brand-primary;
|
|
||||||
//** Success progress bar color
|
|
||||||
$progress-bar-success-bg: $brand-success;
|
|
||||||
//** Warning progress bar color
|
|
||||||
$progress-bar-warning-bg: $brand-warning;
|
|
||||||
//** Danger progress bar color
|
|
||||||
$progress-bar-danger-bg: $brand-danger;
|
|
||||||
//** Info progress bar color
|
|
||||||
$progress-bar-info-bg: $brand-info;
|
|
||||||
|
|
||||||
//== List group
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Background color on `.list-group-item`
|
|
||||||
$list-group-bg: $gray;
|
|
||||||
//** `.list-group-item` border color
|
|
||||||
$list-group-border: #ddd;
|
|
||||||
//** List group border radius
|
|
||||||
$list-group-border-radius: $border-radius-base;
|
|
||||||
|
|
||||||
//** Background color of single list items on hover
|
|
||||||
$list-group-hover-bg: $black;
|
|
||||||
//** Text color of active list items
|
|
||||||
$list-group-active-color: $component-active-color;
|
|
||||||
//** Background color of active list items
|
|
||||||
$list-group-active-bg: $component-active-bg;
|
|
||||||
//** Border color of active list elements
|
|
||||||
$list-group-active-border: $list-group-active-bg;
|
|
||||||
//** Text color for content within active list items
|
|
||||||
$list-group-active-text-color: $component-active-color;
|
|
||||||
|
|
||||||
//** Text color of disabled list items
|
|
||||||
$list-group-disabled-color: $gray-dark;
|
|
||||||
//** Background color of disabled list items
|
|
||||||
$list-group-disabled-bg: $gray-lighter;
|
|
||||||
//** Text color for content within disabled list items
|
|
||||||
$list-group-disabled-text-color: $list-group-disabled-color;
|
|
||||||
|
|
||||||
$list-group-link-color: $black;
|
|
||||||
$list-group-link-hover-color: $list-group-link-color;
|
|
||||||
$list-group-link-heading-color: #333;
|
|
||||||
|
|
||||||
//== Panels
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$panel-bg: $gray;
|
|
||||||
$panel-body-padding: 0 $rhsNB 0 $lhsNB;
|
|
||||||
$panel-heading-padding: 0 $rhsNB 0 $lhsNB;
|
|
||||||
$panel-footer-padding: $panel-heading-padding;
|
|
||||||
$panel-border-radius: $border-radius-base;
|
|
||||||
|
|
||||||
//** Border color for elements within panels
|
|
||||||
$panel-inner-border: #ddd;
|
|
||||||
$panel-footer-bg: #f5f5f5;
|
|
||||||
|
|
||||||
$panel-default-text: $white;
|
|
||||||
$panel-default-border: #ddd;
|
|
||||||
$panel-default-heading-bg: $grayDark;
|
|
||||||
|
|
||||||
$panel-primary-text: $white;
|
|
||||||
$panel-primary-border: $brand-primary;
|
|
||||||
$panel-primary-heading-bg: $cyanDark;
|
|
||||||
|
|
||||||
$panel-success-text: $state-success-text;
|
|
||||||
$panel-success-border: $state-success-border;
|
|
||||||
$panel-success-heading-bg: $state-success-bg;
|
|
||||||
|
|
||||||
$panel-info-text: $state-info-text;
|
|
||||||
$panel-info-border: $state-info-border;
|
|
||||||
$panel-info-heading-bg: $state-info-bg;
|
|
||||||
|
|
||||||
$panel-warning-text: $state-warning-text;
|
|
||||||
$panel-warning-border: $state-warning-border;
|
|
||||||
$panel-warning-heading-bg: $state-warning-bg;
|
|
||||||
|
|
||||||
$panel-danger-text: $state-danger-text;
|
|
||||||
$panel-danger-border: $state-danger-border;
|
|
||||||
$panel-danger-heading-bg: $state-danger-bg;
|
|
||||||
|
|
||||||
//== Thumbnails
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Padding around the thumbnail image
|
|
||||||
$thumbnail-padding: 4px;
|
|
||||||
//** Thumbnail background color
|
|
||||||
$thumbnail-bg: $body-bg;
|
|
||||||
//** Thumbnail border color
|
|
||||||
$thumbnail-border: #ddd;
|
|
||||||
//** Thumbnail border radius
|
|
||||||
$thumbnail-border-radius: $border-radius-base;
|
|
||||||
|
|
||||||
//** Custom text color for thumbnail captions
|
|
||||||
$thumbnail-caption-color: $text-color;
|
|
||||||
//** Padding around the thumbnail caption
|
|
||||||
$thumbnail-caption-padding: 9px;
|
|
||||||
|
|
||||||
//== Wells
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$well-bg: $greenDark;
|
|
||||||
$well-border: $well-bg;
|
|
||||||
|
|
||||||
//== Badges
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$badge-color: $black;
|
|
||||||
//** Linked badge text color on hover
|
|
||||||
$badge-link-hover-color: #fff;
|
|
||||||
$badge-bg: $gray-light;
|
|
||||||
|
|
||||||
//** Badge text color in active nav link
|
|
||||||
$badge-active-color: $link-color;
|
|
||||||
//** Badge background color in active nav link
|
|
||||||
$badge-active-bg: $black;
|
|
||||||
|
|
||||||
$badge-font-weight: normal;
|
|
||||||
$badge-line-height: $line-height-base;
|
|
||||||
$badge-border-radius: 0;
|
|
||||||
|
|
||||||
//== Breadcrumbs
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$breadcrumb-padding-vertical: 8px;
|
|
||||||
$breadcrumb-padding-horizontal: 15px;
|
|
||||||
//** Breadcrumb background color
|
|
||||||
$breadcrumb-bg: #f5f5f5;
|
|
||||||
//** Breadcrumb text color
|
|
||||||
$breadcrumb-color: #ccc;
|
|
||||||
//** Text color of current page in the breadcrumb
|
|
||||||
$breadcrumb-active-color: $gray-light;
|
|
||||||
//** Textual separator for between breadcrumb elements
|
|
||||||
$breadcrumb-separator: "/";
|
|
||||||
|
|
||||||
//== Carousel
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$carousel-text-shadow: none;
|
|
||||||
|
|
||||||
$carousel-control-color: #fff;
|
|
||||||
$carousel-control-width: 15%;
|
|
||||||
$carousel-control-opacity: 1;
|
|
||||||
$carousel-control-font-size: $font-size-base;
|
|
||||||
|
|
||||||
$carousel-indicator-active-bg: #fff;
|
|
||||||
$carousel-indicator-border-color: #fff;
|
|
||||||
|
|
||||||
$carousel-caption-color: #fff;
|
|
||||||
|
|
||||||
//== Close
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$close-font-weight: normal;
|
|
||||||
$close-color: #000;
|
|
||||||
$close-text-shadow: none;
|
|
||||||
|
|
||||||
//== Code
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
$code-color: #c7254e;
|
|
||||||
$code-bg: #f9f2f4;
|
|
||||||
|
|
||||||
$kbd-color: #fff;
|
|
||||||
$kbd-bg: #333;
|
|
||||||
|
|
||||||
$pre-bg: #f5f5f5;
|
|
||||||
$pre-color: $gray-dark;
|
|
||||||
$pre-border-color: #ccc;
|
|
||||||
$pre-scrollable-max-height: 340px;
|
|
||||||
|
|
||||||
//== Type
|
|
||||||
//
|
|
||||||
//##
|
|
||||||
|
|
||||||
//** Horizontal offset for forms and lists.
|
|
||||||
$component-offset-horizontal: 180px;
|
|
||||||
//** Text muted color
|
|
||||||
$text-muted: $gray-dark;
|
|
||||||
//** Abbreviations and acronyms border color
|
|
||||||
$abbr-border-color: $gray-light;
|
|
||||||
//** Headings small color
|
|
||||||
$headings-small-color: $gray-light;
|
|
||||||
//** Blockquote small color
|
|
||||||
$blockquote-small-color: $gray-light;
|
|
||||||
//** Blockquote font size
|
|
||||||
$blockquote-font-size: $font-size-base;
|
|
||||||
//** Blockquote border color
|
|
||||||
$blockquote-border-color: $gray-lighter;
|
|
||||||
//** Page header border color
|
|
||||||
$page-header-border-color: $gray-lighter;
|
|
||||||
//** Width of horizontal description list titles
|
|
||||||
$dl-horizontal-offset: $component-offset-horizontal;
|
|
||||||
//** Horizontal line color.
|
|
||||||
$hr-border: $black;
|
|
|
@ -1,39 +0,0 @@
|
||||||
$blue: #5555ff;
|
|
||||||
$cyan: #55ffff;
|
|
||||||
$green: #55ff55;
|
|
||||||
$indigo: #ff55ff;
|
|
||||||
$red: #ff5555;
|
|
||||||
$yellow: #fefe54;
|
|
||||||
$orange: #a85400;
|
|
||||||
$pink: #fe54fe;
|
|
||||||
$purple: #fe5454;
|
|
||||||
$primary: #fefe54;
|
|
||||||
$body-bg: #000084;
|
|
||||||
$gray-300: #bbb;
|
|
||||||
$body-color: $gray-300;
|
|
||||||
$link-hover-color: $white;
|
|
||||||
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
|
||||||
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
|
||||||
$navbar-dark-color: $gray-300;
|
|
||||||
$navbar-light-brand-color: $gray-300;
|
|
||||||
$success: #00aa00;
|
|
||||||
$danger: #aa0000;
|
|
||||||
$info: #00aaaa;
|
|
||||||
$warning: #aa00aa;
|
|
||||||
$navbar-dark-active-color: $gray-100;
|
|
||||||
$enable-rounded: false;
|
|
||||||
$input-color: $white;
|
|
||||||
$input-bg: rgb(102, 102, 102);
|
|
||||||
$input-disabled-bg: $gray-800;
|
|
||||||
$nav-tabs-link-active-color: $gray-100;
|
|
||||||
$navbar-dark-hover-color: rgba($gray-300, 0.75);
|
|
||||||
$light: $gray-800;
|
|
||||||
$navbar-light-disabled-color: $gray-800;
|
|
||||||
$navbar-light-active-color: $gray-100;
|
|
||||||
$navbar-light-hover-color: $gray-200;
|
|
||||||
$navbar-light-color: $gray-300;
|
|
||||||
$card-bg: $gray-800;
|
|
||||||
$card-border-color: $white;
|
|
||||||
$input-placeholder-color: $gray-500;
|
|
||||||
$mark-bg: #463b00;
|
|
||||||
$secondary: $gray-900;
|
|
|
@ -1,37 +0,0 @@
|
||||||
$blue: #01cdfe;
|
|
||||||
$indigo: #b967ff;
|
|
||||||
$purple: #b967ff;
|
|
||||||
$pink: rgb(255, 64, 186);
|
|
||||||
$red: rgb(255, 95, 110);
|
|
||||||
$orange: rgb(255, 167, 93);
|
|
||||||
$yellow: #fffb96;
|
|
||||||
$green: #05ffa1;
|
|
||||||
$teal: #01cdfe;
|
|
||||||
$cyan: #01cdfe;
|
|
||||||
$enable-shadows: true;
|
|
||||||
$enable-gradients: true;
|
|
||||||
$enable-responsive-font-sizes: true;
|
|
||||||
$body-bg: $gray-900;
|
|
||||||
$body-color: $gray-200;
|
|
||||||
$border-radius: 1rem;
|
|
||||||
$border-radius-lg: 1rem;
|
|
||||||
$font-family-monospace: Arial, "Noto Sans", sans-serif;
|
|
||||||
$yiq-text-light: $gray-300;
|
|
||||||
$secondary: $blue;
|
|
||||||
$text-muted: $gray-500;
|
|
||||||
$primary: $pink;
|
|
||||||
$navbar-light-hover-color: rgba($primary, 0.7);
|
|
||||||
$light: darken($gray-100, 1.5);
|
|
||||||
$font-family-sans-serif: "Lucida Console", Monaco, monospace;
|
|
||||||
$card-bg: $body-bg;
|
|
||||||
$navbar-dark-color: rgba($body-bg, 0.5);
|
|
||||||
$navbar-light-active-color: rgba($gray-200, 0.9);
|
|
||||||
$navbar-light-disabled-color: rgba($gray-200, 0.3);
|
|
||||||
$navbar-light-color: rgba($white, 0.5);
|
|
||||||
$input-bg: $gray-700;
|
|
||||||
$input-color: $gray-200;
|
|
||||||
$input-disabled-bg: $gray-800;
|
|
||||||
$input-border-color: $gray-800;
|
|
||||||
$mark-bg: $gray-600;
|
|
||||||
$pre-color: $gray-200;
|
|
||||||
mark-bg: $gray-600;
|
|
|
@ -1,25 +0,0 @@
|
||||||
$blue: #01cdfe;
|
|
||||||
$indigo: #b967ff;
|
|
||||||
$purple: #b967ff;
|
|
||||||
$pink: rgb(255, 64, 186);
|
|
||||||
$red: rgb(255, 95, 110);
|
|
||||||
$orange: rgb(255, 167, 93);
|
|
||||||
$yellow: #fffb96;
|
|
||||||
$green: #05ffa1;
|
|
||||||
$teal: #01cdfe;
|
|
||||||
$cyan: #01cdfe;
|
|
||||||
$enable-shadows: true;
|
|
||||||
$enable-gradients: true;
|
|
||||||
$enable-responsive-font-sizes: true;
|
|
||||||
$body-bg: $gray-100;
|
|
||||||
$body-color: $gray-700;
|
|
||||||
$border-radius: 1rem;
|
|
||||||
$border-radius-lg: 1rem;
|
|
||||||
$font-family-monospace: Arial, "Noto Sans", sans-serif;
|
|
||||||
$yiq-text-light: $gray-300;
|
|
||||||
$secondary: $blue;
|
|
||||||
$text-muted: $gray-500;
|
|
||||||
$primary: $pink;
|
|
||||||
$navbar-light-hover-color: rgba($primary, 0.7);
|
|
||||||
$light: darken($gray-100, 1.5);
|
|
||||||
$font-family-sans-serif: "Lucida Console", Monaco, monospace;
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { hydrate } from "inferno-hydrate";
|
import { hydrate } from "inferno-hydrate";
|
||||||
import { BrowserRouter } from "inferno-router";
|
import { Router } from "inferno-router";
|
||||||
import { App } from "../shared/components/app/app";
|
import { App } from "../shared/components/app/app";
|
||||||
import { initializeSite } from "../shared/utils";
|
import { initializeSite } from "../shared/utils";
|
||||||
|
|
||||||
import "bootstrap/js/dist/collapse";
|
import "bootstrap/js/dist/collapse";
|
||||||
import "bootstrap/js/dist/dropdown";
|
import "bootstrap/js/dist/dropdown";
|
||||||
|
import { HistoryService } from "../shared/services/HistoryService";
|
||||||
|
|
||||||
const site = window.isoData.site_res;
|
const site = window.isoData.site_res;
|
||||||
initializeSite(site);
|
initializeSite(site);
|
||||||
|
|
||||||
const wrapper = (
|
const wrapper = (
|
||||||
<BrowserRouter>
|
<Router history={HistoryService.history}>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
|
@ -6,19 +6,20 @@ import { Helmet } from "inferno-helmet";
|
||||||
import { matchPath, StaticRouter } from "inferno-router";
|
import { matchPath, StaticRouter } from "inferno-router";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
import IsomorphicCookie from "isomorphic-cookie";
|
||||||
import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client";
|
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import serialize from "serialize-javascript";
|
import serialize from "serialize-javascript";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { App } from "../shared/components/app/app";
|
import { App } from "../shared/components/app/app";
|
||||||
import { getHttpBase, getHttpBaseInternal } from "../shared/env";
|
import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
|
||||||
import {
|
import {
|
||||||
ILemmyConfig,
|
ILemmyConfig,
|
||||||
InitialFetchRequest,
|
InitialFetchRequest,
|
||||||
IsoDataOptionalSite,
|
IsoDataOptionalSite,
|
||||||
} from "../shared/interfaces";
|
} from "../shared/interfaces";
|
||||||
import { routes } from "../shared/routes";
|
import { routes } from "../shared/routes";
|
||||||
|
import { RequestState, wrapClient } from "../shared/services/HttpService";
|
||||||
import {
|
import {
|
||||||
ErrorPageData,
|
ErrorPageData,
|
||||||
favIconPngUrl,
|
favIconPngUrl,
|
||||||
|
@ -38,7 +39,7 @@ if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||||
server.use(function (_req, res, next) {
|
server.use(function (_req, res, next) {
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *`
|
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
|
||||||
);
|
);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -64,7 +65,13 @@ Disallow: /search/
|
||||||
|
|
||||||
server.get("/service-worker.js", async (_req, res) => {
|
server.get("/service-worker.js", async (_req, res) => {
|
||||||
res.setHeader("Content-Type", "application/javascript");
|
res.setHeader("Content-Type", "application/javascript");
|
||||||
res.sendFile(path.resolve("./dist/service-worker.js"));
|
res.sendFile(
|
||||||
|
path.resolve(
|
||||||
|
`./dist/service-worker${
|
||||||
|
process.env.NODE_ENV === "development" ? "-development" : ""
|
||||||
|
}.js`
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get("/robots.txt", async (_req, res) => {
|
server.get("/robots.txt", async (_req, res) => {
|
||||||
|
@ -121,7 +128,7 @@ server.get("/*", async (req, res) => {
|
||||||
const getSiteForm: GetSite = { auth };
|
const getSiteForm: GetSite = { auth };
|
||||||
|
|
||||||
const headers = setForwardedHeaders(req.headers);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
const client = new LemmyHttp(getHttpBaseInternal(), headers);
|
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
||||||
|
|
||||||
const { path, url, query } = req;
|
const { path, url, query } = req;
|
||||||
|
|
||||||
|
@ -129,27 +136,30 @@ server.get("/*", async (req, res) => {
|
||||||
// This bypasses errors, so that the client can hit the error on its own,
|
// This bypasses errors, so that the client can hit the error on its own,
|
||||||
// in order to remove the jwt on the browser. Necessary for wrong jwts
|
// in order to remove the jwt on the browser. Necessary for wrong jwts
|
||||||
let site: GetSiteResponse | undefined = undefined;
|
let site: GetSiteResponse | undefined = undefined;
|
||||||
let routeData: any[] = [];
|
const routeData: RequestState<any>[] = [];
|
||||||
let errorPageData: ErrorPageData | undefined;
|
let errorPageData: ErrorPageData | undefined = undefined;
|
||||||
try {
|
let try_site = await client.getSite(getSiteForm);
|
||||||
let try_site: any = await client.getSite(getSiteForm);
|
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
||||||
if (try_site.error == "not_logged_in") {
|
console.error(
|
||||||
console.error(
|
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
);
|
||||||
);
|
getSiteForm.auth = undefined;
|
||||||
getSiteForm.auth = undefined;
|
auth = undefined;
|
||||||
auth = undefined;
|
try_site = await client.getSite(getSiteForm);
|
||||||
try_site = await client.getSite(getSiteForm);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth && isAuthPath(path)) {
|
if (!auth && isAuthPath(path)) {
|
||||||
res.redirect("/login");
|
return res.redirect("/login");
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
site = try_site;
|
if (try_site.state === "success") {
|
||||||
|
site = try_site.data;
|
||||||
initializeSite(site);
|
initializeSite(site);
|
||||||
|
|
||||||
|
if (path != "/setup" && !site.site_view.local_site.site_setup) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
if (site) {
|
if (site) {
|
||||||
const initialFetchReq: InitialFetchRequest = {
|
const initialFetchReq: InitialFetchRequest = {
|
||||||
client,
|
client,
|
||||||
|
@ -160,23 +170,25 @@ server.get("/*", async (req, res) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeRoute?.fetchInitialData) {
|
if (activeRoute?.fetchInitialData) {
|
||||||
routeData = await Promise.all([
|
routeData.push(
|
||||||
...activeRoute.fetchInitialData(initialFetchReq),
|
...(await Promise.all([
|
||||||
]);
|
...activeRoute.fetchInitialData(initialFetchReq),
|
||||||
|
]))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else if (try_site.state === "failed") {
|
||||||
errorPageData = getErrorPageData(error, site);
|
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to the 404 if there's an API error
|
// Redirect to the 404 if there's an API error
|
||||||
if (routeData[0] && routeData[0].error) {
|
if (routeData[0] && routeData[0].state === "failed") {
|
||||||
const error = routeData[0].error;
|
const error = routeData[0].msg;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error === "instance_is_private") {
|
if (error === "instance_is_private") {
|
||||||
return res.redirect(`/signup`);
|
return res.redirect(`/signup`);
|
||||||
} else {
|
} else {
|
||||||
errorPageData = getErrorPageData(error, site);
|
errorPageData = getErrorPageData(new Error(error), site);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,15 +225,15 @@ server.listen(Number(port), hostname, () => {
|
||||||
function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} {
|
} {
|
||||||
let out: { [key: string]: string } = {};
|
const out: { [key: string]: string } = {};
|
||||||
if (headers.host) {
|
if (headers.host) {
|
||||||
out.host = headers.host;
|
out.host = headers.host;
|
||||||
}
|
}
|
||||||
let realIp = headers["x-real-ip"];
|
const realIp = headers["x-real-ip"];
|
||||||
if (realIp) {
|
if (realIp) {
|
||||||
out["x-real-ip"] = realIp as string;
|
out["x-real-ip"] = realIp as string;
|
||||||
}
|
}
|
||||||
let forwardedFor = headers["x-forwarded-for"];
|
const forwardedFor = headers["x-forwarded-for"];
|
||||||
if (forwardedFor) {
|
if (forwardedFor) {
|
||||||
out["x-forwarded-for"] = forwardedFor as string;
|
out["x-forwarded-for"] = forwardedFor as string;
|
||||||
}
|
}
|
||||||
|
@ -234,7 +246,7 @@ process.on("SIGINT", () => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
const iconSizes = [72, 96, 144, 192, 512];
|
||||||
const defaultLogoPathDirectory = path.join(
|
const defaultLogoPathDirectory = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
"dist",
|
"dist",
|
||||||
|
@ -242,12 +254,15 @@ const defaultLogoPathDirectory = path.join(
|
||||||
"icons"
|
"icons"
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function generateManifestBase64(site: Site) {
|
export async function generateManifestBase64({
|
||||||
const url = (
|
my_user,
|
||||||
process.env.NODE_ENV === "development"
|
site_view: {
|
||||||
? "http://localhost:1236/"
|
site,
|
||||||
: getHttpBase()
|
local_site: { community_creation_admin_only },
|
||||||
).replace(/\/$/g, "");
|
},
|
||||||
|
}: GetSiteResponse) {
|
||||||
|
const url = getHttpBaseExternal();
|
||||||
|
|
||||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
|
@ -281,15 +296,58 @@ export async function generateManifestBase64(site: Site) {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: "Search",
|
||||||
|
short_name: "Search",
|
||||||
|
description: "Perform a search.",
|
||||||
|
url: "/search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Communities",
|
||||||
|
url: "/communities",
|
||||||
|
short_name: "Communities",
|
||||||
|
description: "Browse communities",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.concat(
|
||||||
|
my_user
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "Create Post",
|
||||||
|
url: "/create_post",
|
||||||
|
short_name: "Create Post",
|
||||||
|
description: "Create a post.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
|
my_user?.local_user_view.person.admin || !community_creation_admin_only
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "Create Community",
|
||||||
|
url: "/create_community",
|
||||||
|
short_name: "Create Community",
|
||||||
|
description: "Create a community",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
related_applications: [
|
||||||
|
{
|
||||||
|
platform: "f-droid",
|
||||||
|
url: "https://f-droid.org/packages/com.jerboa/",
|
||||||
|
id: "com.jerboa",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchIconPng(iconUrl: string) {
|
async function fetchIconPng(iconUrl: string) {
|
||||||
return await fetch(
|
return await fetch(iconUrl)
|
||||||
iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal())
|
|
||||||
)
|
|
||||||
.then(res => res.blob())
|
.then(res => res.blob())
|
||||||
.then(blob => blob.arrayBuffer());
|
.then(blob => blob.arrayBuffer());
|
||||||
}
|
}
|
||||||
|
@ -330,14 +388,15 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
|
||||||
.then(buf => buf.toString("base64"))}`
|
.then(buf => buf.toString("base64"))}`
|
||||||
: favIconPngUrl;
|
: favIconPngUrl;
|
||||||
|
|
||||||
const eruda = (
|
const erudaStr =
|
||||||
<>
|
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
? renderToString(
|
||||||
<script>eruda.init();</script>
|
<>
|
||||||
</>
|
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
);
|
<script>eruda.init();</script>
|
||||||
|
</>
|
||||||
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
|
)
|
||||||
|
: "";
|
||||||
|
|
||||||
const helmet = Helmet.renderStatic();
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
|
@ -345,9 +404,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ${helmet.htmlAttributes.toString()} lang="en">
|
<html ${helmet.htmlAttributes.toString()}>
|
||||||
<head>
|
<head>
|
||||||
<script>window.isoData = ${JSON.stringify(isoData)}</script>
|
<script>window.isoData = ${serialize(isoData)}</script>
|
||||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
<script>window.lemmyConfig = ${serialize(config)}</script>
|
||||||
|
|
||||||
<!-- A remote debugging utility for mobile -->
|
<!-- A remote debugging utility for mobile -->
|
||||||
|
@ -375,9 +434,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
|
||||||
site &&
|
site &&
|
||||||
`<link
|
`<link
|
||||||
rel="manifest"
|
rel="manifest"
|
||||||
href={${`data:application/manifest+json;base64,${await generateManifestBase64(
|
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
||||||
site.site_view.site
|
site
|
||||||
)}`}}
|
)}`}
|
||||||
/>`
|
/>`
|
||||||
}
|
}
|
||||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { Provider } from "inferno-i18next-dess";
|
import { Provider } from "inferno-i18next-dess";
|
||||||
import { Route, Switch } from "inferno-router";
|
import { Route, Switch } from "inferno-router";
|
||||||
import { IsoDataOptionalSite } from "shared/interfaces";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
|
import { IsoDataOptionalSite } from "../../interfaces";
|
||||||
import { routes } from "../../routes";
|
import { routes } from "../../routes";
|
||||||
import { isAuthPath, setIsoData } from "../../utils";
|
import { isAuthPath, setIsoData } from "../../utils";
|
||||||
import AuthGuard from "../common/auth-guard";
|
import AuthGuard from "../common/auth-guard";
|
||||||
|
|
|
@ -1,35 +1,23 @@
|
||||||
import { Component, createRef, linkEvent } from "inferno";
|
import { Component, createRef, linkEvent } from "inferno";
|
||||||
import { NavLink } from "inferno-router";
|
import { NavLink } from "inferno-router";
|
||||||
import {
|
import {
|
||||||
CommentResponse,
|
|
||||||
GetReportCount,
|
|
||||||
GetReportCountResponse,
|
GetReportCountResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
GetUnreadCount,
|
|
||||||
GetUnreadCountResponse,
|
GetUnreadCountResponse,
|
||||||
GetUnreadRegistrationApplicationCount,
|
|
||||||
GetUnreadRegistrationApplicationCountResponse,
|
GetUnreadRegistrationApplicationCountResponse,
|
||||||
PrivateMessageResponse,
|
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
amAdmin,
|
amAdmin,
|
||||||
canCreateCommunity,
|
canCreateCommunity,
|
||||||
donateLemmyUrl,
|
donateLemmyUrl,
|
||||||
isBrowser,
|
isBrowser,
|
||||||
myAuth,
|
myAuth,
|
||||||
notifyComment,
|
|
||||||
notifyPrivateMessage,
|
|
||||||
numToSI,
|
numToSI,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
toast,
|
toast,
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { PictrsImage } from "../common/pictrs-image";
|
import { PictrsImage } from "../common/pictrs-image";
|
||||||
|
@ -39,9 +27,9 @@ interface NavbarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavbarState {
|
interface NavbarState {
|
||||||
unreadInboxCount: number;
|
unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
|
||||||
unreadReportCount: number;
|
unreadReportCountRes: RequestState<GetReportCountResponse>;
|
||||||
unreadApplicationCount: number;
|
unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
|
||||||
onSiteBanner?(url: string): any;
|
onSiteBanner?(url: string): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,77 +39,48 @@ function handleCollapseClick(i: Navbar) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogOut() {
|
function handleLogOut(i: Navbar) {
|
||||||
UserService.Instance.logout();
|
UserService.Instance.logout();
|
||||||
|
handleCollapseClick(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<NavbarProps, NavbarState> {
|
export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
private wsSub: Subscription;
|
|
||||||
private userSub: Subscription;
|
|
||||||
private unreadInboxCountSub: Subscription;
|
|
||||||
private unreadReportCountSub: Subscription;
|
|
||||||
private unreadApplicationCountSub: Subscription;
|
|
||||||
state: NavbarState = {
|
state: NavbarState = {
|
||||||
unreadInboxCount: 0,
|
unreadInboxCountRes: { state: "empty" },
|
||||||
unreadReportCount: 0,
|
unreadReportCountRes: { state: "empty" },
|
||||||
unreadApplicationCount: 0,
|
unreadApplicationCountRes: { state: "empty" },
|
||||||
};
|
};
|
||||||
subscription: any;
|
|
||||||
collapseButtonRef = createRef<HTMLButtonElement>();
|
collapseButtonRef = createRef<HTMLButtonElement>();
|
||||||
|
mobileMenuRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
// Subscribe to jwt changes
|
// Subscribe to jwt changes
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
// On the first load, check the unreads
|
// On the first load, check the unreads
|
||||||
let auth = myAuth(false);
|
this.requestNotificationPermission();
|
||||||
if (auth && UserService.Instance.myUserInfo) {
|
await this.fetchUnreads();
|
||||||
this.requestNotificationPermission();
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.userJoin({
|
|
||||||
auth,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.fetchUnreads();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestNotificationPermission();
|
this.requestNotificationPermission();
|
||||||
|
|
||||||
// Subscribe to unread count changes
|
document.addEventListener("mouseup", this.handleOutsideMenuClick);
|
||||||
this.unreadInboxCountSub =
|
|
||||||
UserService.Instance.unreadInboxCountSub.subscribe(res => {
|
|
||||||
this.setState({ unreadInboxCount: res });
|
|
||||||
});
|
|
||||||
// Subscribe to unread report count changes
|
|
||||||
this.unreadReportCountSub =
|
|
||||||
UserService.Instance.unreadReportCountSub.subscribe(res => {
|
|
||||||
this.setState({ unreadReportCount: res });
|
|
||||||
});
|
|
||||||
// Subscribe to unread application count
|
|
||||||
this.unreadApplicationCountSub =
|
|
||||||
UserService.Instance.unreadApplicationCountSub.subscribe(res => {
|
|
||||||
this.setState({ unreadApplicationCount: res });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.wsSub.unsubscribe();
|
document.removeEventListener("mouseup", this.handleOutsideMenuClick);
|
||||||
this.userSub.unsubscribe();
|
}
|
||||||
this.unreadInboxCountSub.unsubscribe();
|
|
||||||
this.unreadReportCountSub.unsubscribe();
|
render() {
|
||||||
this.unreadApplicationCountSub.unsubscribe();
|
return this.navbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
// TODO class active corresponding to current page
|
||||||
render() {
|
navbar() {
|
||||||
const siteView = this.props.siteRes?.site_view;
|
const siteView = this.props.siteRes?.site_view;
|
||||||
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
||||||
return (
|
return (
|
||||||
|
@ -144,15 +103,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
to="/inbox"
|
to="/inbox"
|
||||||
className="p-1 nav-link border-0"
|
className="p-1 nav-link border-0"
|
||||||
title={i18n.t("unread_messages", {
|
title={i18n.t("unread_messages", {
|
||||||
count: Number(this.state.unreadInboxCount),
|
count: Number(this.state.unreadApplicationCountRes.state),
|
||||||
formattedCount: numToSI(this.state.unreadInboxCount),
|
formattedCount: numToSI(this.unreadInboxCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="bell" />
|
<Icon icon="bell" />
|
||||||
{this.state.unreadInboxCount > 0 && (
|
{this.unreadInboxCount > 0 && (
|
||||||
<span className="mx-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadInboxCount)}
|
{numToSI(this.unreadInboxCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -163,15 +122,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
to="/reports"
|
to="/reports"
|
||||||
className="p-1 nav-link border-0"
|
className="p-1 nav-link border-0"
|
||||||
title={i18n.t("unread_reports", {
|
title={i18n.t("unread_reports", {
|
||||||
count: Number(this.state.unreadReportCount),
|
count: Number(this.unreadReportCount),
|
||||||
formattedCount: numToSI(this.state.unreadReportCount),
|
formattedCount: numToSI(this.unreadReportCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="shield" />
|
<Icon icon="shield" />
|
||||||
{this.state.unreadReportCount > 0 && (
|
{this.unreadReportCount > 0 && (
|
||||||
<span className="mx-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadReportCount)}
|
{numToSI(this.unreadReportCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -183,15 +142,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
to="/registration_applications"
|
to="/registration_applications"
|
||||||
className="p-1 nav-link border-0"
|
className="p-1 nav-link border-0"
|
||||||
title={i18n.t("unread_registration_applications", {
|
title={i18n.t("unread_registration_applications", {
|
||||||
count: Number(this.state.unreadApplicationCount),
|
count: Number(this.unreadApplicationCount),
|
||||||
formattedCount: numToSI(this.state.unreadApplicationCount),
|
formattedCount: numToSI(this.unreadApplicationCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="clipboard" />
|
<Icon icon="clipboard" />
|
||||||
{this.state.unreadApplicationCount > 0 && (
|
{this.unreadApplicationCount > 0 && (
|
||||||
<span className="mx-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadApplicationCount)}
|
{numToSI(this.unreadApplicationCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -212,7 +171,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
>
|
>
|
||||||
<Icon icon="menu" />
|
<Icon icon="menu" />
|
||||||
</button>
|
</button>
|
||||||
<div className="collapse navbar-collapse my-2" id="navbarDropdown">
|
<div
|
||||||
|
className="collapse navbar-collapse my-2"
|
||||||
|
id="navbarDropdown"
|
||||||
|
ref={this.mobileMenuRef}
|
||||||
|
>
|
||||||
<ul className="mr-auto navbar-nav">
|
<ul className="mr-auto navbar-nav">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<NavLink
|
<NavLink
|
||||||
|
@ -264,20 +227,16 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="navbar-nav">
|
<ul className="navbar-nav">
|
||||||
{!this.context.router.history.location.pathname.match(
|
<li className="nav-item">
|
||||||
/^\/search/
|
<NavLink
|
||||||
) && (
|
to="/search"
|
||||||
<li className="nav-item">
|
className="nav-link"
|
||||||
<NavLink
|
title={i18n.t("search")}
|
||||||
to="/search"
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
className="nav-link"
|
>
|
||||||
title={i18n.t("search")}
|
<Icon icon="search" />
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
</NavLink>
|
||||||
>
|
</li>
|
||||||
<Icon icon="search" />
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{amAdmin() && (
|
{amAdmin() && (
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<NavLink
|
<NavLink
|
||||||
|
@ -297,15 +256,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
to="/inbox"
|
to="/inbox"
|
||||||
title={i18n.t("unread_messages", {
|
title={i18n.t("unread_messages", {
|
||||||
count: Number(this.state.unreadInboxCount),
|
count: Number(this.unreadInboxCount),
|
||||||
formattedCount: numToSI(this.state.unreadInboxCount),
|
formattedCount: numToSI(this.unreadInboxCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="bell" />
|
<Icon icon="bell" />
|
||||||
{this.state.unreadInboxCount > 0 && (
|
{this.unreadInboxCount > 0 && (
|
||||||
<span className="ml-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadInboxCount)}
|
{numToSI(this.unreadInboxCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -316,15 +275,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
to="/reports"
|
to="/reports"
|
||||||
title={i18n.t("unread_reports", {
|
title={i18n.t("unread_reports", {
|
||||||
count: Number(this.state.unreadReportCount),
|
count: Number(this.unreadReportCount),
|
||||||
formattedCount: numToSI(this.state.unreadReportCount),
|
formattedCount: numToSI(this.unreadReportCount),
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="shield" />
|
<Icon icon="shield" />
|
||||||
{this.state.unreadReportCount > 0 && (
|
{this.unreadReportCount > 0 && (
|
||||||
<span className="ml-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadReportCount)}
|
{numToSI(this.unreadReportCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -336,17 +295,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
to="/registration_applications"
|
to="/registration_applications"
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
title={i18n.t("unread_registration_applications", {
|
title={i18n.t("unread_registration_applications", {
|
||||||
count: Number(this.state.unreadApplicationCount),
|
count: Number(this.unreadApplicationCount),
|
||||||
formattedCount: numToSI(
|
formattedCount: numToSI(this.unreadApplicationCount),
|
||||||
this.state.unreadApplicationCount
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||||
>
|
>
|
||||||
<Icon icon="clipboard" />
|
<Icon icon="clipboard" />
|
||||||
{this.state.unreadApplicationCount > 0 && (
|
{this.unreadApplicationCount > 0 && (
|
||||||
<span className="mx-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
{numToSI(this.state.unreadApplicationCount)}
|
{numToSI(this.unreadApplicationCount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -397,7 +354,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item btn btn-link px-2"
|
className="dropdown-item btn btn-link px-2"
|
||||||
onClick={handleLogOut}
|
onClick={linkEvent(this, handleLogOut)}
|
||||||
>
|
>
|
||||||
<Icon icon="log-out" classes="mr-1" />
|
<Icon icon="log-out" classes="mr-1" />
|
||||||
{i18n.t("logout")}
|
{i18n.t("logout")}
|
||||||
|
@ -437,107 +394,77 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOutsideMenuClick(event: MouseEvent) {
|
||||||
|
if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
|
||||||
|
handleCollapseClick(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get moderatesSomething(): boolean {
|
get moderatesSomething(): boolean {
|
||||||
let mods = UserService.Instance.myUserInfo?.moderates;
|
const mods = UserService.Instance.myUserInfo?.moderates;
|
||||||
let moderatesS = (mods && mods.length > 0) || false;
|
const moderatesS = (mods && mods.length > 0) || false;
|
||||||
return amAdmin() || moderatesS;
|
return amAdmin() || moderatesS;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
async fetchUnreads() {
|
||||||
let op = wsUserOp(msg);
|
const auth = myAuth();
|
||||||
console.log(msg);
|
if (auth) {
|
||||||
if (msg.error) {
|
this.setState({ unreadInboxCountRes: { state: "loading" } });
|
||||||
if (msg.error == "not_logged_in") {
|
|
||||||
UserService.Instance.logout();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (msg.reconnect) {
|
|
||||||
console.log(i18n.t("websocket_reconnected"));
|
|
||||||
let auth = myAuth(false);
|
|
||||||
if (UserService.Instance.myUserInfo && auth) {
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.userJoin({
|
|
||||||
auth,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.fetchUnreads();
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.GetUnreadCount) {
|
|
||||||
let data = wsJsonToRes<GetUnreadCountResponse>(msg);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
unreadInboxCount: data.replies + data.mentions + data.private_messages,
|
unreadInboxCountRes: await HttpService.client.getUnreadCount({
|
||||||
|
auth,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
this.sendUnreadCount();
|
|
||||||
} else if (op == UserOperation.GetReportCount) {
|
|
||||||
let data = wsJsonToRes<GetReportCountResponse>(msg);
|
|
||||||
this.setState({
|
|
||||||
unreadReportCount:
|
|
||||||
data.post_reports +
|
|
||||||
data.comment_reports +
|
|
||||||
(data.private_message_reports ?? 0),
|
|
||||||
});
|
|
||||||
this.sendReportUnread();
|
|
||||||
} else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
|
|
||||||
let data =
|
|
||||||
wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
|
|
||||||
this.setState({ unreadApplicationCount: data.registration_applications });
|
|
||||||
this.sendApplicationUnread();
|
|
||||||
} else if (op == UserOperation.CreateComment) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (
|
|
||||||
mui &&
|
|
||||||
data.recipient_ids.includes(mui.local_user_view.local_user.id)
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
unreadInboxCount: this.state.unreadInboxCount + 1,
|
|
||||||
});
|
|
||||||
this.sendUnreadCount();
|
|
||||||
notifyComment(data.comment_view, this.context.router);
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.CreatePrivateMessage) {
|
|
||||||
let data = wsJsonToRes<PrivateMessageResponse>(msg);
|
|
||||||
|
|
||||||
if (
|
if (this.moderatesSomething) {
|
||||||
data.private_message_view.recipient.id ==
|
this.setState({ unreadReportCountRes: { state: "loading" } });
|
||||||
UserService.Instance.myUserInfo?.local_user_view.person.id
|
|
||||||
) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
unreadInboxCount: this.state.unreadInboxCount + 1,
|
unreadReportCountRes: await HttpService.client.getReportCount({
|
||||||
|
auth,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amAdmin()) {
|
||||||
|
this.setState({ unreadApplicationCountRes: { state: "loading" } });
|
||||||
|
this.setState({
|
||||||
|
unreadApplicationCountRes:
|
||||||
|
await HttpService.client.getUnreadRegistrationApplicationCount({
|
||||||
|
auth,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
this.sendUnreadCount();
|
|
||||||
notifyPrivateMessage(data.private_message_view, this.context.router);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUnreads() {
|
get unreadInboxCount(): number {
|
||||||
console.log("Fetching inbox unreads...");
|
if (this.state.unreadInboxCountRes.state == "success") {
|
||||||
|
const data = this.state.unreadInboxCountRes.data;
|
||||||
|
return data.replies + data.mentions + data.private_messages;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let auth = myAuth();
|
get unreadReportCount(): number {
|
||||||
if (auth) {
|
if (this.state.unreadReportCountRes.state == "success") {
|
||||||
let unreadForm: GetUnreadCount = {
|
const data = this.state.unreadReportCountRes.data;
|
||||||
auth,
|
return (
|
||||||
};
|
data.post_reports +
|
||||||
WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
|
data.comment_reports +
|
||||||
|
(data.private_message_reports ?? 0)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Fetching reports...");
|
get unreadApplicationCount(): number {
|
||||||
|
if (this.state.unreadApplicationCountRes.state == "success") {
|
||||||
let reportCountForm: GetReportCount = {
|
const data = this.state.unreadApplicationCountRes.data;
|
||||||
auth,
|
return data.registration_applications;
|
||||||
};
|
} else {
|
||||||
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
|
return 0;
|
||||||
|
|
||||||
if (amAdmin()) {
|
|
||||||
console.log("Fetching applications...");
|
|
||||||
|
|
||||||
let applicationCountForm: GetUnreadRegistrationApplicationCount = {
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,22 +472,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
return this.context.router.history.location.pathname;
|
return this.context.router.history.location.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
|
||||||
UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReportUnread() {
|
|
||||||
UserService.Instance.unreadReportCountSub.next(
|
|
||||||
this.state.unreadReportCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendApplicationUnread() {
|
|
||||||
UserService.Instance.unreadApplicationCountSub.next(
|
|
||||||
this.state.unreadApplicationCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestNotificationPermission() {
|
requestNotificationPermission() {
|
||||||
if (UserService.Instance.myUserInfo) {
|
if (UserService.Instance.myUserInfo) {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
|
@ -8,8 +8,8 @@ interface Props {
|
||||||
|
|
||||||
export class Theme extends Component<Props> {
|
export class Theme extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
let user = UserService.Instance.myUserInfo;
|
const user = UserService.Instance.myUserInfo;
|
||||||
let hasTheme = user?.local_user_view.local_user.theme !== "browser";
|
const hasTheme = user?.local_user_view.local_user.theme !== "browser";
|
||||||
|
|
||||||
if (user && hasTheme) {
|
if (user && hasTheme) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import { Link } from "inferno-router";
|
import { Link } from "inferno-router";
|
||||||
import {
|
import { CreateComment, EditComment, Language } from "lemmy-js-client";
|
||||||
CommentResponse,
|
|
||||||
CreateComment,
|
|
||||||
EditComment,
|
|
||||||
Language,
|
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { CommentNodeI } from "shared/interfaces";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { CommentNodeI } from "../../interfaces";
|
||||||
import {
|
import { UserService } from "../../services";
|
||||||
capitalizeFirstLetter,
|
import { capitalizeFirstLetter, myAuthRequired } from "../../utils";
|
||||||
myAuth,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
|
||||||
|
@ -28,48 +14,25 @@ interface CommentFormProps {
|
||||||
* Can either be the parent, or the editable comment. The right side is a postId.
|
* Can either be the parent, or the editable comment. The right side is a postId.
|
||||||
*/
|
*/
|
||||||
node: CommentNodeI | number;
|
node: CommentNodeI | number;
|
||||||
|
finished?: boolean;
|
||||||
edit?: boolean;
|
edit?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onReplyCancel?(): any;
|
onReplyCancel?(): void;
|
||||||
allLanguages: Language[];
|
allLanguages: Language[];
|
||||||
siteLanguages: number[];
|
siteLanguages: number[];
|
||||||
|
onUpsertComment(form: EditComment | CreateComment): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommentFormState {
|
export class CommentForm extends Component<CommentFormProps, any> {
|
||||||
buttonTitle: string;
|
|
||||||
finished: boolean;
|
|
||||||
formId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|
||||||
private subscription?: Subscription;
|
|
||||||
state: CommentFormState = {
|
|
||||||
buttonTitle:
|
|
||||||
typeof this.props.node === "number"
|
|
||||||
? capitalizeFirstLetter(i18n.t("post"))
|
|
||||||
: this.props.edit
|
|
||||||
? capitalizeFirstLetter(i18n.t("save"))
|
|
||||||
: capitalizeFirstLetter(i18n.t("reply")),
|
|
||||||
finished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
|
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
|
||||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let initialContent =
|
const initialContent =
|
||||||
typeof this.props.node !== "number"
|
typeof this.props.node !== "number"
|
||||||
? this.props.edit
|
? this.props.edit
|
||||||
? this.props.node.comment_view.comment.content
|
? this.props.node.comment_view.comment.content
|
||||||
|
@ -82,13 +45,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
<MarkdownTextArea
|
<MarkdownTextArea
|
||||||
initialContent={initialContent}
|
initialContent={initialContent}
|
||||||
showLanguage
|
showLanguage
|
||||||
buttonTitle={this.state.buttonTitle}
|
buttonTitle={this.buttonTitle}
|
||||||
finished={this.state.finished}
|
finished={this.props.finished}
|
||||||
replyType={typeof this.props.node !== "number"}
|
replyType={typeof this.props.node !== "number"}
|
||||||
focus={this.props.focus}
|
focus={this.props.focus}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
onSubmit={this.handleCommentSubmit}
|
onSubmit={this.handleCommentSubmit}
|
||||||
onReplyCancel={this.handleReplyCancel}
|
onReplyCancel={this.props.onReplyCancel}
|
||||||
placeholder={i18n.t("comment_here")}
|
placeholder={i18n.t("comment_here")}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
@ -108,77 +71,46 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommentSubmit(msg: {
|
get buttonTitle(): string {
|
||||||
val: string;
|
return typeof this.props.node === "number"
|
||||||
formId: string;
|
? capitalizeFirstLetter(i18n.t("post"))
|
||||||
languageId?: number;
|
: this.props.edit
|
||||||
}) {
|
? capitalizeFirstLetter(i18n.t("save"))
|
||||||
let content = msg.val;
|
: capitalizeFirstLetter(i18n.t("reply"));
|
||||||
let language_id = msg.languageId;
|
}
|
||||||
let node = this.props.node;
|
|
||||||
|
|
||||||
this.setState({ formId: msg.formId });
|
handleCommentSubmit(content: string, form_id: string, language_id?: number) {
|
||||||
|
const { node, onUpsertComment, edit } = this.props;
|
||||||
let auth = myAuth();
|
if (typeof node === "number") {
|
||||||
if (auth) {
|
const post_id = node;
|
||||||
if (typeof node === "number") {
|
onUpsertComment({
|
||||||
let postId = node;
|
content,
|
||||||
let form: CreateComment = {
|
post_id,
|
||||||
|
language_id,
|
||||||
|
form_id,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (edit) {
|
||||||
|
const comment_id = node.comment_view.comment.id;
|
||||||
|
onUpsertComment({
|
||||||
content,
|
content,
|
||||||
form_id: this.state.formId,
|
comment_id,
|
||||||
post_id: postId,
|
form_id,
|
||||||
language_id,
|
language_id,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.createComment(form));
|
|
||||||
} else {
|
} else {
|
||||||
if (this.props.edit) {
|
const post_id = node.comment_view.post.id;
|
||||||
let form: EditComment = {
|
const parent_id = node.comment_view.comment.id;
|
||||||
content,
|
this.props.onUpsertComment({
|
||||||
form_id: this.state.formId,
|
content,
|
||||||
comment_id: node.comment_view.comment.id,
|
parent_id,
|
||||||
language_id,
|
post_id,
|
||||||
auth,
|
form_id,
|
||||||
};
|
language_id,
|
||||||
WebSocketService.Instance.send(wsClient.editComment(form));
|
auth: myAuthRequired(),
|
||||||
} else {
|
});
|
||||||
let form: CreateComment = {
|
|
||||||
content,
|
|
||||||
form_id: this.state.formId,
|
|
||||||
post_id: node.comment_view.post.id,
|
|
||||||
parent_id: node.comment_view.comment.id,
|
|
||||||
language_id,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.createComment(form));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReplyCancel() {
|
|
||||||
this.props.onReplyCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
|
|
||||||
// Only do the showing and hiding if logged in
|
|
||||||
if (UserService.Instance.myUserInfo) {
|
|
||||||
if (
|
|
||||||
op == UserOperation.CreateComment ||
|
|
||||||
op == UserOperation.EditComment
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
|
|
||||||
// This only finishes this form, if the randomly generated form_id matches the one received
|
|
||||||
if (this.state.formId && this.state.formId == data.form_id) {
|
|
||||||
this.setState({ finished: true });
|
|
||||||
|
|
||||||
// Necessary because it broke tribute for some reason
|
|
||||||
this.setState({ finished: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,30 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { CommunityModeratorView, Language, PersonView } from "lemmy-js-client";
|
import {
|
||||||
|
AddAdmin,
|
||||||
|
AddModToCommunity,
|
||||||
|
BanFromCommunity,
|
||||||
|
BanPerson,
|
||||||
|
BlockPerson,
|
||||||
|
CommentId,
|
||||||
|
CommunityModeratorView,
|
||||||
|
CreateComment,
|
||||||
|
CreateCommentLike,
|
||||||
|
CreateCommentReport,
|
||||||
|
DeleteComment,
|
||||||
|
DistinguishComment,
|
||||||
|
EditComment,
|
||||||
|
GetComments,
|
||||||
|
Language,
|
||||||
|
MarkCommentReplyAsRead,
|
||||||
|
MarkPersonMentionAsRead,
|
||||||
|
PersonView,
|
||||||
|
PurgeComment,
|
||||||
|
PurgePerson,
|
||||||
|
RemoveComment,
|
||||||
|
SaveComment,
|
||||||
|
TransferCommunity,
|
||||||
|
} from "lemmy-js-client";
|
||||||
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
||||||
import { colorList } from "../../utils";
|
import { colorList } from "../../utils";
|
||||||
import { CommentNode } from "./comment-node";
|
import { CommentNode } from "./comment-node";
|
||||||
|
@ -24,6 +48,26 @@ interface CommentNodesProps {
|
||||||
hideImages?: boolean;
|
hideImages?: boolean;
|
||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
finished: Map<CommentId, boolean | undefined>;
|
||||||
|
onSaveComment(form: SaveComment): void;
|
||||||
|
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
|
||||||
|
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
|
||||||
|
onCreateComment(form: EditComment | CreateComment): void;
|
||||||
|
onEditComment(form: EditComment | CreateComment): void;
|
||||||
|
onCommentVote(form: CreateCommentLike): void;
|
||||||
|
onBlockPerson(form: BlockPerson): void;
|
||||||
|
onDeleteComment(form: DeleteComment): void;
|
||||||
|
onRemoveComment(form: RemoveComment): void;
|
||||||
|
onDistinguishComment(form: DistinguishComment): void;
|
||||||
|
onAddModToCommunity(form: AddModToCommunity): void;
|
||||||
|
onAddAdmin(form: AddAdmin): void;
|
||||||
|
onBanPersonFromCommunity(form: BanFromCommunity): void;
|
||||||
|
onBanPerson(form: BanPerson): void;
|
||||||
|
onTransferCommunity(form: TransferCommunity): void;
|
||||||
|
onFetchChildren?(form: GetComments): void;
|
||||||
|
onCommentReport(form: CreateCommentReport): void;
|
||||||
|
onPurgePerson(form: PurgePerson): void;
|
||||||
|
onPurgeComment(form: PurgeComment): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<CommentNodesProps, any> {
|
export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||||
|
@ -64,6 +108,26 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
hideImages={this.props.hideImages}
|
hideImages={this.props.hideImages}
|
||||||
|
onCommentReplyRead={this.props.onCommentReplyRead}
|
||||||
|
onPersonMentionRead={this.props.onPersonMentionRead}
|
||||||
|
finished={this.props.finished}
|
||||||
|
onCreateComment={this.props.onCreateComment}
|
||||||
|
onEditComment={this.props.onEditComment}
|
||||||
|
onCommentVote={this.props.onCommentVote}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onSaveComment={this.props.onSaveComment}
|
||||||
|
onDeleteComment={this.props.onDeleteComment}
|
||||||
|
onRemoveComment={this.props.onRemoveComment}
|
||||||
|
onDistinguishComment={this.props.onDistinguishComment}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
|
onFetchChildren={this.props.onFetchChildren}
|
||||||
|
onCommentReport={this.props.onCommentReport}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgeComment={this.props.onPurgeComment}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import {
|
import {
|
||||||
CommentReportView,
|
CommentReportView,
|
||||||
|
@ -7,32 +7,50 @@ import {
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
||||||
import { WebSocketService } from "../../services";
|
import { myAuthRequired } from "../../utils";
|
||||||
import { myAuth, wsClient } from "../../utils";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { Icon } from "../common/icon";
|
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { CommentNode } from "./comment-node";
|
import { CommentNode } from "./comment-node";
|
||||||
|
|
||||||
interface CommentReportProps {
|
interface CommentReportProps {
|
||||||
report: CommentReportView;
|
report: CommentReportView;
|
||||||
|
onResolveReport(form: ResolveCommentReport): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentReport extends Component<CommentReportProps, any> {
|
interface CommentReportState {
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommentReport extends Component<
|
||||||
|
CommentReportProps,
|
||||||
|
CommentReportState
|
||||||
|
> {
|
||||||
|
state: CommentReportState = {
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>
|
||||||
|
): void {
|
||||||
|
if (this.props != nextProps) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let r = this.props.report;
|
const r = this.props.report;
|
||||||
let comment = r.comment;
|
const comment = r.comment;
|
||||||
let tippyContent = i18n.t(
|
const tippyContent = i18n.t(
|
||||||
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
|
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the original post data ( a troll could change it )
|
// Set the original post data ( a troll could change it )
|
||||||
comment.content = r.comment_report.original_comment_text;
|
comment.content = r.comment_report.original_comment_text;
|
||||||
|
|
||||||
let comment_view: CommentView = {
|
const comment_view: CommentView = {
|
||||||
comment,
|
comment,
|
||||||
creator: r.comment_creator,
|
creator: r.comment_creator,
|
||||||
post: r.post,
|
post: r.post,
|
||||||
|
@ -45,7 +63,7 @@ export class CommentReport extends Component<CommentReportProps, any> {
|
||||||
my_vote: r.my_vote,
|
my_vote: r.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
let node: CommentNodeI = {
|
const node: CommentNodeI = {
|
||||||
comment_view,
|
comment_view,
|
||||||
children: [],
|
children: [],
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
@ -62,6 +80,26 @@ export class CommentReport extends Component<CommentReportProps, any> {
|
||||||
allLanguages={[]}
|
allLanguages={[]}
|
||||||
siteLanguages={[]}
|
siteLanguages={[]}
|
||||||
hideImages
|
hideImages
|
||||||
|
// All of these are unused, since its viewonly
|
||||||
|
finished={new Map()}
|
||||||
|
onSaveComment={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onDeleteComment={() => {}}
|
||||||
|
onRemoveComment={() => {}}
|
||||||
|
onCommentVote={() => {}}
|
||||||
|
onCommentReport={() => {}}
|
||||||
|
onDistinguishComment={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
|
onPurgeComment={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onCommentReplyRead={() => {}}
|
||||||
|
onPersonMentionRead={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onCreateComment={() => Promise.resolve({ state: "empty" })}
|
||||||
|
onEditComment={() => Promise.resolve({ state: "empty" })}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{i18n.t("reporter")}: <PersonListing person={r.creator} />
|
{i18n.t("reporter")}: <PersonListing person={r.creator} />
|
||||||
|
@ -90,26 +128,27 @@ export class CommentReport extends Component<CommentReportProps, any> {
|
||||||
data-tippy-content={tippyContent}
|
data-tippy-content={tippyContent}
|
||||||
aria-label={tippyContent}
|
aria-label={tippyContent}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.loading ? (
|
||||||
icon="check"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
r.comment_report.resolved ? "text-success" : "text-danger"
|
<Icon
|
||||||
}`}
|
icon="check"
|
||||||
/>
|
classes={`icon-inline ${
|
||||||
|
r.comment_report.resolved ? "text-success" : "text-danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResolveReport(i: CommentReport) {
|
handleResolveReport(i: CommentReport) {
|
||||||
let auth = myAuth();
|
i.setState({ loading: true });
|
||||||
if (auth) {
|
i.props.onResolveReport({
|
||||||
let form: ResolveCommentReport = {
|
report_id: i.props.report.comment_report.id,
|
||||||
report_id: i.props.report.comment_report.id,
|
resolved: !i.props.report.comment_report.resolved,
|
||||||
resolved: !i.props.report.comment_report.resolved,
|
auth: myAuthRequired(),
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let banner = this.props.banner;
|
const banner = this.props.banner;
|
||||||
let icon = this.props.icon;
|
const icon = this.props.icon;
|
||||||
return (
|
return (
|
||||||
<div className="position-relative mb-2">
|
<div className="position-relative mb-2">
|
||||||
{banner && <PictrsImage src={banner} banner alt="" />}
|
{banner && <PictrsImage src={banner} banner alt="" />}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export class EmojiMart extends Component<EmojiMartProps> {
|
||||||
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let div: any = document.getElementById("emoji-picker");
|
const div: any = document.getElementById("emoji-picker");
|
||||||
if (div) {
|
if (div) {
|
||||||
div.appendChild(
|
div.appendChild(
|
||||||
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)
|
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { htmlToText } from "html-to-text";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { Helmet } from "inferno-helmet";
|
import { Helmet } from "inferno-helmet";
|
||||||
import { httpExternalPath } from "../../env";
|
import { httpExternalPath } from "../../env";
|
||||||
import { md } from "../../utils";
|
import { getLanguages, md } from "../../utils";
|
||||||
|
|
||||||
interface HtmlTagsProps {
|
interface HtmlTagsProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -14,12 +14,15 @@ interface HtmlTagsProps {
|
||||||
/// Taken from https://metatags.io/
|
/// Taken from https://metatags.io/
|
||||||
export class HtmlTags extends Component<HtmlTagsProps, any> {
|
export class HtmlTags extends Component<HtmlTagsProps, any> {
|
||||||
render() {
|
render() {
|
||||||
let url = httpExternalPath(this.props.path);
|
const url = httpExternalPath(this.props.path);
|
||||||
let desc = this.props.description;
|
const desc = this.props.description;
|
||||||
let image = this.props.image;
|
const image = this.props.image;
|
||||||
|
const lang = getLanguages()[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet title={this.props.title}>
|
<Helmet title={this.props.title}>
|
||||||
|
<html lang={lang == "browser" ? "en" : lang} />
|
||||||
|
|
||||||
{["title", "og:title", "twitter:title"].map(t => (
|
{["title", "og:title", "twitter:title"].map(t => (
|
||||||
<meta key={t} property={t} content={this.props.title} />
|
<meta key={t} property={t} content={this.props.title} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService } from "../../services";
|
import { HttpService, UserService } from "../../services";
|
||||||
import { randomStr, toast, uploadImage } from "../../utils";
|
import { randomStr, toast } from "../../utils";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
interface ImageUploadFormProps {
|
interface ImageUploadFormProps {
|
||||||
|
@ -73,27 +73,26 @@ export class ImageUploadForm extends Component<
|
||||||
|
|
||||||
handleImageUpload(i: ImageUploadForm, event: any) {
|
handleImageUpload(i: ImageUploadForm, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.target.files[0];
|
const image = event.target.files[0] as File;
|
||||||
|
|
||||||
i.setState({ loading: true });
|
i.setState({ loading: true });
|
||||||
|
|
||||||
uploadImage(file)
|
HttpService.client.uploadImage({ image }).then(res => {
|
||||||
.then(res => {
|
console.log("pictrs upload:");
|
||||||
console.log("pictrs upload:");
|
console.log(res);
|
||||||
console.log(res);
|
if (res.state === "success") {
|
||||||
if (res.msg === "ok") {
|
if (res.data.msg === "ok") {
|
||||||
i.setState({ loading: false });
|
i.props.onUpload(res.data.url as string);
|
||||||
i.props.onUpload(res.url as string);
|
|
||||||
} else {
|
} else {
|
||||||
i.setState({ loading: false });
|
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
})
|
} else if (res.state === "failed") {
|
||||||
.catch(error => {
|
console.error(res.msg);
|
||||||
i.setState({ loading: false });
|
toast(res.msg, "danger");
|
||||||
console.error(error);
|
}
|
||||||
toast(error, "danger");
|
|
||||||
});
|
i.setState({ loading: false });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveImage(i: ImageUploadForm, event: any) {
|
handleRemoveImage(i: ImageUploadForm, event: any) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ interface LanguageSelectProps {
|
||||||
showSite?: boolean;
|
showSite?: boolean;
|
||||||
iconVersion?: boolean;
|
iconVersion?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
showLanguageWarning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
|
@ -31,12 +32,12 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
|
|
||||||
// Necessary because there is no HTML way to set selected for multiple in value=
|
// Necessary because there is no HTML way to set selected for multiple in value=
|
||||||
setSelectedValues() {
|
setSelectedValues() {
|
||||||
let ids = this.props.selectedLanguageIds?.map(toString);
|
const ids = this.props.selectedLanguageIds?.map(toString);
|
||||||
if (ids) {
|
if (ids) {
|
||||||
let select = (document.getElementById(this.id) as HTMLSelectElement)
|
const select = (document.getElementById(this.id) as HTMLSelectElement)
|
||||||
.options;
|
.options;
|
||||||
for (let i = 0; i < select.length; i++) {
|
for (let i = 0; i < select.length; i++) {
|
||||||
let o = select[i];
|
const o = select[i];
|
||||||
if (ids.includes(o.value)) {
|
if (ids.includes(o.value)) {
|
||||||
o.selected = true;
|
o.selected = true;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
this.selectBtn
|
this.selectBtn
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{this.props.multiple && (
|
{this.props.multiple && this.props.showLanguageWarning && (
|
||||||
<div className="alert alert-warning" role="alert">
|
<div className="alert alert-warning" role="alert">
|
||||||
{i18n.t("undetermined_language_warning")}
|
{i18n.t("undetermined_language_warning")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +108,7 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
)}
|
)}
|
||||||
id={this.id}
|
id={this.id}
|
||||||
onChange={linkEvent(this, this.handleLanguageChange)}
|
onChange={linkEvent(this, this.handleLanguageChange)}
|
||||||
aria-label="action"
|
aria-label={i18n.t("language_select_placeholder")}
|
||||||
multiple={this.props.multiple}
|
multiple={this.props.multiple}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
|
@ -130,8 +131,8 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLanguageChange(i: LanguageSelect, event: any) {
|
handleLanguageChange(i: LanguageSelect, event: any) {
|
||||||
let options: HTMLOptionElement[] = Array.from(event.target.options);
|
const options: HTMLOptionElement[] = Array.from(event.target.options);
|
||||||
let selected: number[] = options
|
const selected: number[] = options
|
||||||
.filter(o => o.selected)
|
.filter(o => o.selected)
|
||||||
.map(o => Number(o.value));
|
.map(o => Number(o.value));
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface ListingTypeSelectProps {
|
||||||
type_: ListingType;
|
type_: ListingType;
|
||||||
showLocal: boolean;
|
showLocal: boolean;
|
||||||
showSubscribed: boolean;
|
showSubscribed: boolean;
|
||||||
onChange?(val: ListingType): any;
|
onChange(val: ListingType): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListingTypeSelectState {
|
interface ListingTypeSelectState {
|
||||||
|
@ -29,11 +29,11 @@ export class ListingTypeSelect extends Component<
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
|
static getDerivedStateFromProps(
|
||||||
|
props: ListingTypeSelectProps
|
||||||
|
): ListingTypeSelectState {
|
||||||
return {
|
return {
|
||||||
type_: props.type_,
|
type_: props.type_,
|
||||||
showLocal: props.showLocal,
|
|
||||||
showSubscribed: props.showSubscribed,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +97,6 @@ export class ListingTypeSelect extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(i: ListingTypeSelect, event: any) {
|
handleTypeChange(i: ListingTypeSelect, event: any) {
|
||||||
i.props.onChange?.(event.target.value);
|
i.props.onChange(event.target.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
import { NoOptionI18nKeys } from "i18next";
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { Prompt } from "inferno-router";
|
|
||||||
import { Language } from "lemmy-js-client";
|
import { Language } from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService } from "../../services";
|
import { HttpService, UserService } from "../../services";
|
||||||
import {
|
import {
|
||||||
concurrentImageUpload,
|
concurrentImageUpload,
|
||||||
customEmojisLookup,
|
customEmojisLookup,
|
||||||
|
@ -20,11 +19,11 @@ import {
|
||||||
setupTippy,
|
setupTippy,
|
||||||
setupTribute,
|
setupTribute,
|
||||||
toast,
|
toast,
|
||||||
uploadImage,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { EmojiPicker } from "./emoji-picker";
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
import { Icon, Spinner } from "./icon";
|
import { Icon, Spinner } from "./icon";
|
||||||
import { LanguageSelect } from "./language-select";
|
import { LanguageSelect } from "./language-select";
|
||||||
|
import NavigationPrompt from "./navigation-prompt";
|
||||||
import ProgressBar from "./progress-bar";
|
import ProgressBar from "./progress-bar";
|
||||||
|
|
||||||
interface MarkdownTextAreaProps {
|
interface MarkdownTextAreaProps {
|
||||||
|
@ -39,9 +38,9 @@ interface MarkdownTextAreaProps {
|
||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
showLanguage?: boolean;
|
showLanguage?: boolean;
|
||||||
hideNavigationWarnings?: boolean;
|
hideNavigationWarnings?: boolean;
|
||||||
onContentChange?(val: string): any;
|
onContentChange?(val: string): void;
|
||||||
onReplyCancel?(): any;
|
onReplyCancel?(): void;
|
||||||
onSubmit?(msg: { val?: string; formId: string; languageId?: number }): any;
|
onSubmit?(content: string, formId: string, languageId?: number): void;
|
||||||
allLanguages: Language[]; // TODO should probably be nullable
|
allLanguages: Language[]; // TODO should probably be nullable
|
||||||
siteLanguages: number[]; // TODO same
|
siteLanguages: number[]; // TODO same
|
||||||
}
|
}
|
||||||
|
@ -55,8 +54,9 @@ interface MarkdownTextAreaState {
|
||||||
content?: string;
|
content?: string;
|
||||||
languageId?: number;
|
languageId?: number;
|
||||||
previewMode: boolean;
|
previewMode: boolean;
|
||||||
loading: boolean;
|
|
||||||
imageUploadStatus?: ImageUploadStatus;
|
imageUploadStatus?: ImageUploadStatus;
|
||||||
|
loading: boolean;
|
||||||
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MarkdownTextArea extends Component<
|
export class MarkdownTextArea extends Component<
|
||||||
|
@ -72,6 +72,7 @@ export class MarkdownTextArea extends Component<
|
||||||
languageId: this.props.initialLanguageId,
|
languageId: this.props.initialLanguageId,
|
||||||
previewMode: false,
|
previewMode: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
submitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -85,7 +86,7 @@ export class MarkdownTextArea extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let textarea: any = document.getElementById(this.id);
|
const textarea: any = document.getElementById(this.id);
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
autosize(textarea);
|
autosize(textarea);
|
||||||
this.tribute.attach(textarea);
|
this.tribute.attach(textarea);
|
||||||
|
@ -105,17 +106,14 @@ export class MarkdownTextArea extends Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (!this.props.hideNavigationWarnings && this.state.content) {
|
|
||||||
window.onbeforeunload = () => true;
|
|
||||||
} else {
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
|
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
|
||||||
if (nextProps.finished) {
|
if (nextProps.finished) {
|
||||||
this.setState({ previewMode: false, loading: false, content: undefined });
|
this.setState({
|
||||||
|
previewMode: false,
|
||||||
|
imageUploadStatus: undefined,
|
||||||
|
loading: false,
|
||||||
|
content: undefined,
|
||||||
|
});
|
||||||
if (this.props.replyType) {
|
if (this.props.replyType) {
|
||||||
this.props.onReplyCancel?.();
|
this.props.onReplyCancel?.();
|
||||||
}
|
}
|
||||||
|
@ -127,18 +125,22 @@ export class MarkdownTextArea extends Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let languageId = this.state.languageId;
|
const languageId = this.state.languageId;
|
||||||
|
|
||||||
|
// TODO add these prompts back in at some point
|
||||||
|
// <Prompt
|
||||||
|
// when={!this.props.hideNavigationWarnings && this.state.content}
|
||||||
|
// message={i18n.t("block_leaving")}
|
||||||
|
// />
|
||||||
return (
|
return (
|
||||||
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
|
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
|
||||||
<Prompt
|
<NavigationPrompt
|
||||||
when={!this.props.hideNavigationWarnings && this.state.content}
|
when={
|
||||||
message={i18n.t("block_leaving")}
|
!this.props.hideNavigationWarnings &&
|
||||||
|
!!this.state.content &&
|
||||||
|
!this.state.submitted
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className={`col-sm-12`}>
|
<div className={`col-sm-12`}>
|
||||||
|
@ -148,6 +150,7 @@ export class MarkdownTextArea extends Component<
|
||||||
value={this.state.content}
|
value={this.state.content}
|
||||||
onInput={linkEvent(this, this.handleContentChange)}
|
onInput={linkEvent(this, this.handleContentChange)}
|
||||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||||
|
onKeyDown={linkEvent(this, this.handleKeyBinds)}
|
||||||
required
|
required
|
||||||
disabled={this.isDisabled}
|
disabled={this.isDisabled}
|
||||||
rows={2}
|
rows={2}
|
||||||
|
@ -210,7 +213,7 @@ export class MarkdownTextArea extends Component<
|
||||||
}`}
|
}`}
|
||||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||||
>
|
>
|
||||||
{i18n.t("preview")}
|
{this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* A flex expander */}
|
{/* A flex expander */}
|
||||||
|
@ -321,7 +324,7 @@ export class MarkdownTextArea extends Component<
|
||||||
handleEmoji(i: MarkdownTextArea, e: any) {
|
handleEmoji(i: MarkdownTextArea, e: any) {
|
||||||
let value = e.native;
|
let value = e.native;
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
let emoji = customEmojisLookup.get(e.id)?.custom_emoji;
|
const emoji = customEmojisLookup.get(e.id)?.custom_emoji;
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`;
|
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`;
|
||||||
}
|
}
|
||||||
|
@ -330,7 +333,7 @@ export class MarkdownTextArea extends Component<
|
||||||
content: `${i.state.content ?? ""} ${value} `,
|
content: `${i.state.content ?? ""} ${value} `,
|
||||||
});
|
});
|
||||||
i.contentChange();
|
i.contentChange();
|
||||||
let textarea: any = document.getElementById(i.id);
|
const textarea: any = document.getElementById(i.id);
|
||||||
autosize.update(textarea);
|
autosize.update(textarea);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,35 +395,35 @@ export class MarkdownTextArea extends Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadSingleImage(i: MarkdownTextArea, file: File) {
|
async uploadSingleImage(i: MarkdownTextArea, image: File) {
|
||||||
try {
|
const res = await HttpService.client.uploadImage({ image });
|
||||||
const res = await uploadImage(file);
|
console.log("pictrs upload:");
|
||||||
console.log("pictrs upload:");
|
console.log(res);
|
||||||
console.log(res);
|
if (res.state === "success") {
|
||||||
if (res.msg === "ok") {
|
if (res.data.msg === "ok") {
|
||||||
const imageMarkdown = `![](${res.url})`;
|
const imageMarkdown = `![](${res.data.url})`;
|
||||||
i.setState(({ content }) => ({
|
i.setState(({ content }) => ({
|
||||||
content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
|
content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
|
||||||
}));
|
}));
|
||||||
i.contentChange();
|
i.contentChange();
|
||||||
const textarea: any = document.getElementById(i.id);
|
const textarea: any = document.getElementById(i.id);
|
||||||
autosize.update(textarea);
|
autosize.update(textarea);
|
||||||
pictrsDeleteToast(file.name, res.delete_url as string);
|
pictrsDeleteToast(image.name, res.data.delete_url as string);
|
||||||
} else {
|
} else {
|
||||||
throw JSON.stringify(res);
|
throw JSON.stringify(res.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else if (res.state === "failed") {
|
||||||
i.setState({ imageUploadStatus: undefined });
|
i.setState({ imageUploadStatus: undefined });
|
||||||
console.error(error);
|
console.error(res.msg);
|
||||||
toast(error, "danger");
|
toast(res.msg, "danger");
|
||||||
|
|
||||||
throw error;
|
throw res.msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentChange() {
|
contentChange() {
|
||||||
// Coerces the undefineds to empty strings, for replacing in the DB
|
// Coerces the undefineds to empty strings, for replacing in the DB
|
||||||
let content = this.state.content ?? "";
|
const content = this.state.content ?? "";
|
||||||
this.props.onContentChange?.(content);
|
this.props.onContentChange?.(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,6 +432,54 @@ export class MarkdownTextArea extends Component<
|
||||||
i.contentChange();
|
i.contentChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keybind handler
|
||||||
|
// Keybinds inspired by github comment area
|
||||||
|
handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "k": {
|
||||||
|
i.handleInsertLink(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Enter": {
|
||||||
|
if (!this.isDisabled) {
|
||||||
|
i.handleSubmit(i, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "b": {
|
||||||
|
i.handleInsertBold(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "i": {
|
||||||
|
i.handleInsertItalic(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "e": {
|
||||||
|
i.handleInsertCode(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "8": {
|
||||||
|
i.handleInsertList(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "s": {
|
||||||
|
i.handleInsertSpoiler(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "p": {
|
||||||
|
if (i.state.content) i.handlePreviewToggle(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ".": {
|
||||||
|
i.handleInsertQuote(i, event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handlePreviewToggle(i: MarkdownTextArea, event: any) {
|
handlePreviewToggle(i: MarkdownTextArea, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ previewMode: !i.state.previewMode });
|
i.setState({ previewMode: !i.state.previewMode });
|
||||||
|
@ -440,13 +491,10 @@ export class MarkdownTextArea extends Component<
|
||||||
|
|
||||||
handleSubmit(i: MarkdownTextArea, event: any) {
|
handleSubmit(i: MarkdownTextArea, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ loading: true });
|
if (i.state.content) {
|
||||||
let msg = {
|
i.setState({ loading: true, submitted: true });
|
||||||
val: i.state.content,
|
i.props.onSubmit?.(i.state.content, i.formId, i.state.languageId);
|
||||||
formId: i.formId,
|
}
|
||||||
languageId: i.state.languageId,
|
|
||||||
};
|
|
||||||
i.props.onSubmit?.(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyCancel(i: MarkdownTextArea) {
|
handleReplyCancel(i: MarkdownTextArea) {
|
||||||
|
@ -565,7 +613,7 @@ export class MarkdownTextArea extends Component<
|
||||||
|
|
||||||
handleInsertList(i: MarkdownTextArea, event: any) {
|
handleInsertList(i: MarkdownTextArea, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.simpleBeginningofLine("-");
|
i.simpleBeginningofLine(`-${i.getSelectedText() ? " " : ""}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInsertQuote(i: MarkdownTextArea, event: any) {
|
handleInsertQuote(i: MarkdownTextArea, event: any) {
|
||||||
|
@ -589,7 +637,7 @@ export class MarkdownTextArea extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
simpleInsert(chars: string) {
|
simpleInsert(chars: string) {
|
||||||
let content = this.state.content;
|
const content = this.state.content;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
this.setState({ content: `${chars} ` });
|
this.setState({ content: `${chars} ` });
|
||||||
} else {
|
} else {
|
||||||
|
@ -598,7 +646,7 @@ export class MarkdownTextArea extends Component<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let textarea: any = document.getElementById(this.id);
|
const textarea: any = document.getElementById(this.id);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
autosize.update(textarea);
|
autosize.update(textarea);
|
||||||
|
@ -608,8 +656,8 @@ export class MarkdownTextArea extends Component<
|
||||||
|
|
||||||
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
|
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let beforeChars = `\n::: spoiler ${i18n.t("spoiler")}\n`;
|
const beforeChars = `\n::: spoiler ${i18n.t("spoiler")}\n`;
|
||||||
let afterChars = "\n:::\n";
|
const afterChars = "\n:::\n";
|
||||||
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
|
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
let lang = getLanguages();
|
const lang = getLanguages();
|
||||||
|
|
||||||
moment.locale(lang);
|
moment.locale(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
createdAndModifiedTimes() {
|
createdAndModifiedTimes() {
|
||||||
let updated = this.props.updated;
|
const updated = this.props.updated;
|
||||||
let line = `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
|
let line = `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
|
||||||
this.props.published
|
this.props.published
|
||||||
)}`;
|
)}`;
|
||||||
|
@ -45,7 +45,7 @@ export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let published = this.props.published;
|
const published = this.props.published;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="pointer unselectable"
|
className="pointer unselectable"
|
||||||
|
|
53
src/shared/components/common/navigation-prompt.tsx
Normal file
53
src/shared/components/common/navigation-prompt.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Component } from "inferno";
|
||||||
|
import { i18n } from "../../../shared/i18next";
|
||||||
|
|
||||||
|
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(i18n.t("block_leaving"))) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={this.props.src}
|
||||||
alt={this.alt()}
|
alt={this.alt()}
|
||||||
|
title={this.alt()}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"img-fluid": !this.props.icon && !this.props.iconOverlay,
|
"img-fluid": !this.props.icon && !this.props.iconOverlay,
|
||||||
|
@ -38,8 +39,9 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
"img-expanded slight-radius":
|
"img-expanded slight-radius":
|
||||||
!this.props.thumbnail && !this.props.icon,
|
!this.props.thumbnail && !this.props.icon,
|
||||||
"img-blur": this.props.thumbnail && this.props.nsfw,
|
"img-blur": this.props.thumbnail && this.props.nsfw,
|
||||||
"rounded-circle img-icon mr-2": this.props.icon,
|
"rounded-circle img-cover img-icon mr-2": this.props.icon,
|
||||||
"ml-2 mb-0 rounded-circle avatar-overlay": this.props.iconOverlay,
|
"ml-2 mb-0 rounded-circle img-cover avatar-overlay":
|
||||||
|
this.props.iconOverlay,
|
||||||
"avatar-pushup": this.props.pushup,
|
"avatar-pushup": this.props.pushup,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -51,17 +53,17 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
// sample url:
|
// sample url:
|
||||||
// http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg
|
// http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg
|
||||||
|
|
||||||
let split = this.props.src.split("/pictrs/image/");
|
const split = this.props.src.split("/pictrs/image/");
|
||||||
|
|
||||||
// If theres not multiple, then its not a pictrs image
|
// If theres not multiple, then its not a pictrs image
|
||||||
if (split.length == 1) {
|
if (split.length == 1) {
|
||||||
return this.props.src;
|
return this.props.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
let host = split[0];
|
const host = split[0];
|
||||||
let path = split[1];
|
const path = split[1];
|
||||||
|
|
||||||
let params = { format };
|
const params = { format };
|
||||||
|
|
||||||
if (this.props.thumbnail) {
|
if (this.props.thumbnail) {
|
||||||
params["thumbnail"] = thumbnailSize;
|
params["thumbnail"] = thumbnailSize;
|
||||||
|
@ -69,8 +71,8 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
params["thumbnail"] = iconThumbnailSize;
|
params["thumbnail"] = iconThumbnailSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
let paramsStr = new URLSearchParams(params).toString();
|
const paramsStr = new URLSearchParams(params).toString();
|
||||||
let out = `${host}/pictrs/image/${path}?${paramsStr}`;
|
const out = `${host}/pictrs/image/${path}?${paramsStr}`;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ThemeColor } from "shared/utils";
|
import { ThemeColor } from "../../utils";
|
||||||
|
|
||||||
interface ProgressBarProps {
|
interface ProgressBarProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import {
|
import {
|
||||||
ApproveRegistrationApplication,
|
ApproveRegistrationApplication,
|
||||||
RegistrationApplicationView,
|
RegistrationApplicationView,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { mdToHtml, myAuthRequired } from "../../utils";
|
||||||
import { mdToHtml, myAuth, wsClient } from "../../utils";
|
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { Spinner } from "./icon";
|
||||||
import { MarkdownTextArea } from "./markdown-textarea";
|
import { MarkdownTextArea } from "./markdown-textarea";
|
||||||
import { MomentTime } from "./moment-time";
|
import { MomentTime } from "./moment-time";
|
||||||
|
|
||||||
interface RegistrationApplicationProps {
|
interface RegistrationApplicationProps {
|
||||||
application: RegistrationApplicationView;
|
application: RegistrationApplicationView;
|
||||||
|
onApproveApplication(form: ApproveRegistrationApplication): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegistrationApplicationState {
|
interface RegistrationApplicationState {
|
||||||
denyReason?: string;
|
denyReason?: string;
|
||||||
denyExpanded: boolean;
|
denyExpanded: boolean;
|
||||||
|
approveLoading: boolean;
|
||||||
|
denyLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegistrationApplication extends Component<
|
export class RegistrationApplication extends Component<
|
||||||
|
@ -27,17 +30,32 @@ export class RegistrationApplication extends Component<
|
||||||
state: RegistrationApplicationState = {
|
state: RegistrationApplicationState = {
|
||||||
denyReason: this.props.application.registration_application.deny_reason,
|
denyReason: this.props.application.registration_application.deny_reason,
|
||||||
denyExpanded: false,
|
denyExpanded: false,
|
||||||
|
approveLoading: false,
|
||||||
|
denyLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
|
this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
|
||||||
}
|
}
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<
|
||||||
|
{ children?: InfernoNode } & RegistrationApplicationProps
|
||||||
|
>
|
||||||
|
): void {
|
||||||
|
if (this.props != nextProps) {
|
||||||
|
this.setState({
|
||||||
|
denyExpanded: false,
|
||||||
|
approveLoading: false,
|
||||||
|
denyLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let a = this.props.application;
|
const a = this.props.application;
|
||||||
let ra = this.props.application.registration_application;
|
const ra = this.props.application.registration_application;
|
||||||
let accepted = a.creator_local_user.accepted_application;
|
const accepted = a.creator_local_user.accepted_application;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -99,7 +117,7 @@ export class RegistrationApplication extends Component<
|
||||||
onClick={linkEvent(this, this.handleApprove)}
|
onClick={linkEvent(this, this.handleApprove)}
|
||||||
aria-label={i18n.t("approve")}
|
aria-label={i18n.t("approve")}
|
||||||
>
|
>
|
||||||
{i18n.t("approve")}
|
{this.state.approveLoading ? <Spinner /> : i18n.t("approve")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(!ra.admin_id || (ra.admin_id && accepted)) && (
|
{(!ra.admin_id || (ra.admin_id && accepted)) && (
|
||||||
|
@ -108,7 +126,7 @@ export class RegistrationApplication extends Component<
|
||||||
onClick={linkEvent(this, this.handleDeny)}
|
onClick={linkEvent(this, this.handleDeny)}
|
||||||
aria-label={i18n.t("deny")}
|
aria-label={i18n.t("deny")}
|
||||||
>
|
>
|
||||||
{i18n.t("deny")}
|
{this.state.denyLoading ? <Spinner /> : i18n.t("deny")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,35 +134,23 @@ export class RegistrationApplication extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleApprove(i: RegistrationApplication) {
|
handleApprove(i: RegistrationApplication) {
|
||||||
let auth = myAuth();
|
i.setState({ denyExpanded: false, approveLoading: true });
|
||||||
if (auth) {
|
i.props.onApproveApplication({
|
||||||
i.setState({ denyExpanded: false });
|
id: i.props.application.registration_application.id,
|
||||||
let form: ApproveRegistrationApplication = {
|
approve: true,
|
||||||
id: i.props.application.registration_application.id,
|
auth: myAuthRequired(),
|
||||||
approve: true,
|
});
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.approveRegistrationApplication(form)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeny(i: RegistrationApplication) {
|
handleDeny(i: RegistrationApplication) {
|
||||||
if (i.state.denyExpanded) {
|
if (i.state.denyExpanded) {
|
||||||
i.setState({ denyExpanded: false });
|
i.setState({ denyExpanded: false, denyLoading: true });
|
||||||
let auth = myAuth();
|
i.props.onApproveApplication({
|
||||||
if (auth) {
|
id: i.props.application.registration_application.id,
|
||||||
let form: ApproveRegistrationApplication = {
|
approve: false,
|
||||||
id: i.props.application.registration_application.id,
|
deny_reason: i.state.denyReason,
|
||||||
approve: false,
|
auth: myAuthRequired(),
|
||||||
deny_reason: i.state.denyReason,
|
});
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.approveRegistrationApplication(form)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
i.setState({ denyExpanded: true });
|
i.setState({ denyExpanded: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,38 @@ function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusSearch(i: SearchableSelect) {
|
||||||
|
if (i.toggleButtonRef.current?.ariaExpanded !== "true") {
|
||||||
|
i.searchInputRef.current?.focus();
|
||||||
|
|
||||||
|
if (i.props.onSearch) {
|
||||||
|
i.props.onSearch("");
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({
|
||||||
|
searchText: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange({ option, i }: { option: Choice; i: SearchableSelect }) {
|
||||||
|
const { onChange, value } = i.props;
|
||||||
|
|
||||||
|
if (option.value !== value?.toString()) {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ searchText: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SearchableSelect extends Component<
|
export class SearchableSelect extends Component<
|
||||||
SearchableSelectProps,
|
SearchableSelectProps,
|
||||||
SearchableSelectState
|
SearchableSelectState
|
||||||
> {
|
> {
|
||||||
private searchInputRef: RefObject<HTMLInputElement> = createRef();
|
searchInputRef: RefObject<HTMLInputElement> = createRef();
|
||||||
private toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
|
toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
|
||||||
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
|
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
|
||||||
|
|
||||||
state: SearchableSelectState = {
|
state: SearchableSelectState = {
|
||||||
|
@ -55,9 +81,6 @@ export class SearchableSelect extends Component<
|
||||||
constructor(props: SearchableSelectProps, context: any) {
|
constructor(props: SearchableSelectProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.focusSearch = this.focusSearch.bind(this);
|
|
||||||
|
|
||||||
if (props.value) {
|
if (props.value) {
|
||||||
let selectedIndex = props.options.findIndex(
|
let selectedIndex = props.options.findIndex(
|
||||||
({ value }) => value === props.value?.toString()
|
({ value }) => value === props.value?.toString()
|
||||||
|
@ -86,7 +109,8 @@ export class SearchableSelect extends Component<
|
||||||
className="custom-select text-start"
|
className="custom-select text-start"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
onClick={this.focusSearch}
|
onClick={linkEvent(this, focusSearch)}
|
||||||
|
ref={this.toggleButtonRef}
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? `${i18n.t("loading")}${loadingEllipses}`
|
? `${i18n.t("loading")}${loadingEllipses}`
|
||||||
|
@ -127,7 +151,7 @@ export class SearchableSelect extends Component<
|
||||||
aria-disabled={option.disabled}
|
aria-disabled={option.disabled}
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
aria-selected={selectedIndex === index}
|
aria-selected={selectedIndex === index}
|
||||||
onClick={() => this.handleChange(option)}
|
onClick={linkEvent({ i: this, option }, handleChange)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
@ -138,20 +162,6 @@ export class SearchableSelect extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusSearch() {
|
|
||||||
if (this.toggleButtonRef.current?.ariaExpanded !== "true") {
|
|
||||||
this.searchInputRef.current?.focus();
|
|
||||||
|
|
||||||
if (this.props.onSearch) {
|
|
||||||
this.props.onSearch("");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
searchText: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps({
|
static getDerivedStateFromProps({
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
|
@ -189,16 +199,4 @@ export class SearchableSelect extends Component<
|
||||||
clearInterval(this.loadingEllipsesInterval);
|
clearInterval(this.loadingEllipsesInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(option: Choice) {
|
|
||||||
const { onChange, value } = this.props;
|
|
||||||
|
|
||||||
if (option.value !== value?.toString()) {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ searchText: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Icon } from "./icon";
|
||||||
|
|
||||||
interface SortSelectProps {
|
interface SortSelectProps {
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
onChange?(val: SortType): any;
|
onChange(val: SortType): void;
|
||||||
hideHot?: boolean;
|
hideHot?: boolean;
|
||||||
hideMostComments?: boolean;
|
hideMostComments?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: any): SortSelectState {
|
static getDerivedStateFromProps(props: SortSelectProps): SortSelectState {
|
||||||
return {
|
return {
|
||||||
sort: props.sort,
|
sort: props.sort,
|
||||||
};
|
};
|
||||||
|
@ -85,6 +85,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: SortSelect, event: any) {
|
handleSortChange(i: SortSelect, event: any) {
|
||||||
i.props.onChange?.(event.target.value);
|
i.props.onChange(event.target.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
CommunityResponse,
|
CommunityResponse,
|
||||||
FollowCommunity,
|
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListCommunities,
|
ListCommunities,
|
||||||
ListCommunitiesResponse,
|
ListCommunitiesResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { InitialFetchRequest } from "shared/interfaces";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
QueryParams,
|
QueryParams,
|
||||||
|
editCommunity,
|
||||||
getPageFromString,
|
getPageFromString,
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
isBrowser,
|
|
||||||
myAuth,
|
myAuth,
|
||||||
|
myAuthRequired,
|
||||||
numToSI,
|
numToSI,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
showLocal,
|
showLocal,
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
@ -37,10 +31,10 @@ import { CommunityLink } from "./community-link";
|
||||||
const communityLimit = 50;
|
const communityLimit = 50;
|
||||||
|
|
||||||
interface CommunitiesState {
|
interface CommunitiesState {
|
||||||
listCommunitiesResponse?: ListCommunitiesResponse;
|
listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
|
||||||
loading: boolean;
|
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunitiesProps {
|
interface CommunitiesProps {
|
||||||
|
@ -48,51 +42,17 @@ interface CommunitiesProps {
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCommunitiesQueryParams() {
|
|
||||||
return getQueryParams<CommunitiesProps>({
|
|
||||||
listingType: getListingTypeFromQuery,
|
|
||||||
page: getPageFromString,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getListingTypeFromQuery(listingType?: string): ListingType {
|
function getListingTypeFromQuery(listingType?: string): ListingType {
|
||||||
return listingType ? (listingType as ListingType) : "Local";
|
return listingType ? (listingType as ListingType) : "Local";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSubscribe(community_id: number, follow: boolean) {
|
|
||||||
const auth = myAuth();
|
|
||||||
if (auth) {
|
|
||||||
const form: FollowCommunity = {
|
|
||||||
community_id,
|
|
||||||
follow,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.followCommunity(form));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refetch() {
|
|
||||||
const { listingType, page } = getCommunitiesQueryParams();
|
|
||||||
|
|
||||||
const listCommunitiesForm: ListCommunities = {
|
|
||||||
type_: listingType,
|
|
||||||
sort: "TopMonth",
|
|
||||||
limit: communityLimit,
|
|
||||||
page,
|
|
||||||
auth: myAuth(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Communities extends Component<any, CommunitiesState> {
|
export class Communities extends Component<any, CommunitiesState> {
|
||||||
private subscription?: Subscription;
|
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
state: CommunitiesState = {
|
state: CommunitiesState = {
|
||||||
loading: true,
|
listCommunitiesResponse: { state: "empty" },
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -100,25 +60,19 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
|
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path === this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
const listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listCommunitiesResponse: listRes,
|
listCommunitiesResponse: this.isoData.routeData[0],
|
||||||
loading: false,
|
isIsomorphic: true,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
refetch();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentDidMount() {
|
||||||
if (isBrowser()) {
|
if (!this.state.isIsomorphic) {
|
||||||
this.subscription?.unsubscribe();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,20 +82,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderListings() {
|
||||||
const { listingType, page } = getCommunitiesQueryParams();
|
switch (this.state.listCommunitiesResponse.state) {
|
||||||
|
case "loading":
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
|
||||||
<HtmlTags
|
|
||||||
title={this.documentTitle}
|
|
||||||
path={this.context.router.route.match.url}
|
|
||||||
/>
|
|
||||||
{this.state.loading ? (
|
|
||||||
<h5>
|
<h5>
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
);
|
||||||
|
case "success": {
|
||||||
|
const { listingType, page } = this.getCommunitiesQueryParams();
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
|
@ -182,60 +133,82 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.state.listCommunitiesResponse?.communities.map(cv => (
|
{this.state.listCommunitiesResponse.data.communities.map(
|
||||||
<tr key={cv.community.id}>
|
cv => (
|
||||||
<td>
|
<tr key={cv.community.id}>
|
||||||
<CommunityLink community={cv.community} />
|
<td>
|
||||||
</td>
|
<CommunityLink community={cv.community} />
|
||||||
<td className="text-right">
|
</td>
|
||||||
{numToSI(cv.counts.subscribers)}
|
<td className="text-right">
|
||||||
</td>
|
{numToSI(cv.counts.subscribers)}
|
||||||
<td className="text-right">
|
</td>
|
||||||
{numToSI(cv.counts.users_active_month)}
|
<td className="text-right">
|
||||||
</td>
|
{numToSI(cv.counts.users_active_month)}
|
||||||
<td className="text-right d-none d-lg-table-cell">
|
</td>
|
||||||
{numToSI(cv.counts.posts)}
|
<td className="text-right d-none d-lg-table-cell">
|
||||||
</td>
|
{numToSI(cv.counts.posts)}
|
||||||
<td className="text-right d-none d-lg-table-cell">
|
</td>
|
||||||
{numToSI(cv.counts.comments)}
|
<td className="text-right d-none d-lg-table-cell">
|
||||||
</td>
|
{numToSI(cv.counts.comments)}
|
||||||
<td className="text-right">
|
</td>
|
||||||
{cv.subscribed == "Subscribed" && (
|
<td className="text-right">
|
||||||
<button
|
{cv.subscribed == "Subscribed" && (
|
||||||
className="btn btn-link d-inline-block"
|
<button
|
||||||
onClick={linkEvent(
|
className="btn btn-link d-inline-block"
|
||||||
cv.community.id,
|
onClick={linkEvent(
|
||||||
this.handleUnsubscribe
|
{
|
||||||
)}
|
i: this,
|
||||||
>
|
communityId: cv.community.id,
|
||||||
{i18n.t("unsubscribe")}
|
follow: false,
|
||||||
</button>
|
},
|
||||||
)}
|
this.handleFollow
|
||||||
{cv.subscribed === "NotSubscribed" && (
|
)}
|
||||||
<button
|
>
|
||||||
className="btn btn-link d-inline-block"
|
{i18n.t("unsubscribe")}
|
||||||
onClick={linkEvent(
|
</button>
|
||||||
cv.community.id,
|
)}
|
||||||
this.handleSubscribe
|
{cv.subscribed === "NotSubscribed" && (
|
||||||
)}
|
<button
|
||||||
>
|
className="btn btn-link d-inline-block"
|
||||||
{i18n.t("subscribe")}
|
onClick={linkEvent(
|
||||||
</button>
|
{
|
||||||
)}
|
i: this,
|
||||||
{cv.subscribed === "Pending" && (
|
communityId: cv.community.id,
|
||||||
<div className="text-warning d-inline-block">
|
follow: true,
|
||||||
{i18n.t("subscribe_pending")}
|
},
|
||||||
</div>
|
this.handleFollow
|
||||||
)}
|
)}
|
||||||
</td>
|
>
|
||||||
</tr>
|
{i18n.t("subscribe")}
|
||||||
))}
|
</button>
|
||||||
|
)}
|
||||||
|
{cv.subscribed === "Pending" && (
|
||||||
|
<div className="text-warning d-inline-block">
|
||||||
|
{i18n.t("subscribe_pending")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<Paginator page={page} onChange={this.handlePageChange} />
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="container-lg">
|
||||||
|
<HtmlTags
|
||||||
|
title={this.documentTitle}
|
||||||
|
path={this.context.router.route.match.url}
|
||||||
|
/>
|
||||||
|
{this.renderListings()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -266,9 +239,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
|
async updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
|
||||||
const { listingType: urlListingType, page: urlPage } =
|
const { listingType: urlListingType, page: urlPage } =
|
||||||
getCommunitiesQueryParams();
|
this.getCommunitiesQueryParams();
|
||||||
|
|
||||||
const queryParams: QueryParams<CommunitiesProps> = {
|
const queryParams: QueryParams<CommunitiesProps> = {
|
||||||
listingType: listingType ?? urlListingType,
|
listingType: listingType ?? urlListingType,
|
||||||
|
@ -277,7 +250,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
|
|
||||||
this.props.history.push(`/communities${getQueryString(queryParams)}`);
|
this.props.history.push(`/communities${getQueryString(queryParams)}`);
|
||||||
|
|
||||||
refetch();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePageChange(page: number) {
|
handlePageChange(page: number) {
|
||||||
|
@ -291,19 +264,12 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
|
||||||
toggleSubscribe(communityId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubscribe(communityId: number) {
|
|
||||||
toggleSubscribe(communityId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSearchChange(i: Communities, event: any) {
|
handleSearchChange(i: Communities, event: any) {
|
||||||
i.setState({ searchText: event.target.value });
|
i.setState({ searchText: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchSubmit(i: Communities) {
|
handleSearchSubmit(i: Communities, event: any) {
|
||||||
|
event.preventDefault();
|
||||||
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
||||||
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
||||||
}
|
}
|
||||||
|
@ -312,7 +278,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
query: { listingType, page },
|
query: { listingType, page },
|
||||||
client,
|
client,
|
||||||
auth,
|
auth,
|
||||||
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<any>[] {
|
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<
|
||||||
|
RequestState<any>
|
||||||
|
>[] {
|
||||||
const listCommunitiesForm: ListCommunities = {
|
const listCommunitiesForm: ListCommunities = {
|
||||||
type_: getListingTypeFromQuery(listingType),
|
type_: getListingTypeFromQuery(listingType),
|
||||||
sort: "TopMonth",
|
sort: "TopMonth",
|
||||||
|
@ -324,33 +292,56 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
return [client.listCommunities(listCommunitiesForm)];
|
return [client.listCommunities(listCommunitiesForm)];
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
getCommunitiesQueryParams() {
|
||||||
const op = wsUserOp(msg);
|
return getQueryParams<CommunitiesProps>({
|
||||||
console.log(msg);
|
listingType: getListingTypeFromQuery,
|
||||||
if (msg.error) {
|
page: getPageFromString,
|
||||||
toast(i18n.t(msg.error), "danger");
|
});
|
||||||
} else if (op === UserOperation.ListCommunities) {
|
}
|
||||||
const data = wsJsonToRes<ListCommunitiesResponse>(msg);
|
|
||||||
this.setState({ listCommunitiesResponse: data, loading: false });
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
} else if (op === UserOperation.FollowCommunity) {
|
|
||||||
const {
|
|
||||||
community_view: {
|
|
||||||
community,
|
|
||||||
subscribed,
|
|
||||||
counts: { subscribers },
|
|
||||||
},
|
|
||||||
} = wsJsonToRes<CommunityResponse>(msg);
|
|
||||||
const res = this.state.listCommunitiesResponse;
|
|
||||||
const found = res?.communities.find(
|
|
||||||
({ community: { id } }) => id == community.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (found) {
|
async handleFollow(data: {
|
||||||
found.subscribed = subscribed;
|
i: Communities;
|
||||||
found.counts.subscribers = subscribers;
|
communityId: number;
|
||||||
this.setState(this.state);
|
follow: boolean;
|
||||||
|
}) {
|
||||||
|
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" } });
|
||||||
|
|
||||||
|
const { listingType, page } = this.getCommunitiesQueryParams();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
listCommunitiesResponse: await HttpService.client.listCommunities({
|
||||||
|
type_: listingType,
|
||||||
|
sort: "TopMonth",
|
||||||
|
limit: communityLimit,
|
||||||
|
page,
|
||||||
|
auth: myAuth(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
|
||||||
|
this.setState(s => {
|
||||||
|
if (
|
||||||
|
s.listCommunitiesResponse.state == "success" &&
|
||||||
|
res.state == "success"
|
||||||
|
) {
|
||||||
|
s.listCommunitiesResponse.data.communities = editCommunity(
|
||||||
|
res.data.community_view,
|
||||||
|
s.listCommunitiesResponse.data.communities
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
return s;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,17 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { Prompt } from "inferno-router";
|
|
||||||
import {
|
import {
|
||||||
CommunityResponse,
|
|
||||||
CommunityView,
|
CommunityView,
|
||||||
CreateCommunity,
|
CreateCommunity,
|
||||||
EditCommunity,
|
EditCommunity,
|
||||||
Language,
|
Language,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { capitalizeFirstLetter, myAuthRequired, randomStr } from "../../utils";
|
||||||
import {
|
|
||||||
capitalizeFirstLetter,
|
|
||||||
myAuth,
|
|
||||||
randomStr,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { ImageUploadForm } from "../common/image-upload-form";
|
import { ImageUploadForm } from "../common/image-upload-form";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import NavigationPrompt from "../common/navigation-prompt";
|
||||||
|
|
||||||
interface CommunityFormProps {
|
interface CommunityFormProps {
|
||||||
community_view?: CommunityView; // If a community is given, that means this is an edit
|
community_view?: CommunityView; // If a community is given, that means this is an edit
|
||||||
|
@ -31,9 +19,9 @@ interface CommunityFormProps {
|
||||||
siteLanguages: number[];
|
siteLanguages: number[];
|
||||||
communityLanguages?: number[];
|
communityLanguages?: number[];
|
||||||
onCancel?(): any;
|
onCancel?(): any;
|
||||||
onCreate?(community: CommunityView): any;
|
onUpsertCommunity(form: CreateCommunity | EditCommunity): void;
|
||||||
onEdit?(community: CommunityView): any;
|
|
||||||
enableNsfw?: boolean;
|
enableNsfw?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunityFormState {
|
interface CommunityFormState {
|
||||||
|
@ -47,7 +35,7 @@ interface CommunityFormState {
|
||||||
posting_restricted_to_mods?: boolean;
|
posting_restricted_to_mods?: boolean;
|
||||||
discussion_languages?: number[];
|
discussion_languages?: number[];
|
||||||
};
|
};
|
||||||
loading: boolean;
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommunityForm extends Component<
|
export class CommunityForm extends Component<
|
||||||
|
@ -55,11 +43,10 @@ export class CommunityForm extends Component<
|
||||||
CommunityFormState
|
CommunityFormState
|
||||||
> {
|
> {
|
||||||
private id = `community-form-${randomStr()}`;
|
private id = `community-form-${randomStr()}`;
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
state: CommunityFormState = {
|
state: CommunityFormState = {
|
||||||
form: {},
|
form: {},
|
||||||
loading: false,
|
submitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -77,12 +64,11 @@ export class CommunityForm extends Component<
|
||||||
this.handleDiscussionLanguageChange =
|
this.handleDiscussionLanguageChange =
|
||||||
this.handleDiscussionLanguageChange.bind(this);
|
this.handleDiscussionLanguageChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
const cv = this.props.community_view;
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
let cv = this.props.community_view;
|
|
||||||
|
|
||||||
if (cv) {
|
if (cv) {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
...this.state,
|
||||||
form: {
|
form: {
|
||||||
name: cv.community.name,
|
name: cv.community.name,
|
||||||
title: cv.community.title,
|
title: cv.community.title,
|
||||||
|
@ -93,80 +79,34 @@ export class CommunityForm extends Component<
|
||||||
posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
|
posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
|
||||||
discussion_languages: this.props.communityLanguages,
|
discussion_languages: this.props.communityLanguages,
|
||||||
},
|
},
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (
|
|
||||||
!this.state.loading &&
|
|
||||||
(this.state.form.name ||
|
|
||||||
this.state.form.title ||
|
|
||||||
this.state.form.description)
|
|
||||||
) {
|
|
||||||
window.onbeforeunload = () => true;
|
|
||||||
} else {
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
||||||
<Prompt
|
<NavigationPrompt
|
||||||
when={
|
when={
|
||||||
!this.state.loading &&
|
!this.props.loading &&
|
||||||
(this.state.form.name ||
|
!!(
|
||||||
|
this.state.form.name ||
|
||||||
this.state.form.title ||
|
this.state.form.title ||
|
||||||
this.state.form.description)
|
this.state.form.description
|
||||||
|
) &&
|
||||||
|
!this.state.submitted
|
||||||
}
|
}
|
||||||
message={i18n.t("block_leaving")}
|
|
||||||
/>
|
/>
|
||||||
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
{!this.props.community_view && (
|
||||||
{!this.props.community_view && (
|
|
||||||
<div className="form-group row">
|
|
||||||
<label
|
|
||||||
className="col-12 col-sm-2 col-form-label"
|
|
||||||
htmlFor="community-name"
|
|
||||||
>
|
|
||||||
{i18n.t("name")}
|
|
||||||
<span
|
|
||||||
className="position-absolute pointer unselectable ml-2 text-muted"
|
|
||||||
data-tippy-content={i18n.t("name_explain")}
|
|
||||||
>
|
|
||||||
<Icon icon="help-circle" classes="icon-inline" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="col-12 col-sm-10">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="community-name"
|
|
||||||
className="form-control"
|
|
||||||
value={this.state.form.name}
|
|
||||||
onInput={linkEvent(this, this.handleCommunityNameChange)}
|
|
||||||
required
|
|
||||||
minLength={3}
|
|
||||||
pattern="[a-z0-9_]+"
|
|
||||||
title={i18n.t("community_reqs")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<label
|
<label
|
||||||
className="col-12 col-sm-2 col-form-label"
|
className="col-12 col-sm-2 col-form-label"
|
||||||
htmlFor="community-title"
|
htmlFor="community-name"
|
||||||
>
|
>
|
||||||
{i18n.t("display_name")}
|
{i18n.t("name")}
|
||||||
<span
|
<span
|
||||||
className="position-absolute pointer unselectable ml-2 text-muted"
|
className="position-absolute pointer unselectable ml-2 text-muted"
|
||||||
data-tippy-content={i18n.t("display_name_explain")}
|
data-tippy-content={i18n.t("name_explain")}
|
||||||
>
|
>
|
||||||
<Icon icon="help-circle" classes="icon-inline" />
|
<Icon icon="help-circle" classes="icon-inline" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -174,142 +114,182 @@ export class CommunityForm extends Component<
|
||||||
<div className="col-12 col-sm-10">
|
<div className="col-12 col-sm-10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="community-title"
|
id="community-name"
|
||||||
value={this.state.form.title}
|
|
||||||
onInput={linkEvent(this, this.handleCommunityTitleChange)}
|
|
||||||
className="form-control"
|
className="form-control"
|
||||||
|
value={this.state.form.name}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityNameChange)}
|
||||||
required
|
required
|
||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={100}
|
pattern="[a-z0-9_]+"
|
||||||
|
title={i18n.t("community_reqs")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group row">
|
)}
|
||||||
<label className="col-12 col-sm-2">{i18n.t("icon")}</label>
|
<div className="form-group row">
|
||||||
<div className="col-12 col-sm-10">
|
<label
|
||||||
<ImageUploadForm
|
className="col-12 col-sm-2 col-form-label"
|
||||||
uploadTitle={i18n.t("upload_icon")}
|
htmlFor="community-title"
|
||||||
imageSrc={this.state.form.icon}
|
>
|
||||||
onUpload={this.handleIconUpload}
|
{i18n.t("display_name")}
|
||||||
onRemove={this.handleIconRemove}
|
<span
|
||||||
rounded
|
className="position-absolute pointer unselectable ml-2 text-muted"
|
||||||
/>
|
data-tippy-content={i18n.t("display_name_explain")}
|
||||||
</div>
|
>
|
||||||
|
<Icon icon="help-circle" classes="icon-inline" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="col-12 col-sm-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="community-title"
|
||||||
|
value={this.state.form.title}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityTitleChange)}
|
||||||
|
className="form-control"
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group row">
|
</div>
|
||||||
<label className="col-12 col-sm-2">{i18n.t("banner")}</label>
|
<div className="form-group row">
|
||||||
<div className="col-12 col-sm-10">
|
<label className="col-12 col-sm-2">{i18n.t("icon")}</label>
|
||||||
<ImageUploadForm
|
<div className="col-12 col-sm-10">
|
||||||
uploadTitle={i18n.t("upload_banner")}
|
<ImageUploadForm
|
||||||
imageSrc={this.state.form.banner}
|
uploadTitle={i18n.t("upload_icon")}
|
||||||
onUpload={this.handleBannerUpload}
|
imageSrc={this.state.form.icon}
|
||||||
onRemove={this.handleBannerRemove}
|
onUpload={this.handleIconUpload}
|
||||||
/>
|
onRemove={this.handleIconRemove}
|
||||||
</div>
|
rounded
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group row">
|
</div>
|
||||||
<label className="col-12 col-sm-2 col-form-label" htmlFor={this.id}>
|
<div className="form-group row">
|
||||||
{i18n.t("sidebar")}
|
<label className="col-12 col-sm-2">{i18n.t("banner")}</label>
|
||||||
</label>
|
<div className="col-12 col-sm-10">
|
||||||
<div className="col-12 col-sm-10">
|
<ImageUploadForm
|
||||||
<MarkdownTextArea
|
uploadTitle={i18n.t("upload_banner")}
|
||||||
initialContent={this.state.form.description}
|
imageSrc={this.state.form.banner}
|
||||||
placeholder={i18n.t("description")}
|
onUpload={this.handleBannerUpload}
|
||||||
onContentChange={this.handleCommunityDescriptionChange}
|
onRemove={this.handleBannerRemove}
|
||||||
allLanguages={[]}
|
/>
|
||||||
siteLanguages={[]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group row">
|
||||||
|
<label className="col-12 col-sm-2 col-form-label" htmlFor={this.id}>
|
||||||
|
{i18n.t("sidebar")}
|
||||||
|
</label>
|
||||||
|
<div className="col-12 col-sm-10">
|
||||||
|
<MarkdownTextArea
|
||||||
|
initialContent={this.state.form.description}
|
||||||
|
placeholder={i18n.t("description")}
|
||||||
|
onContentChange={this.handleCommunityDescriptionChange}
|
||||||
|
hideNavigationWarnings
|
||||||
|
allLanguages={[]}
|
||||||
|
siteLanguages={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{this.props.enableNsfw && (
|
{this.props.enableNsfw && (
|
||||||
<div className="form-group row">
|
|
||||||
<legend className="col-form-label col-sm-2 pt-0">
|
|
||||||
{i18n.t("nsfw")}
|
|
||||||
</legend>
|
|
||||||
<div className="col-10">
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
className="form-check-input position-static"
|
|
||||||
id="community-nsfw"
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.form.nsfw}
|
|
||||||
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<legend className="col-form-label col-6 pt-0">
|
<legend className="col-form-label col-sm-2 pt-0">
|
||||||
{i18n.t("only_mods_can_post_in_community")}
|
{i18n.t("nsfw")}
|
||||||
</legend>
|
</legend>
|
||||||
<div className="col-6">
|
<div className="col-10">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input
|
<input
|
||||||
className="form-check-input position-static"
|
className="form-check-input position-static"
|
||||||
id="community-only-mods-can-post"
|
id="community-nsfw"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.form.posting_restricted_to_mods}
|
checked={this.state.form.nsfw}
|
||||||
onChange={linkEvent(
|
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
|
||||||
this,
|
|
||||||
this.handleCommunityPostingRestrictedToMods
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LanguageSelect
|
)}
|
||||||
allLanguages={this.props.allLanguages}
|
<div className="form-group row">
|
||||||
siteLanguages={this.props.siteLanguages}
|
<legend className="col-form-label col-6 pt-0">
|
||||||
showSite
|
{i18n.t("only_mods_can_post_in_community")}
|
||||||
selectedLanguageIds={this.state.form.discussion_languages}
|
</legend>
|
||||||
multiple={true}
|
<div className="col-6">
|
||||||
onChange={this.handleDiscussionLanguageChange}
|
<div className="form-check">
|
||||||
/>
|
<input
|
||||||
<div className="form-group row">
|
className="form-check-input position-static"
|
||||||
<div className="col-12">
|
id="community-only-mods-can-post"
|
||||||
<button
|
type="checkbox"
|
||||||
type="submit"
|
checked={this.state.form.posting_restricted_to_mods}
|
||||||
className="btn btn-secondary mr-2"
|
onChange={linkEvent(
|
||||||
disabled={this.state.loading}
|
this,
|
||||||
>
|
this.handleCommunityPostingRestrictedToMods
|
||||||
{this.state.loading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : this.props.community_view ? (
|
|
||||||
capitalizeFirstLetter(i18n.t("save"))
|
|
||||||
) : (
|
|
||||||
capitalizeFirstLetter(i18n.t("create"))
|
|
||||||
)}
|
)}
|
||||||
</button>
|
/>
|
||||||
{this.props.community_view && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={linkEvent(this, this.handleCancel)}
|
|
||||||
>
|
|
||||||
{i18n.t("cancel")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</>
|
<LanguageSelect
|
||||||
|
allLanguages={this.props.allLanguages}
|
||||||
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
showSite
|
||||||
|
selectedLanguageIds={this.state.form.discussion_languages}
|
||||||
|
multiple={true}
|
||||||
|
onChange={this.handleDiscussionLanguageChange}
|
||||||
|
/>
|
||||||
|
<div className="form-group row">
|
||||||
|
<div className="col-12">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-secondary mr-2"
|
||||||
|
disabled={this.props.loading}
|
||||||
|
>
|
||||||
|
{this.props.loading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : this.props.community_view ? (
|
||||||
|
capitalizeFirstLetter(i18n.t("save"))
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t("create"))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{this.props.community_view && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleCancel)}
|
||||||
|
>
|
||||||
|
{i18n.t("cancel")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCreateCommunitySubmit(i: CommunityForm, event: any) {
|
handleCreateCommunitySubmit(i: CommunityForm, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ loading: true });
|
i.setState({ submitted: true });
|
||||||
let cForm = i.state.form;
|
const cForm = i.state.form;
|
||||||
let auth = myAuth();
|
const auth = myAuthRequired();
|
||||||
|
|
||||||
let cv = i.props.community_view;
|
const cv = i.props.community_view;
|
||||||
|
|
||||||
if (auth) {
|
if (cv) {
|
||||||
if (cv) {
|
i.props.onUpsertCommunity({
|
||||||
let form: EditCommunity = {
|
community_id: cv.community.id,
|
||||||
community_id: cv.community.id,
|
title: cForm.title,
|
||||||
|
description: cForm.description,
|
||||||
|
icon: cForm.icon,
|
||||||
|
banner: cForm.banner,
|
||||||
|
nsfw: cForm.nsfw,
|
||||||
|
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
|
||||||
|
discussion_languages: cForm.discussion_languages,
|
||||||
|
auth,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (cForm.title && cForm.name) {
|
||||||
|
i.props.onUpsertCommunity({
|
||||||
|
name: cForm.name,
|
||||||
title: cForm.title,
|
title: cForm.title,
|
||||||
description: cForm.description,
|
description: cForm.description,
|
||||||
icon: cForm.icon,
|
icon: cForm.icon,
|
||||||
|
@ -318,37 +298,17 @@ export class CommunityForm extends Component<
|
||||||
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
|
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
|
||||||
discussion_languages: cForm.discussion_languages,
|
discussion_languages: cForm.discussion_languages,
|
||||||
auth,
|
auth,
|
||||||
};
|
});
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.editCommunity(form));
|
|
||||||
} else {
|
|
||||||
if (cForm.title && cForm.name) {
|
|
||||||
let form: CreateCommunity = {
|
|
||||||
name: cForm.name,
|
|
||||||
title: cForm.title,
|
|
||||||
description: cForm.description,
|
|
||||||
icon: cForm.icon,
|
|
||||||
banner: cForm.banner,
|
|
||||||
nsfw: cForm.nsfw,
|
|
||||||
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
|
|
||||||
discussion_languages: cForm.discussion_languages,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.createCommunity(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityNameChange(i: CommunityForm, event: any) {
|
handleCommunityNameChange(i: CommunityForm, event: any) {
|
||||||
i.state.form.name = event.target.value;
|
i.setState(s => ((s.form.name = event.target.value), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityTitleChange(i: CommunityForm, event: any) {
|
handleCommunityTitleChange(i: CommunityForm, event: any) {
|
||||||
i.state.form.title = event.target.value;
|
i.setState(s => ((s.form.title = event.target.value), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityDescriptionChange(val: string) {
|
handleCommunityDescriptionChange(val: string) {
|
||||||
|
@ -356,13 +316,13 @@ export class CommunityForm extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityNsfwChange(i: CommunityForm, event: any) {
|
handleCommunityNsfwChange(i: CommunityForm, event: any) {
|
||||||
i.state.form.nsfw = event.target.checked;
|
i.setState(s => ((s.form.nsfw = event.target.checked), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
|
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
|
||||||
i.state.form.posting_restricted_to_mods = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.form.posting_restricted_to_mods = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancel(i: CommunityForm) {
|
handleCancel(i: CommunityForm) {
|
||||||
|
@ -388,56 +348,4 @@ export class CommunityForm extends Component<
|
||||||
handleDiscussionLanguageChange(val: number[]) {
|
handleDiscussionLanguageChange(val: number[]) {
|
||||||
this.setState(s => ((s.form.discussion_languages = val), s));
|
this.setState(s => ((s.form.discussion_languages = val), s));
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
// Errors handled by top level pages
|
|
||||||
// toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ loading: false });
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.CreateCommunity) {
|
|
||||||
let data = wsJsonToRes<CommunityResponse>(msg);
|
|
||||||
this.props.onCreate?.(data.community_view);
|
|
||||||
|
|
||||||
// Update myUserInfo
|
|
||||||
let community = data.community_view.community;
|
|
||||||
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
|
||||||
let person = mui.local_user_view.person;
|
|
||||||
mui.follows.push({
|
|
||||||
community,
|
|
||||||
follower: person,
|
|
||||||
});
|
|
||||||
mui.moderates.push({
|
|
||||||
community,
|
|
||||||
moderator: person,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.EditCommunity) {
|
|
||||||
let data = wsJsonToRes<CommunityResponse>(msg);
|
|
||||||
this.setState({ loading: false });
|
|
||||||
this.props.onEdit?.(data.community_view);
|
|
||||||
let community = data.community_view.community;
|
|
||||||
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
|
||||||
let followFound = mui.follows.findIndex(
|
|
||||||
f => f.community.id == community.id
|
|
||||||
);
|
|
||||||
if (followFound) {
|
|
||||||
mui.follows[followFound].community = community;
|
|
||||||
}
|
|
||||||
|
|
||||||
let moderatesFound = mui.moderates.findIndex(
|
|
||||||
f => f.community.id == community.id
|
|
||||||
);
|
|
||||||
if (moderatesFound) {
|
|
||||||
mui.moderates[moderatesFound].community = community;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,22 +18,22 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let community = this.props.community;
|
const community = this.props.community;
|
||||||
let name_: string, title: string, link: string;
|
let name_: string, title: string, link: string;
|
||||||
let local = community.local == null ? true : community.local;
|
const local = community.local == null ? true : community.local;
|
||||||
if (local) {
|
if (local) {
|
||||||
name_ = community.name;
|
name_ = community.name;
|
||||||
title = community.title;
|
title = community.title;
|
||||||
link = `/c/${community.name}`;
|
link = `/c/${community.name}`;
|
||||||
} else {
|
} else {
|
||||||
let domain = hostname(community.actor_id);
|
const domain = hostname(community.actor_id);
|
||||||
name_ = `${community.name}@${domain}`;
|
name_ = `${community.name}@${domain}`;
|
||||||
title = `${community.title}@${domain}`;
|
title = `${community.title}@${domain}`;
|
||||||
link = !this.props.realLink ? `/c/${name_}` : community.actor_id;
|
link = !this.props.realLink ? `/c/${name_}` : community.actor_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
let apubName = `!${name_}`;
|
const apubName = `!${name_}`;
|
||||||
let displayName = this.props.useApubName ? apubName : title;
|
const displayName = this.props.useApubName ? apubName : title;
|
||||||
return !this.props.realLink ? (
|
return !this.props.realLink ? (
|
||||||
<Link
|
<Link
|
||||||
title={apubName}
|
title={apubName}
|
||||||
|
@ -55,7 +55,7 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarAndName(displayName: string) {
|
avatarAndName(displayName: string) {
|
||||||
let icon = this.props.community.icon;
|
const icon = this.props.community.icon;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!this.props.hideAvatar &&
|
{!this.props.hideAvatar &&
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,16 +1,12 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { CommunityView, GetSiteResponse } from "lemmy-js-client";
|
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
|
||||||
import {
|
import {
|
||||||
enableNsfw,
|
CreateCommunity as CreateCommunityI,
|
||||||
isBrowser,
|
GetSiteResponse,
|
||||||
setIsoData,
|
} from "lemmy-js-client";
|
||||||
toast,
|
import { i18n } from "../../i18next";
|
||||||
wsSubscribe,
|
import { HttpService } from "../../services/HttpService";
|
||||||
} from "../../utils";
|
import { enableNsfw, setIsoData } from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
|
||||||
import { CommunityForm } from "./community-form";
|
import { CommunityForm } from "./community-form";
|
||||||
|
|
||||||
interface CreateCommunityState {
|
interface CreateCommunityState {
|
||||||
|
@ -20,7 +16,6 @@ interface CreateCommunityState {
|
||||||
|
|
||||||
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: CreateCommunityState = {
|
state: CreateCommunityState = {
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -28,15 +23,6 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
|
@ -52,35 +38,33 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
/>
|
/>
|
||||||
{this.state.loading ? (
|
<div className="row">
|
||||||
<h5>
|
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<Spinner large />
|
<h5>{i18n.t("create_community")}</h5>
|
||||||
</h5>
|
<CommunityForm
|
||||||
) : (
|
onUpsertCommunity={this.handleCommunityCreate}
|
||||||
<div className="row">
|
enableNsfw={enableNsfw(this.state.siteRes)}
|
||||||
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
allLanguages={this.state.siteRes.all_languages}
|
||||||
<h5>{i18n.t("create_community")}</h5>
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
<CommunityForm
|
communityLanguages={this.state.siteRes.discussion_languages}
|
||||||
onCreate={this.handleCommunityCreate}
|
loading={this.state.loading}
|
||||||
enableNsfw={enableNsfw(this.state.siteRes)}
|
/>
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
|
||||||
communityLanguages={this.state.siteRes.discussion_languages}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityCreate(cv: CommunityView) {
|
async handleCommunityCreate(form: CreateCommunityI) {
|
||||||
this.props.history.push(`/c/${cv.community.name}`);
|
this.setState({ loading: true });
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
const res = await HttpService.client.createCommunity(form);
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
if (res.state === "success") {
|
||||||
|
const name = res.data.community_view.community.name;
|
||||||
|
this.props.history.replace(`/c/${name}`);
|
||||||
|
} else {
|
||||||
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { Link } from "inferno-router";
|
import { Link } from "inferno-router";
|
||||||
import {
|
import {
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
@ -6,6 +6,7 @@ import {
|
||||||
CommunityModeratorView,
|
CommunityModeratorView,
|
||||||
CommunityView,
|
CommunityView,
|
||||||
DeleteCommunity,
|
DeleteCommunity,
|
||||||
|
EditCommunity,
|
||||||
FollowCommunity,
|
FollowCommunity,
|
||||||
Language,
|
Language,
|
||||||
PersonView,
|
PersonView,
|
||||||
|
@ -13,7 +14,7 @@ import {
|
||||||
RemoveCommunity,
|
RemoveCommunity,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
import {
|
import {
|
||||||
amAdmin,
|
amAdmin,
|
||||||
amMod,
|
amMod,
|
||||||
|
@ -21,9 +22,8 @@ import {
|
||||||
getUnixTime,
|
getUnixTime,
|
||||||
hostname,
|
hostname,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
myAuth,
|
myAuthRequired,
|
||||||
numToSI,
|
numToSI,
|
||||||
wsClient,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||||
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
||||||
|
@ -42,6 +42,13 @@ interface SidebarProps {
|
||||||
enableNsfw?: boolean;
|
enableNsfw?: boolean;
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
onDeleteCommunity(form: DeleteCommunity): void;
|
||||||
|
onRemoveCommunity(form: RemoveCommunity): void;
|
||||||
|
onLeaveModTeam(form: AddModToCommunity): void;
|
||||||
|
onFollowCommunity(form: FollowCommunity): void;
|
||||||
|
onBlockCommunity(form: BlockCommunity): void;
|
||||||
|
onPurgeCommunity(form: PurgeCommunity): void;
|
||||||
|
onEditCommunity(form: EditCommunity): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarState {
|
interface SidebarState {
|
||||||
|
@ -51,8 +58,13 @@ interface SidebarState {
|
||||||
showRemoveDialog: boolean;
|
showRemoveDialog: boolean;
|
||||||
showPurgeDialog: boolean;
|
showPurgeDialog: boolean;
|
||||||
purgeReason?: string;
|
purgeReason?: string;
|
||||||
purgeLoading: boolean;
|
|
||||||
showConfirmLeaveModTeam: boolean;
|
showConfirmLeaveModTeam: boolean;
|
||||||
|
deleteCommunityLoading: boolean;
|
||||||
|
removeCommunityLoading: boolean;
|
||||||
|
leaveModTeamLoading: boolean;
|
||||||
|
followCommunityLoading: boolean;
|
||||||
|
blockCommunityLoading: boolean;
|
||||||
|
purgeCommunityLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
@ -60,16 +72,44 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
showRemoveDialog: false,
|
showRemoveDialog: false,
|
||||||
showPurgeDialog: false,
|
showPurgeDialog: false,
|
||||||
purgeLoading: false,
|
|
||||||
showConfirmLeaveModTeam: false,
|
showConfirmLeaveModTeam: false,
|
||||||
|
deleteCommunityLoading: false,
|
||||||
|
removeCommunityLoading: false,
|
||||||
|
leaveModTeamLoading: false,
|
||||||
|
followCommunityLoading: false,
|
||||||
|
blockCommunityLoading: false,
|
||||||
|
purgeCommunityLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleEditCommunity = this.handleEditCommunity.bind(this);
|
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>
|
||||||
|
): void {
|
||||||
|
if (this.props.moderators != nextProps.moderators) {
|
||||||
|
this.setState({
|
||||||
|
showConfirmLeaveModTeam: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.community_view != nextProps.community_view) {
|
||||||
|
this.setState({
|
||||||
|
showEdit: false,
|
||||||
|
showPurgeDialog: false,
|
||||||
|
showRemoveDialog: false,
|
||||||
|
deleteCommunityLoading: false,
|
||||||
|
removeCommunityLoading: false,
|
||||||
|
leaveModTeamLoading: false,
|
||||||
|
followCommunityLoading: false,
|
||||||
|
blockCommunityLoading: false,
|
||||||
|
purgeCommunityLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -81,7 +121,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
communityLanguages={this.props.communityLanguages}
|
communityLanguages={this.props.communityLanguages}
|
||||||
onEdit={this.handleEditCommunity}
|
onUpsertCommunity={this.props.onEditCommunity}
|
||||||
onCancel={this.handleEditCancel}
|
onCancel={this.handleEditCancel}
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableNsfw={this.props.enableNsfw}
|
||||||
/>
|
/>
|
||||||
|
@ -124,30 +164,42 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
communityTitle() {
|
communityTitle() {
|
||||||
let community = this.props.community_view.community;
|
const community = this.props.community_view.community;
|
||||||
let subscribed = this.props.community_view.subscribed;
|
const subscribed = this.props.community_view.subscribed;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
{this.props.showIcon && !community.removed && (
|
{this.props.showIcon && !community.removed && (
|
||||||
<BannerIconHeader icon={community.icon} banner={community.banner} />
|
<BannerIconHeader icon={community.icon} banner={community.banner} />
|
||||||
)}
|
)}
|
||||||
<span className="mr-2">{community.title}</span>
|
<span className="mr-2">
|
||||||
|
<CommunityLink community={community} hideAvatar />
|
||||||
|
</span>
|
||||||
{subscribed === "Subscribed" && (
|
{subscribed === "Subscribed" && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary btn-sm mr-2"
|
className="btn btn-secondary btn-sm mr-2"
|
||||||
onClick={linkEvent(this, this.handleUnsubscribe)}
|
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
||||||
>
|
>
|
||||||
<Icon icon="check" classes="icon-inline text-success mr-1" />
|
{this.state.followCommunityLoading ? (
|
||||||
{i18n.t("joined")}
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon="check" classes="icon-inline text-success mr-1" />
|
||||||
|
{i18n.t("joined")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{subscribed === "Pending" && (
|
{subscribed === "Pending" && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-warning mr-2"
|
className="btn btn-warning mr-2"
|
||||||
onClick={linkEvent(this, this.handleUnsubscribe)}
|
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
||||||
>
|
>
|
||||||
{i18n.t("subscribe_pending")}
|
{this.state.followCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("subscribe_pending")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{community.removed && (
|
{community.removed && (
|
||||||
|
@ -178,8 +230,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
badges() {
|
badges() {
|
||||||
let community_view = this.props.community_view;
|
const community_view = this.props.community_view;
|
||||||
let counts = community_view.counts;
|
const counts = community_view.counts;
|
||||||
return (
|
return (
|
||||||
<ul className="my-1 list-inline">
|
<ul className="my-1 list-inline">
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
|
@ -284,7 +336,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
createPost() {
|
createPost() {
|
||||||
let cv = this.props.community_view;
|
const cv = this.props.community_view;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={`btn btn-secondary btn-block mb-2 ${
|
className={`btn btn-secondary btn-block mb-2 ${
|
||||||
|
@ -298,15 +350,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe() {
|
subscribe() {
|
||||||
let community_view = this.props.community_view;
|
const community_view = this.props.community_view;
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
{community_view.subscribed == "NotSubscribed" && (
|
{community_view.subscribed == "NotSubscribed" && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary btn-block"
|
className="btn btn-secondary btn-block"
|
||||||
onClick={linkEvent(this, this.handleSubscribe)}
|
onClick={linkEvent(this, this.handleFollowCommunity)}
|
||||||
>
|
>
|
||||||
{i18n.t("subscribe")}
|
{this.state.followCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("subscribe")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -314,8 +370,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
blockCommunity() {
|
blockCommunity() {
|
||||||
let community_view = this.props.community_view;
|
const community_view = this.props.community_view;
|
||||||
let blocked = this.props.community_view.blocked;
|
const blocked = this.props.community_view.blocked;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
@ -323,16 +379,24 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
(blocked ? (
|
(blocked ? (
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger btn-block"
|
className="btn btn-danger btn-block"
|
||||||
onClick={linkEvent(this, this.handleUnblock)}
|
onClick={linkEvent(this, this.handleBlockCommunity)}
|
||||||
>
|
>
|
||||||
{i18n.t("unblock_community")}
|
{this.state.blockCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("unblock_community")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger btn-block"
|
className="btn btn-danger btn-block"
|
||||||
onClick={linkEvent(this, this.handleBlock)}
|
onClick={linkEvent(this, this.handleBlockCommunity)}
|
||||||
>
|
>
|
||||||
{i18n.t("block_community")}
|
{this.state.blockCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("block_community")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -340,7 +404,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
description() {
|
description() {
|
||||||
let desc = this.props.community_view.community.description;
|
const desc = this.props.community_view.community.description;
|
||||||
return (
|
return (
|
||||||
desc && (
|
desc && (
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
|
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
|
||||||
|
@ -349,7 +413,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
adminButtons() {
|
adminButtons() {
|
||||||
let community_view = this.props.community_view;
|
const community_view = this.props.community_view;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul className="list-inline mb-1 text-muted font-weight-bold">
|
<ul className="list-inline mb-1 text-muted font-weight-bold">
|
||||||
|
@ -386,7 +450,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<li className="list-inline-item-action">
|
<li className="list-inline-item-action">
|
||||||
<button
|
<button
|
||||||
className="btn btn-link text-muted d-inline-block"
|
className="btn btn-link text-muted d-inline-block"
|
||||||
onClick={linkEvent(this, this.handleLeaveModTeamClick)}
|
onClick={linkEvent(this, this.handleLeaveModTeam)}
|
||||||
>
|
>
|
||||||
{i18n.t("yes")}
|
{i18n.t("yes")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -408,7 +472,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<li className="list-inline-item-action">
|
<li className="list-inline-item-action">
|
||||||
<button
|
<button
|
||||||
className="btn btn-link text-muted d-inline-block"
|
className="btn btn-link text-muted d-inline-block"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteCommunity)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
!community_view.community.deleted
|
!community_view.community.deleted
|
||||||
? i18n.t("delete")
|
? i18n.t("delete")
|
||||||
|
@ -420,12 +484,16 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
: i18n.t("restore")
|
: i18n.t("restore")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.deleteCommunityLoading ? (
|
||||||
icon="trash"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
community_view.community.deleted && "text-danger"
|
<Icon
|
||||||
}`}
|
icon="trash"
|
||||||
/>
|
classes={`icon-inline ${
|
||||||
|
community_view.community.deleted && "text-danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}{" "}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
@ -443,9 +511,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link text-muted d-inline-block"
|
className="btn btn-link text-muted d-inline-block"
|
||||||
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
onClick={linkEvent(this, this.handleRemoveCommunity)}
|
||||||
>
|
>
|
||||||
{i18n.t("restore")}
|
{this.state.removeCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("restore")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
@ -459,7 +531,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{this.state.showRemoveDialog && (
|
{this.state.showRemoveDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleRemoveCommunity)}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="col-form-label" htmlFor="remove-reason">
|
<label className="col-form-label" htmlFor="remove-reason">
|
||||||
{i18n.t("reason")}
|
{i18n.t("reason")}
|
||||||
|
@ -480,13 +552,17 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
{i18n.t("remove_community")}
|
{this.state.removeCommunityLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("remove_community")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
{this.state.showPurgeDialog && (
|
{this.state.showPurgeDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
|
<form onSubmit={linkEvent(this, this.handlePurgeCommunity)}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<PurgeWarning />
|
<PurgeWarning />
|
||||||
</div>
|
</div>
|
||||||
|
@ -504,7 +580,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
{this.state.purgeLoading ? (
|
{this.state.purgeCommunityLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
@ -526,93 +602,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
i.setState({ showEdit: true });
|
i.setState({ showEdit: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditCommunity() {
|
|
||||||
this.setState({ showEdit: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEditCancel() {
|
handleEditCancel() {
|
||||||
this.setState({ showEdit: false });
|
this.setState({ showEdit: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick(i: Sidebar, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
let auth = myAuth();
|
|
||||||
if (auth) {
|
|
||||||
let deleteForm: DeleteCommunity = {
|
|
||||||
community_id: i.props.community_view.community.id,
|
|
||||||
deleted: !i.props.community_view.community.deleted,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShowConfirmLeaveModTeamClick(i: Sidebar) {
|
handleShowConfirmLeaveModTeamClick(i: Sidebar) {
|
||||||
i.setState({ showConfirmLeaveModTeam: true });
|
i.setState({ showConfirmLeaveModTeam: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeaveModTeamClick(i: Sidebar) {
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
let auth = myAuth();
|
|
||||||
if (auth && mui) {
|
|
||||||
let form: AddModToCommunity = {
|
|
||||||
person_id: mui.local_user_view.person.id,
|
|
||||||
community_id: i.props.community_view.community.id,
|
|
||||||
added: false,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.addModToCommunity(form));
|
|
||||||
i.setState({ showConfirmLeaveModTeam: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelLeaveModTeamClick(i: Sidebar) {
|
handleCancelLeaveModTeamClick(i: Sidebar) {
|
||||||
i.setState({ showConfirmLeaveModTeam: false });
|
i.setState({ showConfirmLeaveModTeam: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(i: Sidebar, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
let community_id = i.props.community_view.community.id;
|
|
||||||
let auth = myAuth();
|
|
||||||
if (auth) {
|
|
||||||
let form: FollowCommunity = {
|
|
||||||
community_id,
|
|
||||||
follow: false,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.followCommunity(form));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update myUserInfo
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
|
||||||
mui.follows = mui.follows.filter(i => i.community.id != community_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubscribe(i: Sidebar, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
let community_id = i.props.community_view.community.id;
|
|
||||||
let auth = myAuth();
|
|
||||||
if (auth) {
|
|
||||||
let form: FollowCommunity = {
|
|
||||||
community_id,
|
|
||||||
follow: true,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.followCommunity(form));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update myUserInfo
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
|
||||||
mui.follows.push({
|
|
||||||
community: i.props.community_view.community,
|
|
||||||
follower: mui.local_user_view.person,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get canPost(): boolean {
|
get canPost(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.props.community_view.community.posting_restricted_to_mods ||
|
!this.props.community_view.community.posting_restricted_to_mods ||
|
||||||
|
@ -633,23 +634,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
i.setState({ removeExpires: event.target.value });
|
i.setState({ removeExpires: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModRemoveSubmit(i: Sidebar, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
let auth = myAuth();
|
|
||||||
if (auth) {
|
|
||||||
let removeForm: RemoveCommunity = {
|
|
||||||
community_id: i.props.community_view.community.id,
|
|
||||||
removed: !i.props.community_view.community.removed,
|
|
||||||
reason: i.state.removeReason,
|
|
||||||
expires: getUnixTime(i.state.removeExpires),
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
|
|
||||||
|
|
||||||
i.setState({ showRemoveDialog: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePurgeCommunityShow(i: Sidebar) {
|
handlePurgeCommunityShow(i: Sidebar) {
|
||||||
i.setState({ showPurgeDialog: true, showRemoveDialog: false });
|
i.setState({ showPurgeDialog: true, showRemoveDialog: false });
|
||||||
}
|
}
|
||||||
|
@ -658,48 +642,75 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
i.setState({ purgeReason: event.target.value });
|
i.setState({ purgeReason: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePurgeSubmit(i: Sidebar, event: any) {
|
// TODO Do we need two of these?
|
||||||
event.preventDefault();
|
handleUnfollowCommunity(i: Sidebar) {
|
||||||
|
i.setState({ followCommunityLoading: true });
|
||||||
|
i.props.onFollowCommunity({
|
||||||
|
community_id: i.props.community_view.community.id,
|
||||||
|
follow: false,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let auth = myAuth();
|
handleFollowCommunity(i: Sidebar) {
|
||||||
if (auth) {
|
i.setState({ followCommunityLoading: true });
|
||||||
let form: PurgeCommunity = {
|
i.props.onFollowCommunity({
|
||||||
|
community_id: i.props.community_view.community.id,
|
||||||
|
follow: true,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlockCommunity(i: Sidebar) {
|
||||||
|
i.setState({ blockCommunityLoading: true });
|
||||||
|
i.props.onBlockCommunity({
|
||||||
|
community_id: 0,
|
||||||
|
block: !i.props.community_view.blocked,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLeaveModTeam(i: Sidebar) {
|
||||||
|
const myId = UserService.Instance.myUserInfo?.local_user_view.person.id;
|
||||||
|
if (myId) {
|
||||||
|
i.setState({ leaveModTeamLoading: true });
|
||||||
|
i.props.onLeaveModTeam({
|
||||||
community_id: i.props.community_view.community.id,
|
community_id: i.props.community_view.community.id,
|
||||||
reason: i.state.purgeReason,
|
person_id: 92,
|
||||||
auth,
|
added: false,
|
||||||
};
|
auth: myAuthRequired(),
|
||||||
WebSocketService.Instance.send(wsClient.purgeCommunity(form));
|
});
|
||||||
i.setState({ purgeLoading: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlock(i: Sidebar, event: any) {
|
handleDeleteCommunity(i: Sidebar) {
|
||||||
event.preventDefault();
|
i.setState({ deleteCommunityLoading: true });
|
||||||
let auth = myAuth();
|
i.props.onDeleteCommunity({
|
||||||
if (auth) {
|
community_id: i.props.community_view.community.id,
|
||||||
let blockCommunityForm: BlockCommunity = {
|
deleted: !i.props.community_view.community.deleted,
|
||||||
community_id: i.props.community_view.community.id,
|
auth: myAuthRequired(),
|
||||||
block: true,
|
});
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.blockCommunity(blockCommunityForm)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnblock(i: Sidebar, event: any) {
|
handleRemoveCommunity(i: Sidebar, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let auth = myAuth();
|
i.setState({ removeCommunityLoading: true });
|
||||||
if (auth) {
|
i.props.onRemoveCommunity({
|
||||||
let blockCommunityForm: BlockCommunity = {
|
community_id: i.props.community_view.community.id,
|
||||||
community_id: i.props.community_view.community.id,
|
removed: !i.props.community_view.community.removed,
|
||||||
block: false,
|
reason: i.state.removeReason,
|
||||||
auth,
|
expires: getUnixTime(i.state.removeExpires), // TODO fix this
|
||||||
};
|
auth: myAuthRequired(),
|
||||||
WebSocketService.Instance.send(
|
});
|
||||||
wsClient.blockCommunity(blockCommunityForm)
|
}
|
||||||
);
|
|
||||||
}
|
handlePurgeCommunity(i: Sidebar, event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
i.setState({ purgeCommunityLoading: true });
|
||||||
|
i.props.onPurgeCommunity({
|
||||||
|
community_id: i.props.community_view.community.id,
|
||||||
|
reason: i.state.purgeReason,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
import autosize from "autosize";
|
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
BannedPersonsResponse,
|
BannedPersonsResponse,
|
||||||
GetBannedPersons,
|
CreateCustomEmoji,
|
||||||
|
DeleteCustomEmoji,
|
||||||
|
EditCustomEmoji,
|
||||||
|
EditSite,
|
||||||
GetFederatedInstancesResponse,
|
GetFederatedInstancesResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
PersonView,
|
PersonView,
|
||||||
SiteResponse,
|
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { WebSocketService } from "../../services";
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
isBrowser,
|
fetchThemeList,
|
||||||
myAuth,
|
myAuthRequired,
|
||||||
randomStr,
|
removeFromEmojiDataModel,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
showLocal,
|
showLocal,
|
||||||
toast,
|
toast,
|
||||||
wsClient,
|
updateEmojiDataModel,
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
@ -37,76 +34,92 @@ import { TaglineForm } from "./tagline-form";
|
||||||
|
|
||||||
interface AdminSettingsState {
|
interface AdminSettingsState {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
instancesRes?: GetFederatedInstancesResponse;
|
|
||||||
banned: PersonView[];
|
banned: PersonView[];
|
||||||
loading: boolean;
|
currentTab: string;
|
||||||
leaveAdminTeamLoading: boolean;
|
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||||
|
bannedRes: RequestState<BannedPersonsResponse>;
|
||||||
|
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
||||||
|
themeList: string[];
|
||||||
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminSettings extends Component<any, AdminSettingsState> {
|
export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
private siteConfigTextAreaId = `site-config-${randomStr()}`;
|
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: AdminSettingsState = {
|
state: AdminSettingsState = {
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
banned: [],
|
banned: [],
|
||||||
loading: true,
|
currentTab: "site",
|
||||||
leaveAdminTeamLoading: false,
|
bannedRes: { state: "empty" },
|
||||||
|
instancesRes: { state: "empty" },
|
||||||
|
leaveAdminTeamRes: { state: "empty" },
|
||||||
|
themeList: [],
|
||||||
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.handleEditSite = this.handleEditSite.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.handleEditEmoji = this.handleEditEmoji.bind(this);
|
||||||
|
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
|
||||||
|
this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path == this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
|
const [bannedRes, instancesRes] = this.isoData.routeData;
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
banned: (this.isoData.routeData[0] as BannedPersonsResponse).banned,
|
bannedRes,
|
||||||
instancesRes: this.isoData
|
instancesRes,
|
||||||
.routeData[1] as GetFederatedInstancesResponse,
|
isIsomorphic: true,
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
let cAuth = myAuth();
|
|
||||||
if (cAuth) {
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.getBannedPersons({
|
|
||||||
auth: cAuth,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.getFederatedInstances({ auth: cAuth })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
async fetchData() {
|
||||||
let promises: Promise<any>[] = [];
|
this.setState({
|
||||||
|
bannedRes: { state: "loading" },
|
||||||
|
instancesRes: { state: "loading" },
|
||||||
|
themeList: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const auth = myAuthRequired();
|
||||||
|
|
||||||
|
const [bannedRes, instancesRes, themeList] = await Promise.all([
|
||||||
|
HttpService.client.getBannedPersons({ auth }),
|
||||||
|
HttpService.client.getFederatedInstances({ auth }),
|
||||||
|
fetchThemeList(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
bannedRes,
|
||||||
|
instancesRes,
|
||||||
|
themeList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetchInitialData({
|
||||||
|
auth,
|
||||||
|
client,
|
||||||
|
}: InitialFetchRequest): Promise<any>[] {
|
||||||
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
|
|
||||||
let auth = req.auth;
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
let bannedPersonsForm: GetBannedPersons = { auth };
|
promises.push(client.getBannedPersons({ auth }));
|
||||||
promises.push(req.client.getBannedPersons(bannedPersonsForm));
|
promises.push(client.getFederatedInstances({ auth }));
|
||||||
promises.push(req.client.getFederatedInstances({ auth }));
|
} else {
|
||||||
|
promises.push(
|
||||||
|
Promise.resolve({ state: "empty" }),
|
||||||
|
Promise.resolve({ state: "empty" })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
if (isBrowser()) {
|
if (!this.state.isIsomorphic) {
|
||||||
var textarea: any = document.getElementById(this.siteConfigTextAreaId);
|
await this.fetchData();
|
||||||
autosize(textarea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,74 +130,80 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const federationData =
|
||||||
|
this.state.instancesRes.state === "success"
|
||||||
|
? this.state.instancesRes.data.federated_instances
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
/>
|
/>
|
||||||
{this.state.loading ? (
|
<Tabs
|
||||||
<h5>
|
tabs={[
|
||||||
<Spinner large />
|
{
|
||||||
</h5>
|
key: "site",
|
||||||
) : (
|
label: i18n.t("site"),
|
||||||
<Tabs
|
getNode: () => (
|
||||||
tabs={[
|
<div className="row">
|
||||||
{
|
<div className="col-12 col-md-6">
|
||||||
key: "site",
|
<SiteForm
|
||||||
label: i18n.t("site"),
|
showLocal={showLocal(this.isoData)}
|
||||||
getNode: () => (
|
allowedInstances={federationData?.allowed}
|
||||||
<div className="row">
|
blockedInstances={federationData?.blocked}
|
||||||
<div className="col-12 col-md-6">
|
onSaveSite={this.handleEditSite}
|
||||||
<SiteForm
|
siteRes={this.state.siteRes}
|
||||||
siteRes={this.state.siteRes}
|
themeList={this.state.themeList}
|
||||||
instancesRes={this.state.instancesRes}
|
/>
|
||||||
showLocal={showLocal(this.isoData)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6">
|
|
||||||
{this.admins()}
|
|
||||||
{this.bannedUsers()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
<div className="col-12 col-md-6">
|
||||||
},
|
{this.admins()}
|
||||||
{
|
{this.bannedUsers()}
|
||||||
key: "rate_limiting",
|
</div>
|
||||||
label: "Rate Limiting",
|
</div>
|
||||||
getNode: () => (
|
),
|
||||||
<RateLimitForm
|
},
|
||||||
localSiteRateLimit={
|
{
|
||||||
this.state.siteRes.site_view.local_site_rate_limit
|
key: "rate_limiting",
|
||||||
}
|
label: "Rate Limiting",
|
||||||
applicationQuestion={
|
getNode: () => (
|
||||||
this.state.siteRes.site_view.local_site
|
<RateLimitForm
|
||||||
.application_question
|
rateLimits={
|
||||||
}
|
this.state.siteRes.site_view.local_site_rate_limit
|
||||||
|
}
|
||||||
|
onSaveSite={this.handleEditSite}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "taglines",
|
||||||
|
label: i18n.t("taglines"),
|
||||||
|
getNode: () => (
|
||||||
|
<div className="row">
|
||||||
|
<TaglineForm
|
||||||
|
taglines={this.state.siteRes.taglines}
|
||||||
|
onSaveSite={this.handleEditSite}
|
||||||
/>
|
/>
|
||||||
),
|
</div>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
key: "taglines",
|
{
|
||||||
label: i18n.t("taglines"),
|
key: "emojis",
|
||||||
getNode: () => (
|
label: i18n.t("emojis"),
|
||||||
<div className="row">
|
getNode: () => (
|
||||||
<TaglineForm siteRes={this.state.siteRes} />
|
<div className="row">
|
||||||
</div>
|
<EmojiForm
|
||||||
),
|
onCreate={this.handleCreateEmoji}
|
||||||
},
|
onDelete={this.handleDeleteEmoji}
|
||||||
{
|
onEdit={this.handleEditEmoji}
|
||||||
key: "emojis",
|
/>
|
||||||
label: i18n.t("emojis"),
|
</div>
|
||||||
getNode: () => (
|
),
|
||||||
<div className="row">
|
},
|
||||||
<EmojiForm />
|
]}
|
||||||
</div>
|
/>
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -211,7 +230,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
onClick={linkEvent(this, this.handleLeaveAdminTeam)}
|
onClick={linkEvent(this, this.handleLeaveAdminTeam)}
|
||||||
className="btn btn-danger mb-2"
|
className="btn btn-danger mb-2"
|
||||||
>
|
>
|
||||||
{this.state.leaveAdminTeamLoading ? (
|
{this.state.leaveAdminTeamRes.state == "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
i18n.t("leave_admin_team")
|
i18n.t("leave_admin_team")
|
||||||
|
@ -221,52 +240,83 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bannedUsers() {
|
bannedUsers() {
|
||||||
return (
|
switch (this.state.bannedRes.state) {
|
||||||
<>
|
case "loading":
|
||||||
<h5>{i18n.t("banned_users")}</h5>
|
return (
|
||||||
<ul className="list-unstyled">
|
<h5>
|
||||||
{this.state.banned.map(banned => (
|
<Spinner large />
|
||||||
<li key={banned.person.id} className="list-inline-item">
|
</h5>
|
||||||
<PersonListing person={banned.person} />
|
);
|
||||||
</li>
|
case "success": {
|
||||||
))}
|
const bans = this.state.bannedRes.data.banned;
|
||||||
</ul>
|
return (
|
||||||
</>
|
<>
|
||||||
);
|
<h5>{i18n.t("banned_users")}</h5>
|
||||||
}
|
<ul className="list-unstyled">
|
||||||
|
{bans.map(banned => (
|
||||||
handleLeaveAdminTeam(i: AdminSettings) {
|
<li key={banned.person.id} className="list-inline-item">
|
||||||
let auth = myAuth();
|
<PersonListing person={banned.person} />
|
||||||
if (auth) {
|
</li>
|
||||||
i.setState({ leaveAdminTeamLoading: true });
|
))}
|
||||||
WebSocketService.Instance.send(wsClient.leaveAdmin({ auth }));
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
async handleEditSite(form: EditSite) {
|
||||||
let op = wsUserOp(msg);
|
const editRes = await HttpService.client.editSite(form);
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
if (editRes.state === "success") {
|
||||||
toast(i18n.t(msg.error), "danger");
|
this.setState(s => {
|
||||||
this.context.router.history.push("/");
|
s.siteRes.site_view = editRes.data.site_view;
|
||||||
this.setState({ loading: false });
|
// TODO: Where to get taglines from?
|
||||||
return;
|
s.siteRes.taglines = editRes.data.taglines;
|
||||||
} else if (op == UserOperation.EditSite) {
|
return s;
|
||||||
let data = wsJsonToRes<SiteResponse>(msg);
|
});
|
||||||
this.setState(s => ((s.siteRes.site_view = data.site_view), s));
|
|
||||||
toast(i18n.t("site_saved"));
|
toast(i18n.t("site_saved"));
|
||||||
} else if (op == UserOperation.GetBannedPersons) {
|
}
|
||||||
let data = wsJsonToRes<BannedPersonsResponse>(msg);
|
|
||||||
this.setState({ banned: data.banned, loading: false });
|
return editRes;
|
||||||
} else if (op == UserOperation.LeaveAdmin) {
|
}
|
||||||
let data = wsJsonToRes<GetSiteResponse>(msg);
|
|
||||||
this.setState(s => ((s.siteRes.site_view = data.site_view), s));
|
handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
|
||||||
this.setState({ leaveAdminTeamLoading: false });
|
i.ctx.setState({ currentTab: i.tab });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleLeaveAdminTeam(i: AdminSettings) {
|
||||||
|
i.setState({ leaveAdminTeamRes: { state: "loading" } });
|
||||||
|
this.setState({
|
||||||
|
leaveAdminTeamRes: await HttpService.client.leaveAdmin({
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.leaveAdminTeamRes.state === "success") {
|
||||||
toast(i18n.t("left_admin_team"));
|
toast(i18n.t("left_admin_team"));
|
||||||
this.context.router.history.push("/");
|
this.context.router.history.replace("/");
|
||||||
} else if (op == UserOperation.GetFederatedInstances) {
|
}
|
||||||
let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
|
}
|
||||||
this.setState({ instancesRes: data });
|
|
||||||
|
async handleEditEmoji(form: EditCustomEmoji) {
|
||||||
|
const res = await HttpService.client.editCustomEmoji(form);
|
||||||
|
if (res.state === "success") {
|
||||||
|
updateEmojiDataModel(res.data.custom_emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteEmoji(form: DeleteCustomEmoji) {
|
||||||
|
const res = await HttpService.client.deleteCustomEmoji(form);
|
||||||
|
if (res.state === "success") {
|
||||||
|
removeFromEmojiDataModel(res.data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateEmoji(form: CreateCustomEmoji) {
|
||||||
|
const res = await HttpService.client.createCustomEmoji(form);
|
||||||
|
if (res.state === "success") {
|
||||||
|
updateEmojiDataModel(res.data.custom_emoji);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,30 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
CreateCustomEmoji,
|
CreateCustomEmoji,
|
||||||
CustomEmojiResponse,
|
|
||||||
DeleteCustomEmoji,
|
DeleteCustomEmoji,
|
||||||
DeleteCustomEmojiResponse,
|
|
||||||
EditCustomEmoji,
|
EditCustomEmoji,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { HttpService } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
customEmojisLookup,
|
customEmojisLookup,
|
||||||
isBrowser,
|
myAuthRequired,
|
||||||
myAuth,
|
|
||||||
pictrsDeleteToast,
|
pictrsDeleteToast,
|
||||||
removeFromEmojiDataModel,
|
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
toast,
|
||||||
updateEmojiDataModel,
|
|
||||||
uploadImage,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { EmojiMart } from "../common/emoji-mart";
|
import { EmojiMart } from "../common/emoji-mart";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { Paginator } from "../common/paginator";
|
import { Paginator } from "../common/paginator";
|
||||||
|
|
||||||
|
interface EmojiFormProps {
|
||||||
|
onEdit(form: EditCustomEmoji): void;
|
||||||
|
onCreate(form: CreateCustomEmoji): void;
|
||||||
|
onDelete(form: DeleteCustomEmoji): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface EmojiFormState {
|
interface EmojiFormState {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
customEmojis: CustomEmojiViewForm[];
|
customEmojis: CustomEmojiViewForm[];
|
||||||
|
@ -49,9 +43,8 @@ interface CustomEmojiViewForm {
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmojiForm extends Component<any, EmojiFormState> {
|
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription: Subscription | undefined;
|
|
||||||
private itemsPerPage = 15;
|
private itemsPerPage = 15;
|
||||||
private emptyState: EmojiFormState = {
|
private emptyState: EmojiFormState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -75,20 +68,12 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
}
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return i18n.t("custom_emojis");
|
return i18n.t("custom_emojis");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
|
@ -232,7 +217,7 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
"btn btn-link btn-animate"
|
"btn btn-link btn-animate"
|
||||||
}
|
}
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
{ form: this, cv: cv },
|
{ i: this, cv: cv },
|
||||||
this.handleEditEmojiClick
|
this.handleEditEmojiClick
|
||||||
)}
|
)}
|
||||||
data-tippy-content={i18n.t("save")}
|
data-tippy-content={i18n.t("save")}
|
||||||
|
@ -253,7 +238,7 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted"
|
className="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
{ form: this, index: index, cv: cv },
|
{ i: this, index: index, cv: cv },
|
||||||
this.handleDeleteEmojiClick
|
this.handleDeleteEmojiClick
|
||||||
)}
|
)}
|
||||||
data-tippy-content={i18n.t("delete")}
|
data-tippy-content={i18n.t("delete")}
|
||||||
|
@ -325,10 +310,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props: { form: EmojiForm; index: number },
|
props: { form: EmojiForm; index: number },
|
||||||
event: any
|
event: any
|
||||||
) {
|
) {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...props.form.state.customEmojis];
|
||||||
let pagedIndex =
|
const pagedIndex =
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||||
let item = {
|
const item = {
|
||||||
...props.form.state.customEmojis[pagedIndex],
|
...props.form.state.customEmojis[pagedIndex],
|
||||||
category: event.target.value,
|
category: event.target.value,
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -341,10 +326,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props: { form: EmojiForm; index: number },
|
props: { form: EmojiForm; index: number },
|
||||||
event: any
|
event: any
|
||||||
) {
|
) {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...props.form.state.customEmojis];
|
||||||
let pagedIndex =
|
const pagedIndex =
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||||
let item = {
|
const item = {
|
||||||
...props.form.state.customEmojis[pagedIndex],
|
...props.form.state.customEmojis[pagedIndex],
|
||||||
shortcode: event.target.value,
|
shortcode: event.target.value,
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -357,10 +342,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props: { form: EmojiForm; index: number; overrideValue: string | null },
|
props: { form: EmojiForm; index: number; overrideValue: string | null },
|
||||||
event: any
|
event: any
|
||||||
) {
|
) {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...props.form.state.customEmojis];
|
||||||
let pagedIndex =
|
const pagedIndex =
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||||
let item = {
|
const item = {
|
||||||
...props.form.state.customEmojis[pagedIndex],
|
...props.form.state.customEmojis[pagedIndex],
|
||||||
image_url: props.overrideValue ?? event.target.value,
|
image_url: props.overrideValue ?? event.target.value,
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -373,10 +358,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props: { form: EmojiForm; index: number },
|
props: { form: EmojiForm; index: number },
|
||||||
event: any
|
event: any
|
||||||
) {
|
) {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...props.form.state.customEmojis];
|
||||||
let pagedIndex =
|
const pagedIndex =
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||||
let item = {
|
const item = {
|
||||||
...props.form.state.customEmojis[pagedIndex],
|
...props.form.state.customEmojis[pagedIndex],
|
||||||
alt_text: event.target.value,
|
alt_text: event.target.value,
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -389,10 +374,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props: { form: EmojiForm; index: number },
|
props: { form: EmojiForm; index: number },
|
||||||
event: any
|
event: any
|
||||||
) {
|
) {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...props.form.state.customEmojis];
|
||||||
let pagedIndex =
|
const pagedIndex =
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||||
let item = {
|
const item = {
|
||||||
...props.form.state.customEmojis[pagedIndex],
|
...props.form.state.customEmojis[pagedIndex],
|
||||||
keywords: event.target.value,
|
keywords: event.target.value,
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -401,60 +386,56 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
props.form.setState({ customEmojis: custom_emojis });
|
props.form.setState({ customEmojis: custom_emojis });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteEmojiClick(props: {
|
handleDeleteEmojiClick(d: {
|
||||||
form: EmojiForm;
|
i: EmojiForm;
|
||||||
index: number;
|
index: number;
|
||||||
cv: CustomEmojiViewForm;
|
cv: CustomEmojiViewForm;
|
||||||
}) {
|
}) {
|
||||||
let pagedIndex =
|
const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
|
||||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
if (d.cv.id != 0) {
|
||||||
if (props.cv.id != 0) {
|
d.i.props.onDelete({
|
||||||
const deleteForm: DeleteCustomEmoji = {
|
id: d.cv.id,
|
||||||
id: props.cv.id,
|
auth: myAuthRequired(),
|
||||||
auth: myAuth() ?? "",
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
|
|
||||||
} else {
|
} else {
|
||||||
let custom_emojis = [...props.form.state.customEmojis];
|
const custom_emojis = [...d.i.state.customEmojis];
|
||||||
custom_emojis.splice(Number(pagedIndex), 1);
|
custom_emojis.splice(Number(pagedIndex), 1);
|
||||||
props.form.setState({ customEmojis: custom_emojis });
|
d.i.setState({ customEmojis: custom_emojis });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
|
handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
|
||||||
const keywords = props.cv.keywords
|
const keywords = d.cv.keywords
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter(x => x.length > 0) as string[];
|
.filter(x => x.length > 0) as string[];
|
||||||
const uniqueKeywords = Array.from(new Set(keywords));
|
const uniqueKeywords = Array.from(new Set(keywords));
|
||||||
if (props.cv.id != 0) {
|
if (d.cv.id != 0) {
|
||||||
const editForm: EditCustomEmoji = {
|
d.i.props.onEdit({
|
||||||
id: props.cv.id,
|
id: d.cv.id,
|
||||||
category: props.cv.category,
|
category: d.cv.category,
|
||||||
image_url: props.cv.image_url,
|
image_url: d.cv.image_url,
|
||||||
alt_text: props.cv.alt_text,
|
alt_text: d.cv.alt_text,
|
||||||
keywords: uniqueKeywords,
|
keywords: uniqueKeywords,
|
||||||
auth: myAuth() ?? "",
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
|
|
||||||
} else {
|
} else {
|
||||||
const createForm: CreateCustomEmoji = {
|
d.i.props.onCreate({
|
||||||
category: props.cv.category,
|
category: d.cv.category,
|
||||||
shortcode: props.cv.shortcode,
|
shortcode: d.cv.shortcode,
|
||||||
image_url: props.cv.image_url,
|
image_url: d.cv.image_url,
|
||||||
alt_text: props.cv.alt_text,
|
alt_text: d.cv.alt_text,
|
||||||
keywords: uniqueKeywords,
|
keywords: uniqueKeywords,
|
||||||
auth: myAuth() ?? "",
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddEmojiClick(form: EmojiForm, event: any) {
|
handleAddEmojiClick(form: EmojiForm, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let custom_emojis = [...form.state.customEmojis];
|
const custom_emojis = [...form.state.customEmojis];
|
||||||
const page =
|
const page =
|
||||||
1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
|
1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
|
||||||
let item: CustomEmojiViewForm = {
|
const item: CustomEmojiViewForm = {
|
||||||
id: 0,
|
id: 0,
|
||||||
shortcode: "",
|
shortcode: "",
|
||||||
alt_text: "",
|
alt_text: "",
|
||||||
|
@ -477,26 +458,26 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
file = event;
|
file = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadImage(file)
|
HttpService.client.uploadImage({ image: file }).then(res => {
|
||||||
.then(res => {
|
console.log("pictrs upload:");
|
||||||
console.log("pictrs upload:");
|
console.log(res);
|
||||||
console.log(res);
|
if (res.state === "success") {
|
||||||
if (res.msg === "ok") {
|
if (res.data.msg === "ok") {
|
||||||
pictrsDeleteToast(file.name, res.delete_url as string);
|
pictrsDeleteToast(file.name, res.data.delete_url as string);
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
let hash = res.files?.at(0)?.file;
|
const hash = res.data.files?.at(0)?.file;
|
||||||
let url = `${res.url}/${hash}`;
|
const url = `${res.data.url}/${hash}`;
|
||||||
props.form.handleEmojiImageUrlChange(
|
props.form.handleEmojiImageUrlChange(
|
||||||
{ form: props.form, index: props.index, overrideValue: url },
|
{ form: props.form, index: props.index, overrideValue: url },
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
} else if (res.state === "failed") {
|
||||||
.catch(error => {
|
console.error(res.msg);
|
||||||
console.error(error);
|
toast(res.msg, "danger");
|
||||||
toast(error, "danger");
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
configurePicker(): any {
|
configurePicker(): any {
|
||||||
|
@ -506,51 +487,4 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
||||||
dynamicWidth: true,
|
dynamicWidth: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.context.router.history.push("/");
|
|
||||||
this.setState({ loading: false });
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.CreateCustomEmoji) {
|
|
||||||
let data = wsJsonToRes<CustomEmojiResponse>(msg);
|
|
||||||
const custom_emoji_view = data.custom_emoji;
|
|
||||||
updateEmojiDataModel(custom_emoji_view);
|
|
||||||
let currentEmojis = this.state.customEmojis;
|
|
||||||
let newEmojiIndex = currentEmojis.findIndex(
|
|
||||||
x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
|
|
||||||
);
|
|
||||||
currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
|
|
||||||
currentEmojis[newEmojiIndex].changed = false;
|
|
||||||
this.setState({ customEmojis: currentEmojis });
|
|
||||||
toast(i18n.t("saved_emoji"));
|
|
||||||
this.setState({ loading: false });
|
|
||||||
} else if (op == UserOperation.EditCustomEmoji) {
|
|
||||||
let data = wsJsonToRes<CustomEmojiResponse>(msg);
|
|
||||||
const custom_emoji_view = data.custom_emoji;
|
|
||||||
updateEmojiDataModel(data.custom_emoji);
|
|
||||||
let currentEmojis = this.state.customEmojis;
|
|
||||||
let newEmojiIndex = currentEmojis.findIndex(
|
|
||||||
x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
|
|
||||||
);
|
|
||||||
currentEmojis[newEmojiIndex].changed = false;
|
|
||||||
this.setState({ customEmojis: currentEmojis });
|
|
||||||
toast(i18n.t("saved_emoji"));
|
|
||||||
this.setState({ loading: false });
|
|
||||||
} else if (op == UserOperation.DeleteCustomEmoji) {
|
|
||||||
let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
|
|
||||||
if (data.success) {
|
|
||||||
removeFromEmojiDataModel(data.id);
|
|
||||||
let custom_emojis = [
|
|
||||||
...this.state.customEmojis.filter(x => x.id != data.id),
|
|
||||||
];
|
|
||||||
this.setState({ customEmojis: custom_emojis });
|
|
||||||
toast(i18n.t("deleted_emoji"));
|
|
||||||
}
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,106 +3,113 @@ import {
|
||||||
GetFederatedInstancesResponse,
|
GetFederatedInstancesResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Instance,
|
Instance,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { WebSocketService } from "../../services";
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
import {
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
isBrowser,
|
import { relTags, setIsoData } from "../../utils";
|
||||||
relTags,
|
|
||||||
setIsoData,
|
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
|
import { Spinner } from "../common/icon";
|
||||||
|
|
||||||
interface InstancesState {
|
interface InstancesState {
|
||||||
|
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
instancesRes?: GetFederatedInstancesResponse;
|
isIsomorphic: boolean;
|
||||||
loading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Instances extends Component<any, InstancesState> {
|
export class Instances extends Component<any, InstancesState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
state: InstancesState = {
|
state: InstancesState = {
|
||||||
|
instancesRes: { state: "empty" },
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
loading: true,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path == this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
instancesRes: this.isoData
|
instancesRes: this.isoData.routeData[0],
|
||||||
.routeData[0] as GetFederatedInstancesResponse,
|
isIsomorphic: true,
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
WebSocketService.Instance.send(wsClient.getFederatedInstances({}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
async componentDidMount() {
|
||||||
let promises: Promise<any>[] = [];
|
if (!this.state.isIsomorphic) {
|
||||||
|
await this.fetchInstances();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
promises.push(req.client.getFederatedInstances({}));
|
async fetchInstances() {
|
||||||
|
this.setState({
|
||||||
|
instancesRes: { state: "loading" },
|
||||||
|
});
|
||||||
|
|
||||||
return promises;
|
this.setState({
|
||||||
|
instancesRes: await HttpService.client.getFederatedInstances({}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetchInitialData(
|
||||||
|
req: InitialFetchRequest
|
||||||
|
): Promise<RequestState<any>>[] {
|
||||||
|
return [req.client.getFederatedInstances({})];
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
|
return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
renderInstances() {
|
||||||
if (isBrowser()) {
|
switch (this.state.instancesRes.state) {
|
||||||
this.subscription?.unsubscribe();
|
case "loading":
|
||||||
|
return (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
case "success": {
|
||||||
|
const instances = this.state.instancesRes.data.federated_instances;
|
||||||
|
return instances ? (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<h5>{i18n.t("linked_instances")}</h5>
|
||||||
|
{this.itemList(instances.linked)}
|
||||||
|
</div>
|
||||||
|
{instances.allowed && instances.allowed.length > 0 && (
|
||||||
|
<div className="col-md-6">
|
||||||
|
<h5>{i18n.t("allowed_instances")}</h5>
|
||||||
|
{this.itemList(instances.allowed)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{instances.blocked && instances.blocked.length > 0 && (
|
||||||
|
<div className="col-md-6">
|
||||||
|
<h5>{i18n.t("blocked_instances")}</h5>
|
||||||
|
{this.itemList(instances.blocked)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let federated_instances = this.state.instancesRes?.federated_instances;
|
return (
|
||||||
return federated_instances ? (
|
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
/>
|
/>
|
||||||
<div className="row">
|
{this.renderInstances()}
|
||||||
<div className="col-md-6">
|
|
||||||
<h5>{i18n.t("linked_instances")}</h5>
|
|
||||||
{this.itemList(federated_instances.linked)}
|
|
||||||
</div>
|
|
||||||
{federated_instances.allowed &&
|
|
||||||
federated_instances.allowed.length > 0 && (
|
|
||||||
<div className="col-md-6">
|
|
||||||
<h5>{i18n.t("allowed_instances")}</h5>
|
|
||||||
{this.itemList(federated_instances.allowed)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{federated_instances.blocked &&
|
|
||||||
federated_instances.blocked.length > 0 && (
|
|
||||||
<div className="col-md-6">
|
|
||||||
<h5>{i18n.t("blocked_instances")}</h5>
|
|
||||||
{this.itemList(federated_instances.blocked)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,17 +143,4 @@ export class Instances extends Component<any, InstancesState> {
|
||||||
<div>{i18n.t("none_found")}</div>
|
<div>{i18n.t("none_found")}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.context.router.history.push("/");
|
|
||||||
this.setState({ loading: false });
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.GetFederatedInstances) {
|
|
||||||
let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
|
|
||||||
this.setState({ loading: false, instancesRes: data });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class Legal extends Component<any, LegalState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let legal = this.state.siteRes.site_view.local_site.legal_information;
|
const legal = this.state.siteRes.site_view.local_site.legal_information;
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
|
|
|
@ -1,58 +1,35 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
||||||
GetSiteResponse,
|
|
||||||
Login as LoginI,
|
|
||||||
LoginResponse,
|
|
||||||
PasswordReset,
|
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
import {
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
isBrowser,
|
import { isBrowser, myAuth, setIsoData, toast, validEmail } from "../../utils";
|
||||||
setIsoData,
|
|
||||||
toast,
|
|
||||||
validEmail,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
loginRes: RequestState<LoginResponse>;
|
||||||
form: {
|
form: {
|
||||||
username_or_email?: string;
|
username_or_email?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
totp_2fa_token?: string;
|
totp_2fa_token?: string;
|
||||||
};
|
};
|
||||||
loginLoading: boolean;
|
|
||||||
showTotp: boolean;
|
showTotp: boolean;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
export class Login extends Component<any, State> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
loginRes: { state: "empty" },
|
||||||
form: {},
|
form: {},
|
||||||
loginLoading: false,
|
|
||||||
showTotp: false,
|
showTotp: false,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
if (isBrowser()) {
|
|
||||||
WebSocketService.Instance.send(wsClient.getCaptcha({}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -62,12 +39,6 @@ export class Login extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return `${i18n.t("login")} - ${this.state.siteRes.site_view.site.name}`;
|
return `${i18n.t("login")} - ${this.state.siteRes.site_view.site.name}`;
|
||||||
}
|
}
|
||||||
|
@ -169,7 +140,11 @@ export class Login extends Component<any, State> {
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
{this.state.loginLoading ? <Spinner /> : i18n.t("login")}
|
{this.state.loginRes.state == "loading" ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("login")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -178,25 +153,49 @@ export class Login extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoginSubmit(i: Login, event: any) {
|
async handleLoginSubmit(i: Login, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ loginLoading: true });
|
const { password, totp_2fa_token, username_or_email } = i.state.form;
|
||||||
let lForm = i.state.form;
|
|
||||||
let username_or_email = lForm.username_or_email;
|
|
||||||
let password = lForm.password;
|
|
||||||
let totp_2fa_token = lForm.totp_2fa_token;
|
|
||||||
if (username_or_email && password) {
|
if (username_or_email && password) {
|
||||||
let form: LoginI = {
|
i.setState({ loginRes: { state: "loading" } });
|
||||||
|
|
||||||
|
const loginRes = await HttpService.client.login({
|
||||||
username_or_email,
|
username_or_email,
|
||||||
password,
|
password,
|
||||||
totp_2fa_token,
|
totp_2fa_token,
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.login(form));
|
switch (loginRes.state) {
|
||||||
|
case "failed": {
|
||||||
|
if (loginRes.msg === "missing_totp_token") {
|
||||||
|
i.setState({ showTotp: true });
|
||||||
|
toast(i18n.t("enter_two_factor_code"), "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ loginRes: { state: "empty" } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "success": {
|
||||||
|
UserService.Instance.login(loginRes.data);
|
||||||
|
const site = await HttpService.client.getSite({
|
||||||
|
auth: myAuth(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (site.state === "success") {
|
||||||
|
UserService.Instance.myUserInfo = site.data.my_user;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.props.history.replace("/");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoginUsernameChange(i: Login, event: any) {
|
handleLoginUsernameChange(i: Login, event: any) {
|
||||||
i.state.form.username_or_email = event.target.value;
|
i.state.form.username_or_email = event.target.value.trim();
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,40 +209,13 @@ export class Login extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePasswordReset(i: Login, event: any) {
|
async handlePasswordReset(i: Login, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let email = i.state.form.username_or_email;
|
const email = i.state.form.username_or_email;
|
||||||
if (email) {
|
if (email) {
|
||||||
let resetForm: PasswordReset = { email };
|
const res = await HttpService.client.passwordReset({ email });
|
||||||
WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
|
if (res.state == "success") {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
// If the error comes back that the token is missing, show the TOTP field
|
|
||||||
if (msg.error == "missing_totp_token") {
|
|
||||||
this.setState({ showTotp: true, loginLoading: false });
|
|
||||||
toast(i18n.t("enter_two_factor_code"));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ form: {}, loginLoading: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (op == UserOperation.Login) {
|
|
||||||
let data = wsJsonToRes<LoginResponse>(msg);
|
|
||||||
UserService.Instance.login(data);
|
|
||||||
this.props.history.push("/");
|
|
||||||
location.reload();
|
|
||||||
} else if (op == UserOperation.PasswordReset) {
|
|
||||||
toast(i18n.t("reset_password_mail_sent"));
|
toast(i18n.t("reset_password_mail_sent"));
|
||||||
} else if (op == UserOperation.GetSite) {
|
|
||||||
let data = wsJsonToRes<GetSiteResponse>(msg);
|
|
||||||
this.setState({ siteRes: data });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Component, FormEventHandler, linkEvent } from "inferno";
|
import { Component, FormEventHandler, linkEvent } from "inferno";
|
||||||
import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
|
import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { capitalizeFirstLetter, myAuthRequired } from "../../utils";
|
||||||
import { capitalizeFirstLetter, myAuth, wsClient } from "../../utils";
|
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import Tabs from "../common/tabs";
|
import Tabs from "../common/tabs";
|
||||||
|
|
||||||
|
@ -23,8 +22,8 @@ interface RateLimitsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RateLimitFormProps {
|
interface RateLimitFormProps {
|
||||||
localSiteRateLimit: LocalSiteRateLimit;
|
rateLimits: LocalSiteRateLimit;
|
||||||
applicationQuestion?: string;
|
onSaveSite(form: EditSite): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RateLimitFormState {
|
interface RateLimitFormState {
|
||||||
|
@ -107,18 +106,19 @@ function handlePerSecondChange(
|
||||||
|
|
||||||
function submitRateLimitForm(i: RateLimitsForm, event: any) {
|
function submitRateLimitForm(i: RateLimitsForm, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const auth = myAuth() ?? "TODO";
|
const auth = myAuthRequired();
|
||||||
const form: EditSite = Object.entries(i.state.form).reduce(
|
const form: EditSite = Object.entries(i.state.form).reduce(
|
||||||
(acc, [key, val]) => {
|
(acc, [key, val]) => {
|
||||||
acc[`rate_limit_${key}`] = val;
|
acc[`rate_limit_${key}`] = val;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{ auth, application_question: i.props.applicationQuestion }
|
{
|
||||||
|
auth,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
i.setState({ loading: true });
|
i.setState({ loading: true });
|
||||||
|
i.props.onSaveSite(form);
|
||||||
WebSocketService.Instance.send(wsClient.editSite(form));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RateLimitsForm extends Component<
|
export default class RateLimitsForm extends Component<
|
||||||
|
@ -127,43 +127,10 @@ export default class RateLimitsForm extends Component<
|
||||||
> {
|
> {
|
||||||
state: RateLimitFormState = {
|
state: RateLimitFormState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
form: {},
|
form: this.props.rateLimits,
|
||||||
};
|
};
|
||||||
constructor(props: RateLimitFormProps, context) {
|
constructor(props: RateLimitFormProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
const {
|
|
||||||
comment,
|
|
||||||
comment_per_second,
|
|
||||||
image,
|
|
||||||
image_per_second,
|
|
||||||
message,
|
|
||||||
message_per_second,
|
|
||||||
post,
|
|
||||||
post_per_second,
|
|
||||||
register,
|
|
||||||
register_per_second,
|
|
||||||
search,
|
|
||||||
search_per_second,
|
|
||||||
} = props.localSiteRateLimit;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
form: {
|
|
||||||
comment,
|
|
||||||
comment_per_second,
|
|
||||||
image,
|
|
||||||
image_per_second,
|
|
||||||
message,
|
|
||||||
message_per_second,
|
|
||||||
post,
|
|
||||||
post_per_second,
|
|
||||||
register,
|
|
||||||
register_per_second,
|
|
||||||
search,
|
|
||||||
search_per_second,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -210,15 +177,4 @@ export default class RateLimitsForm extends Component<
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate({ localSiteRateLimit }: RateLimitFormProps) {
|
|
||||||
if (
|
|
||||||
this.state.loading &&
|
|
||||||
Object.entries(localSiteRateLimit).some(
|
|
||||||
([key, val]) => this.state.form[key] !== val
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { Helmet } from "inferno-helmet";
|
import { Helmet } from "inferno-helmet";
|
||||||
import {
|
import {
|
||||||
|
CreateSite,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
Register,
|
Register,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { delay, retryWhen, take } from "rxjs/operators";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
import { setIsoData, toast, wsClient } from "../../utils";
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
|
import { fetchThemeList, setIsoData } from "../../utils";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import { SiteForm } from "./site-form";
|
import { SiteForm } from "./site-form";
|
||||||
|
|
||||||
|
@ -29,37 +26,32 @@ interface State {
|
||||||
answer?: string;
|
answer?: string;
|
||||||
};
|
};
|
||||||
doneRegisteringUser: boolean;
|
doneRegisteringUser: boolean;
|
||||||
userLoading: boolean;
|
registerRes: RequestState<LoginResponse>;
|
||||||
|
themeList: string[];
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Setup extends Component<any, State> {
|
export class Setup extends Component<any, State> {
|
||||||
private subscription: Subscription;
|
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
registerRes: { state: "empty" },
|
||||||
|
themeList: [],
|
||||||
form: {
|
form: {
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
},
|
},
|
||||||
doneRegisteringUser: !!UserService.Instance.myUserInfo,
|
doneRegisteringUser: !!UserService.Instance.myUserInfo,
|
||||||
userLoading: false,
|
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.handleCreateSite = this.handleCreateSite.bind(this);
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
|
||||||
.subscribe(
|
|
||||||
msg => this.parseMessage(msg),
|
|
||||||
err => console.error(err),
|
|
||||||
() => console.log("complete")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentDidMount() {
|
||||||
this.subscription.unsubscribe();
|
this.setState({ themeList: await fetchThemeList() });
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
|
@ -76,7 +68,12 @@ export class Setup extends Component<any, State> {
|
||||||
{!this.state.doneRegisteringUser ? (
|
{!this.state.doneRegisteringUser ? (
|
||||||
this.registerUser()
|
this.registerUser()
|
||||||
) : (
|
) : (
|
||||||
<SiteForm siteRes={this.state.siteRes} showLocal />
|
<SiteForm
|
||||||
|
showLocal
|
||||||
|
onSaveSite={this.handleCreateSite}
|
||||||
|
siteRes={this.state.siteRes}
|
||||||
|
themeList={this.state.themeList}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -161,7 +158,11 @@ export class Setup extends Component<any, State> {
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
{this.state.userLoading ? <Spinner /> : i18n.t("sign_up")}
|
{this.state.registerRes.state == "loading" ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
i18n.t("sign_up")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,29 +170,58 @@ export class Setup extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegisterSubmit(i: Setup, event: any) {
|
async handleRegisterSubmit(i: Setup, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ userLoading: true });
|
i.setState({ registerRes: { state: "loading" } });
|
||||||
event.preventDefault();
|
const {
|
||||||
let cForm = i.state.form;
|
username,
|
||||||
if (cForm.username && cForm.password && cForm.password_verify) {
|
password_verify,
|
||||||
let form: Register = {
|
password,
|
||||||
username: cForm.username,
|
email,
|
||||||
password: cForm.password,
|
show_nsfw,
|
||||||
password_verify: cForm.password_verify,
|
captcha_uuid,
|
||||||
email: cForm.email,
|
captcha_answer,
|
||||||
show_nsfw: cForm.show_nsfw,
|
honeypot,
|
||||||
captcha_uuid: cForm.captcha_uuid,
|
answer,
|
||||||
captcha_answer: cForm.captcha_answer,
|
} = i.state.form;
|
||||||
honeypot: cForm.honeypot,
|
|
||||||
answer: cForm.answer,
|
if (username && password && password_verify) {
|
||||||
|
const form: Register = {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
password_verify,
|
||||||
|
email,
|
||||||
|
show_nsfw,
|
||||||
|
captcha_uuid,
|
||||||
|
captcha_answer,
|
||||||
|
honeypot,
|
||||||
|
answer,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.send(wsClient.register(form));
|
i.setState({
|
||||||
|
registerRes: await HttpService.client.register(form),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i.state.registerRes.state == "success") {
|
||||||
|
const data = i.state.registerRes.data;
|
||||||
|
|
||||||
|
UserService.Instance.login(data);
|
||||||
|
if (UserService.Instance.jwtInfo) {
|
||||||
|
i.setState({ doneRegisteringUser: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateSite(form: CreateSite) {
|
||||||
|
const createRes = await HttpService.client.createSite(form);
|
||||||
|
if (createRes.state === "success") {
|
||||||
|
this.props.history.replace("/");
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegisterUsernameChange(i: Setup, event: any) {
|
handleRegisterUsernameChange(i: Setup, event: any) {
|
||||||
i.state.form.username = event.target.value;
|
i.state.form.username = event.target.value.trim();
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,22 +239,4 @@ export class Setup extends Component<any, State> {
|
||||||
i.state.form.password_verify = event.target.value;
|
i.state.form.password_verify = event.target.value;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ userLoading: false });
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.Register) {
|
|
||||||
let data = wsJsonToRes<LoginResponse>(msg);
|
|
||||||
this.setState({ userLoading: false });
|
|
||||||
UserService.Instance.login(data);
|
|
||||||
if (UserService.Instance.jwtInfo) {
|
|
||||||
this.setState({ doneRegisteringUser: true });
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.CreateSite) {
|
|
||||||
window.location.href = "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,24 +7,19 @@ import {
|
||||||
GetCaptchaResponse,
|
GetCaptchaResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
Register,
|
|
||||||
SiteView,
|
SiteView,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
isBrowser,
|
isBrowser,
|
||||||
joinLemmyUrl,
|
joinLemmyUrl,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
|
myAuth,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
toast,
|
||||||
validEmail,
|
validEmail,
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
|
@ -58,6 +53,8 @@ const passwordStrengthOptions: Options<string> = [
|
||||||
];
|
];
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
registerRes: RequestState<LoginResponse>;
|
||||||
|
captchaRes: RequestState<GetCaptchaResponse>;
|
||||||
form: {
|
form: {
|
||||||
username?: string;
|
username?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@ -69,22 +66,20 @@ interface State {
|
||||||
honeypot?: string;
|
honeypot?: string;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
};
|
};
|
||||||
registerLoading: boolean;
|
|
||||||
captcha?: GetCaptchaResponse;
|
|
||||||
captchaPlaying: boolean;
|
captchaPlaying: boolean;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Signup extends Component<any, State> {
|
export class Signup extends Component<any, State> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
private audio?: HTMLAudioElement;
|
private audio?: HTMLAudioElement;
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
registerRes: { state: "empty" },
|
||||||
|
captchaRes: { state: "empty" },
|
||||||
form: {
|
form: {
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
},
|
},
|
||||||
registerLoading: false,
|
|
||||||
captchaPlaying: false,
|
captchaPlaying: false,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
};
|
};
|
||||||
|
@ -93,23 +88,30 @@ export class Signup extends Component<any, State> {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleAnswerChange = this.handleAnswerChange.bind(this);
|
this.handleAnswerChange = this.handleAnswerChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
async componentDidMount() {
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
if (this.state.siteRes.site_view.local_site.captcha_enabled) {
|
||||||
|
await this.fetchCaptcha();
|
||||||
if (isBrowser()) {
|
|
||||||
WebSocketService.Instance.send(wsClient.getCaptcha({}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async fetchCaptcha() {
|
||||||
if (isBrowser()) {
|
this.setState({ captchaRes: { state: "loading" } });
|
||||||
this.subscription?.unsubscribe();
|
this.setState({
|
||||||
}
|
captchaRes: await HttpService.client.getCaptcha({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState(s => {
|
||||||
|
if (s.captchaRes.state == "success") {
|
||||||
|
s.form.captcha_uuid = s.captchaRes.data.ok?.uuid;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
let siteView = this.state.siteRes.site_view;
|
const siteView = this.state.siteRes.site_view;
|
||||||
return `${this.titleName(siteView)} - ${siteView.site.name}`;
|
return `${this.titleName(siteView)} - ${siteView.site.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +142,7 @@ export class Signup extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerForm() {
|
registerForm() {
|
||||||
let siteView = this.state.siteRes.site_view;
|
const siteView = this.state.siteRes.site_view;
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
||||||
<h5>{this.titleName(siteView)}</h5>
|
<h5>{this.titleName(siteView)}</h5>
|
||||||
|
@ -285,6 +287,7 @@ export class Signup extends Component<any, State> {
|
||||||
</label>
|
</label>
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<MarkdownTextArea
|
<MarkdownTextArea
|
||||||
|
initialContent=""
|
||||||
onContentChange={this.handleAnswerChange}
|
onContentChange={this.handleAnswerChange}
|
||||||
hideNavigationWarnings
|
hideNavigationWarnings
|
||||||
allLanguages={[]}
|
allLanguages={[]}
|
||||||
|
@ -294,36 +297,7 @@ export class Signup extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{this.renderCaptcha()}
|
||||||
{this.state.captcha && (
|
|
||||||
<div className="form-group row">
|
|
||||||
<label className="col-sm-2" htmlFor="register-captcha">
|
|
||||||
<span className="mr-2">{i18n.t("enter_code")}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={linkEvent(this, this.handleRegenCaptcha)}
|
|
||||||
aria-label={i18n.t("captcha")}
|
|
||||||
>
|
|
||||||
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
{this.showCaptcha()}
|
|
||||||
<div className="col-sm-6">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="register-captcha"
|
|
||||||
value={this.state.form.captcha_answer}
|
|
||||||
onInput={linkEvent(
|
|
||||||
this,
|
|
||||||
this.handleRegisterCaptchaAnswerChange
|
|
||||||
)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{siteView.local_site.enable_nsfw && (
|
{siteView.local_site.enable_nsfw && (
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
|
@ -358,7 +332,7 @@ export class Signup extends Component<any, State> {
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
{this.state.registerLoading ? (
|
{this.state.registerRes.state == "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
this.titleName(siteView)
|
this.titleName(siteView)
|
||||||
|
@ -370,8 +344,47 @@ export class Signup extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showCaptcha() {
|
renderCaptcha() {
|
||||||
let captchaRes = this.state.captcha?.ok;
|
switch (this.state.captchaRes.state) {
|
||||||
|
case "loading":
|
||||||
|
return <Spinner />;
|
||||||
|
case "success": {
|
||||||
|
const res = this.state.captchaRes.data;
|
||||||
|
return (
|
||||||
|
<div className="form-group row">
|
||||||
|
<label className="col-sm-2" htmlFor="register-captcha">
|
||||||
|
<span className="mr-2">{i18n.t("enter_code")}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleRegenCaptcha)}
|
||||||
|
aria-label={i18n.t("captcha")}
|
||||||
|
>
|
||||||
|
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
{this.showCaptcha(res)}
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="register-captcha"
|
||||||
|
value={this.state.form.captcha_answer}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleRegisterCaptchaAnswerChange
|
||||||
|
)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showCaptcha(res: GetCaptchaResponse) {
|
||||||
|
const captchaRes = res?.ok;
|
||||||
return captchaRes ? (
|
return captchaRes ? (
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<>
|
<>
|
||||||
|
@ -401,14 +414,14 @@ export class Signup extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get passwordStrength(): string | undefined {
|
get passwordStrength(): string | undefined {
|
||||||
let password = this.state.form.password;
|
const password = this.state.form.password;
|
||||||
return password
|
return password
|
||||||
? passwordStrength(password, passwordStrengthOptions).value
|
? passwordStrength(password, passwordStrengthOptions).value
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get passwordColorClass(): string {
|
get passwordColorClass(): string {
|
||||||
let strength = this.passwordStrength;
|
const strength = this.passwordStrength;
|
||||||
|
|
||||||
if (strength && ["weak", "medium"].includes(strength)) {
|
if (strength && ["weak", "medium"].includes(strength)) {
|
||||||
return "text-warning";
|
return "text-warning";
|
||||||
|
@ -419,28 +432,71 @@ export class Signup extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegisterSubmit(i: Signup, event: any) {
|
async handleRegisterSubmit(i: Signup, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ registerLoading: true });
|
const {
|
||||||
let cForm = i.state.form;
|
show_nsfw,
|
||||||
if (cForm.username && cForm.password && cForm.password_verify) {
|
answer,
|
||||||
let form: Register = {
|
captcha_answer,
|
||||||
username: cForm.username,
|
captcha_uuid,
|
||||||
password: cForm.password,
|
email,
|
||||||
password_verify: cForm.password_verify,
|
honeypot,
|
||||||
email: cForm.email,
|
password,
|
||||||
show_nsfw: cForm.show_nsfw,
|
password_verify,
|
||||||
captcha_uuid: cForm.captcha_uuid,
|
username,
|
||||||
captcha_answer: cForm.captcha_answer,
|
} = i.state.form;
|
||||||
honeypot: cForm.honeypot,
|
if (username && password && password_verify) {
|
||||||
answer: cForm.answer,
|
i.setState({ registerRes: { state: "loading" } });
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.register(form));
|
const registerRes = await HttpService.client.register({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
password_verify,
|
||||||
|
email,
|
||||||
|
show_nsfw,
|
||||||
|
captcha_uuid,
|
||||||
|
captcha_answer,
|
||||||
|
honeypot,
|
||||||
|
answer,
|
||||||
|
});
|
||||||
|
switch (registerRes.state) {
|
||||||
|
case "failed": {
|
||||||
|
toast(registerRes.msg, "danger");
|
||||||
|
i.setState({ registerRes: { state: "empty" } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "success": {
|
||||||
|
const data = registerRes.data;
|
||||||
|
|
||||||
|
// Only log them in if a jwt was set
|
||||||
|
if (data.jwt) {
|
||||||
|
UserService.Instance.login(data);
|
||||||
|
|
||||||
|
const site = await HttpService.client.getSite({ auth: myAuth() });
|
||||||
|
|
||||||
|
if (site.state === "success") {
|
||||||
|
UserService.Instance.myUserInfo = site.data.my_user;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.props.history.replace("/communities");
|
||||||
|
} else {
|
||||||
|
if (data.verify_email_sent) {
|
||||||
|
toast(i18n.t("verify_email_sent"));
|
||||||
|
}
|
||||||
|
if (data.registration_created) {
|
||||||
|
toast(i18n.t("registration_application_sent"));
|
||||||
|
}
|
||||||
|
i.props.history.push("/");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegisterUsernameChange(i: Signup, event: any) {
|
handleRegisterUsernameChange(i: Signup, event: any) {
|
||||||
i.state.form.username = event.target.value;
|
i.state.form.username = event.target.value.trim();
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,19 +537,20 @@ export class Signup extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegenCaptcha(i: Signup) {
|
async handleRegenCaptcha(i: Signup) {
|
||||||
i.audio = undefined;
|
i.audio = undefined;
|
||||||
i.setState({ captchaPlaying: false });
|
i.setState({ captchaPlaying: false });
|
||||||
WebSocketService.Instance.send(wsClient.getCaptcha({}));
|
await i.fetchCaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCaptchaPlay(i: Signup) {
|
handleCaptchaPlay(i: Signup) {
|
||||||
// This was a bad bug, it should only build the new audio on a new file.
|
// This was a bad bug, it should only build the new audio on a new file.
|
||||||
// Replays would stop prematurely if this was rebuilt every time.
|
// Replays would stop prematurely if this was rebuilt every time.
|
||||||
let captchaRes = i.state.captcha?.ok;
|
|
||||||
if (captchaRes) {
|
if (i.state.captchaRes.state == "success" && i.state.captchaRes.data.ok) {
|
||||||
|
const captchaRes = i.state.captchaRes.data.ok;
|
||||||
if (!i.audio) {
|
if (!i.audio) {
|
||||||
let base64 = `data:audio/wav;base64,${captchaRes.wav}`;
|
const base64 = `data:audio/wav;base64,${captchaRes.wav}`;
|
||||||
i.audio = new Audio(base64);
|
i.audio = new Audio(base64);
|
||||||
i.audio.play();
|
i.audio.play();
|
||||||
|
|
||||||
|
@ -512,45 +569,4 @@ export class Signup extends Component<any, State> {
|
||||||
captchaPngSrc(captcha: CaptchaResponse) {
|
captchaPngSrc(captcha: CaptchaResponse) {
|
||||||
return `data:image/png;base64,${captcha.png}`;
|
return `data:image/png;base64,${captcha.png}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState(s => ((s.form.captcha_answer = undefined), s));
|
|
||||||
// Refetch another captcha
|
|
||||||
// WebSocketService.Instance.send(wsClient.getCaptcha());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (op == UserOperation.Register) {
|
|
||||||
let data = wsJsonToRes<LoginResponse>(msg);
|
|
||||||
// Only log them in if a jwt was set
|
|
||||||
if (data.jwt) {
|
|
||||||
UserService.Instance.login(data);
|
|
||||||
this.props.history.push("/communities");
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
if (data.verify_email_sent) {
|
|
||||||
toast(i18n.t("verify_email_sent"));
|
|
||||||
}
|
|
||||||
if (data.registration_created) {
|
|
||||||
toast(i18n.t("registration_application_sent"));
|
|
||||||
}
|
|
||||||
this.props.history.push("/");
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.GetCaptcha) {
|
|
||||||
let data = wsJsonToRes<GetCaptchaResponse>(msg);
|
|
||||||
if (data.ok) {
|
|
||||||
this.setState({ captcha: data });
|
|
||||||
this.setState(s => ((s.form.captcha_uuid = data.ok?.uuid), s));
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.PasswordReset) {
|
|
||||||
toast(i18n.t("reset_password_mail_sent"));
|
|
||||||
} else if (op == UserOperation.GetSite) {
|
|
||||||
let data = wsJsonToRes<GetSiteResponse>(msg);
|
|
||||||
this.setState({ siteRes: data });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -67,7 +67,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
siteInfo() {
|
siteInfo() {
|
||||||
let site = this.props.site;
|
const site = this.props.site;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{site.description && <h6>{site.description}</h6>}
|
{site.description && <h6>{site.description}</h6>}
|
||||||
|
@ -98,8 +98,8 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
badges(siteAggregates: SiteAggregates) {
|
badges(siteAggregates: SiteAggregates) {
|
||||||
let counts = siteAggregates;
|
const counts = siteAggregates;
|
||||||
let online = this.props.online ?? 1;
|
const online = this.props.online ?? 1;
|
||||||
return (
|
return (
|
||||||
<ul className="my-2 list-inline">
|
<ul className="my-2 list-inline">
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { Component, InfernoMouseEvent, linkEvent } from "inferno";
|
import { Component, InfernoMouseEvent, linkEvent } from "inferno";
|
||||||
import { EditSite, GetSiteResponse } from "lemmy-js-client";
|
import { EditSite, Tagline } from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { capitalizeFirstLetter, myAuthRequired } from "../../utils";
|
||||||
import { capitalizeFirstLetter, myAuth, wsClient } from "../../utils";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
|
||||||
interface TaglineFormProps {
|
interface TaglineFormProps {
|
||||||
siteRes: GetSiteResponse;
|
taglines: Array<Tagline>;
|
||||||
|
onSaveSite(form: EditSite): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaglineFormState {
|
interface TaglineFormState {
|
||||||
siteRes: GetSiteResponse;
|
taglines: Array<string>;
|
||||||
siteForm: EditSite;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
editingRow?: number;
|
editingRow?: number;
|
||||||
}
|
}
|
||||||
|
@ -21,12 +20,8 @@ interface TaglineFormState {
|
||||||
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
state: TaglineFormState = {
|
state: TaglineFormState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
siteRes: this.props.siteRes,
|
|
||||||
editingRow: undefined,
|
editingRow: undefined,
|
||||||
siteForm: {
|
taglines: this.props.taglines.map(x => x.content),
|
||||||
taglines: this.props.siteRes.taglines?.map(x => x.content),
|
|
||||||
auth: "TODO",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -54,7 +49,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
<th style="width:121px"></th>
|
<th style="width:121px"></th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.state.siteForm.taglines?.map((cv, index) => (
|
{this.state.taglines.map((cv, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
{this.state.editingRow == index && (
|
{this.state.editingRow == index && (
|
||||||
|
@ -64,8 +59,8 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
this.handleTaglineChange(this, index, s)
|
this.handleTaglineChange(this, index, s)
|
||||||
}
|
}
|
||||||
hideNavigationWarnings
|
hideNavigationWarnings
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
allLanguages={[]}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={[]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.state.editingRow != index && <div>{cv}</div>}
|
{this.state.editingRow != index && <div>{cv}</div>}
|
||||||
|
@ -74,7 +69,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted"
|
className="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
{ form: this, index: index },
|
{ i: this, index: index },
|
||||||
this.handleEditTaglineClick
|
this.handleEditTaglineClick
|
||||||
)}
|
)}
|
||||||
data-tippy-content={i18n.t("edit")}
|
data-tippy-content={i18n.t("edit")}
|
||||||
|
@ -86,7 +81,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted"
|
className="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
{ form: this, index: index },
|
{ i: this, index: index },
|
||||||
this.handleDeleteTaglineClick
|
this.handleDeleteTaglineClick
|
||||||
)}
|
)}
|
||||||
data-tippy-content={i18n.t("delete")}
|
data-tippy-content={i18n.t("delete")}
|
||||||
|
@ -131,46 +126,38 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTaglineChange(i: TaglineForm, index: number, val: string) {
|
handleTaglineChange(i: TaglineForm, index: number, val: string) {
|
||||||
let taglines = i.state.siteForm.taglines;
|
if (i.state.taglines) {
|
||||||
if (taglines) {
|
i.setState(prev => ({
|
||||||
taglines[index] = val;
|
...prev,
|
||||||
i.setState(i.state);
|
taglines: prev.taglines.map((tl, i) => (i === index ? val : tl)),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteTaglineClick(
|
handleDeleteTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
||||||
props: { form: TaglineForm; index: number },
|
|
||||||
event: any
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let taglines = props.form.state.siteForm.taglines;
|
d.i.setState(prev => ({
|
||||||
if (taglines) {
|
...prev,
|
||||||
taglines.splice(props.index, 1);
|
taglines: prev.taglines.filter((_, i) => i !== d.index),
|
||||||
props.form.state.siteForm.taglines = undefined;
|
editingRow: undefined,
|
||||||
props.form.setState(props.form.state);
|
}));
|
||||||
props.form.state.siteForm.taglines = taglines;
|
|
||||||
props.form.setState({ ...props.form.state, editingRow: undefined });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditTaglineClick(
|
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
||||||
props: { form: TaglineForm; index: number },
|
|
||||||
event: any
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.state.editingRow == props.index) {
|
if (this.state.editingRow == d.index) {
|
||||||
props.form.setState({ editingRow: undefined });
|
d.i.setState({ editingRow: undefined });
|
||||||
} else {
|
} else {
|
||||||
props.form.setState({ editingRow: props.index });
|
d.i.setState({ editingRow: d.index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveClick(i: TaglineForm) {
|
async handleSaveClick(i: TaglineForm) {
|
||||||
i.setState({ loading: true });
|
i.setState({ loading: true });
|
||||||
let auth = myAuth() ?? "TODO";
|
i.props.onSaveSite({
|
||||||
i.setState(s => ((s.siteForm.auth = auth), s));
|
taglines: i.state.taglines,
|
||||||
WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
|
auth: myAuthRequired(),
|
||||||
i.setState({ ...i.state, editingRow: undefined });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddTaglineClick(
|
handleAddTaglineClick(
|
||||||
|
@ -178,13 +165,12 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
event: InfernoMouseEvent<HTMLButtonElement>
|
event: InfernoMouseEvent<HTMLButtonElement>
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!i.state.siteForm.taglines) {
|
const newTaglines = [...i.state.taglines];
|
||||||
i.state.siteForm.taglines = [];
|
newTaglines.push("");
|
||||||
}
|
|
||||||
i.state.siteForm.taglines.push("");
|
|
||||||
i.setState({
|
i.setState({
|
||||||
...i.state,
|
taglines: newTaglines,
|
||||||
editingRow: i.state.siteForm.taglines.length - 1,
|
editingRow: newTaglines.length - 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,11 @@ import {
|
||||||
AdminPurgeCommunityView,
|
AdminPurgeCommunityView,
|
||||||
AdminPurgePersonView,
|
AdminPurgePersonView,
|
||||||
AdminPurgePostView,
|
AdminPurgePostView,
|
||||||
CommunityModeratorView,
|
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
GetCommunityResponse,
|
GetCommunityResponse,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
GetModlogResponse,
|
GetModlogResponse,
|
||||||
GetPersonDetails,
|
GetPersonDetails,
|
||||||
GetPersonDetailsResponse,
|
|
||||||
ModAddCommunityView,
|
ModAddCommunityView,
|
||||||
ModAddView,
|
ModAddView,
|
||||||
ModBanFromCommunityView,
|
ModBanFromCommunityView,
|
||||||
|
@ -27,15 +25,12 @@ import {
|
||||||
ModTransferCommunityView,
|
ModTransferCommunityView,
|
||||||
ModlogActionType,
|
ModlogActionType,
|
||||||
Person,
|
Person,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../i18next";
|
import { i18n } from "../i18next";
|
||||||
import { InitialFetchRequest } from "../interfaces";
|
import { InitialFetchRequest } from "../interfaces";
|
||||||
import { WebSocketService } from "../services";
|
import { FirstLoadService } from "../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../services/HttpService";
|
||||||
import {
|
import {
|
||||||
Choice,
|
Choice,
|
||||||
QueryParams,
|
QueryParams,
|
||||||
|
@ -49,13 +44,9 @@ import {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
getUpdatedSearchId,
|
getUpdatedSearchId,
|
||||||
isBrowser,
|
|
||||||
myAuth,
|
myAuth,
|
||||||
personToChoice,
|
personToChoice,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { HtmlTags } from "./common/html-tags";
|
import { HtmlTags } from "./common/html-tags";
|
||||||
import { Icon, Spinner } from "./common/icon";
|
import { Icon, Spinner } from "./common/icon";
|
||||||
|
@ -100,10 +91,8 @@ const getModlogQueryParams = () =>
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ModlogState {
|
interface ModlogState {
|
||||||
res?: GetModlogResponse;
|
res: RequestState<GetModlogResponse>;
|
||||||
communityMods?: CommunityModeratorView[];
|
communityRes: RequestState<GetCommunityResponse>;
|
||||||
communityName?: string;
|
|
||||||
loadingModlog: boolean;
|
|
||||||
loadingModSearch: boolean;
|
loadingModSearch: boolean;
|
||||||
loadingUserSearch: boolean;
|
loadingUserSearch: boolean;
|
||||||
modSearchOptions: Choice[];
|
modSearchOptions: Choice[];
|
||||||
|
@ -629,7 +618,7 @@ async function createNewOptions({
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
newOptions.push(
|
newOptions.push(
|
||||||
...(await fetchUsers(text)).users
|
...(await fetchUsers(text))
|
||||||
.slice(0, Number(fetchLimit))
|
.slice(0, Number(fetchLimit))
|
||||||
.map<Choice>(personToChoice)
|
.map<Choice>(personToChoice)
|
||||||
);
|
);
|
||||||
|
@ -643,10 +632,10 @@ export class Modlog extends Component<
|
||||||
ModlogState
|
ModlogState
|
||||||
> {
|
> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
state: ModlogState = {
|
state: ModlogState = {
|
||||||
loadingModlog: true,
|
res: { state: "empty" },
|
||||||
|
communityRes: { state: "empty" },
|
||||||
loadingModSearch: false,
|
loadingModSearch: false,
|
||||||
loadingUserSearch: false,
|
loadingUserSearch: false,
|
||||||
userSearchOptions: [],
|
userSearchOptions: [],
|
||||||
|
@ -662,58 +651,35 @@ export class Modlog extends Component<
|
||||||
this.handleUserChange = this.handleUserChange.bind(this);
|
this.handleUserChange = this.handleUserChange.bind(this);
|
||||||
this.handleModChange = this.handleModChange.bind(this);
|
this.handleModChange = this.handleModChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path === this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
|
const [res, communityRes, filteredModRes, filteredUserRes] =
|
||||||
|
this.isoData.routeData;
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
res: this.isoData.routeData[0] as GetModlogResponse,
|
res,
|
||||||
|
communityRes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const communityRes: GetCommunityResponse | undefined =
|
if (filteredModRes.state === "success") {
|
||||||
this.isoData.routeData[1];
|
|
||||||
|
|
||||||
// Getting the moderators
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
communityMods: communityRes?.moderators,
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredModRes: GetPersonDetailsResponse | undefined =
|
|
||||||
this.isoData.routeData[2];
|
|
||||||
if (filteredModRes) {
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
modSearchOptions: [personToChoice(filteredModRes.person_view)],
|
modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredUserRes: GetPersonDetailsResponse | undefined =
|
if (filteredUserRes.state === "success") {
|
||||||
this.isoData.routeData[3];
|
|
||||||
if (filteredUserRes) {
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
userSearchOptions: [personToChoice(filteredUserRes.person_view)],
|
userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = { ...this.state, loadingModlog: false };
|
|
||||||
} else {
|
|
||||||
this.refetch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get combined() {
|
get combined() {
|
||||||
const res = this.state.res;
|
const res = this.state.res;
|
||||||
const combined = res ? buildCombined(res) : [];
|
const combined = res.state == "success" ? buildCombined(res.data) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -737,7 +703,10 @@ export class Modlog extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
get amAdminOrMod(): boolean {
|
get amAdminOrMod(): boolean {
|
||||||
return amAdmin() || amMod(this.state.communityMods);
|
const amMod_ =
|
||||||
|
this.state.communityRes.state == "success" &&
|
||||||
|
amMod(this.state.communityRes.data.moderators);
|
||||||
|
return amAdmin() || amMod_;
|
||||||
}
|
}
|
||||||
|
|
||||||
modOrAdminText(person?: Person): string {
|
modOrAdminText(person?: Person): string {
|
||||||
|
@ -755,14 +724,12 @@ export class Modlog extends Component<
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
communityName,
|
|
||||||
loadingModlog,
|
|
||||||
loadingModSearch,
|
loadingModSearch,
|
||||||
loadingUserSearch,
|
loadingUserSearch,
|
||||||
userSearchOptions,
|
userSearchOptions,
|
||||||
modSearchOptions,
|
modSearchOptions,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { actionType, page, modId, userId } = getModlogQueryParams();
|
const { actionType, modId, userId } = getModlogQueryParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
|
@ -785,14 +752,17 @@ export class Modlog extends Component<
|
||||||
#<strong>#</strong>#
|
#<strong>#</strong>#
|
||||||
</T>
|
</T>
|
||||||
</div>
|
</div>
|
||||||
<h5>
|
{this.state.communityRes.state === "success" && (
|
||||||
{communityName && (
|
<h5>
|
||||||
<Link className="text-body" to={`/c/${communityName}`}>
|
<Link
|
||||||
/c/{communityName}{" "}
|
className="text-body"
|
||||||
|
to={`/c/${this.state.communityRes.data.community_view.community.name}`}
|
||||||
|
>
|
||||||
|
/c/{this.state.communityRes.data.community_view.community.name}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
<span>{i18n.t("modlog")}</span>
|
||||||
<span>{i18n.t("modlog")}</span>
|
</h5>
|
||||||
</h5>
|
)}
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<select
|
<select
|
||||||
value={actionType}
|
value={actionType}
|
||||||
|
@ -841,30 +811,41 @@ export class Modlog extends Component<
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="table-responsive">
|
{this.renderModlogTable()}
|
||||||
{loadingModlog ? (
|
|
||||||
<h5>
|
|
||||||
<Spinner large />
|
|
||||||
</h5>
|
|
||||||
) : (
|
|
||||||
<table id="modlog_table" className="table table-sm table-hover">
|
|
||||||
<thead className="pointer">
|
|
||||||
<tr>
|
|
||||||
<th> {i18n.t("time")}</th>
|
|
||||||
<th>{i18n.t("mod")}</th>
|
|
||||||
<th>{i18n.t("action")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{this.combined}
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
<Paginator page={page} onChange={this.handlePageChange} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderModlogTable() {
|
||||||
|
switch (this.state.res.state) {
|
||||||
|
case "loading":
|
||||||
|
return (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
case "success": {
|
||||||
|
const page = getModlogQueryParams().page;
|
||||||
|
return (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table id="modlog_table" className="table table-sm table-hover">
|
||||||
|
<thead className="pointer">
|
||||||
|
<tr>
|
||||||
|
<th> {i18n.t("time")}</th>
|
||||||
|
<th>{i18n.t("mod")}</th>
|
||||||
|
<th>{i18n.t("action")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{this.combined}
|
||||||
|
</table>
|
||||||
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleFilterActionChange(i: Modlog, event: any) {
|
handleFilterActionChange(i: Modlog, event: any) {
|
||||||
i.updateUrl({
|
i.updateUrl({
|
||||||
actionType: event.target.value as ModlogActionType,
|
actionType: event.target.value as ModlogActionType,
|
||||||
|
@ -918,7 +899,7 @@ export class Modlog extends Component<
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
|
async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
|
||||||
const {
|
const {
|
||||||
page: urlPage,
|
page: urlPage,
|
||||||
actionType: urlActionType,
|
actionType: urlActionType,
|
||||||
|
@ -941,42 +922,39 @@ export class Modlog extends Component<
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState({
|
await this.refetch();
|
||||||
loadingModlog: true,
|
|
||||||
res: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.refetch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
async refetch() {
|
||||||
const auth = myAuth(false);
|
const auth = myAuth();
|
||||||
const { actionType, page, modId, userId } = getModlogQueryParams();
|
const { actionType, page, modId, userId } = getModlogQueryParams();
|
||||||
const { communityId: urlCommunityId } = this.props.match.params;
|
const { communityId: urlCommunityId } = this.props.match.params;
|
||||||
const communityId = getIdFromString(urlCommunityId);
|
const communityId = getIdFromString(urlCommunityId);
|
||||||
|
|
||||||
const modlogForm: GetModlog = {
|
this.setState({ res: { state: "loading" } });
|
||||||
community_id: communityId,
|
this.setState({
|
||||||
page,
|
res: await HttpService.client.getModlog({
|
||||||
limit: fetchLimit,
|
community_id: communityId,
|
||||||
type_: actionType,
|
page,
|
||||||
other_person_id: userId ?? undefined,
|
limit: fetchLimit,
|
||||||
mod_person_id: !this.isoData.site_res.site_view.local_site
|
type_: actionType,
|
||||||
.hide_modlog_mod_names
|
other_person_id: userId ?? undefined,
|
||||||
? modId ?? undefined
|
mod_person_id: !this.isoData.site_res.site_view.local_site
|
||||||
: undefined,
|
.hide_modlog_mod_names
|
||||||
auth,
|
? modId ?? undefined
|
||||||
};
|
: undefined,
|
||||||
|
auth,
|
||||||
WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
if (communityId) {
|
if (communityId) {
|
||||||
const communityForm: GetCommunity = {
|
this.setState({ communityRes: { state: "loading" } });
|
||||||
id: communityId,
|
this.setState({
|
||||||
auth,
|
communityRes: await HttpService.client.getCommunity({
|
||||||
};
|
id: communityId,
|
||||||
|
auth,
|
||||||
WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -986,9 +964,11 @@ export class Modlog extends Component<
|
||||||
query: { modId: urlModId, page, userId: urlUserId, actionType },
|
query: { modId: urlModId, page, userId: urlUserId, actionType },
|
||||||
auth,
|
auth,
|
||||||
site,
|
site,
|
||||||
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<any>[] {
|
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<
|
||||||
|
RequestState<any>
|
||||||
|
>[] {
|
||||||
const pathSplit = path.split("/");
|
const pathSplit = path.split("/");
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
const communityId = getIdFromString(pathSplit[2]);
|
const communityId = getIdFromString(pathSplit[2]);
|
||||||
const modId = !site.site_view.local_site.hide_modlog_mod_names
|
const modId = !site.site_view.local_site.hide_modlog_mod_names
|
||||||
? getIdFromString(urlModId)
|
? getIdFromString(urlModId)
|
||||||
|
@ -1014,7 +994,7 @@ export class Modlog extends Component<
|
||||||
};
|
};
|
||||||
promises.push(client.getCommunity(communityForm));
|
promises.push(client.getCommunity(communityForm));
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modId) {
|
if (modId) {
|
||||||
|
@ -1025,7 +1005,7 @@ export class Modlog extends Component<
|
||||||
|
|
||||||
promises.push(client.getPersonDetails(getPersonForm));
|
promises.push(client.getPersonDetails(getPersonForm));
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
@ -1036,43 +1016,9 @@ export class Modlog extends Component<
|
||||||
|
|
||||||
promises.push(client.getPersonDetails(getPersonForm));
|
promises.push(client.getPersonDetails(getPersonForm));
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
const op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
} else {
|
|
||||||
switch (op) {
|
|
||||||
case UserOperation.GetModlog: {
|
|
||||||
const res = wsJsonToRes<GetModlogResponse>(msg);
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
this.setState({ res, loadingModlog: false });
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserOperation.GetCommunity: {
|
|
||||||
const {
|
|
||||||
moderators,
|
|
||||||
community_view: {
|
|
||||||
community: { name },
|
|
||||||
},
|
|
||||||
} = wsJsonToRes<GetCommunityResponse>(msg);
|
|
||||||
this.setState({
|
|
||||||
communityMods: moderators,
|
|
||||||
communityName: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,59 +1,35 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
||||||
GetSiteResponse,
|
|
||||||
LoginResponse,
|
|
||||||
PasswordChangeAfterReset,
|
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { HttpService, UserService } from "../../services";
|
||||||
import {
|
import { RequestState } from "../../services/HttpService";
|
||||||
capitalizeFirstLetter,
|
import { capitalizeFirstLetter, myAuth, setIsoData } from "../../utils";
|
||||||
isBrowser,
|
|
||||||
setIsoData,
|
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
passwordChangeRes: RequestState<LoginResponse>;
|
||||||
form: {
|
form: {
|
||||||
token: string;
|
token: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
password_verify?: string;
|
password_verify?: string;
|
||||||
};
|
};
|
||||||
loading: boolean;
|
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PasswordChange extends Component<any, State> {
|
export class PasswordChange extends Component<any, State> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
passwordChangeRes: { state: "empty" },
|
||||||
|
siteRes: this.isoData.site_res,
|
||||||
form: {
|
form: {
|
||||||
token: this.props.match.params.token,
|
token: this.props.match.params.token,
|
||||||
},
|
},
|
||||||
loading: false,
|
|
||||||
siteRes: this.isoData.site_res,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
|
@ -117,7 +93,7 @@ export class PasswordChange extends Component<any, State> {
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
{this.state.loading ? (
|
{this.state.passwordChangeRes.state == "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
capitalizeFirstLetter(i18n.t("save"))
|
capitalizeFirstLetter(i18n.t("save"))
|
||||||
|
@ -139,36 +115,33 @@ export class PasswordChange extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePasswordChangeSubmit(i: PasswordChange, event: any) {
|
async handlePasswordChangeSubmit(i: PasswordChange, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ loading: true });
|
i.setState({ passwordChangeRes: { state: "loading" } });
|
||||||
|
|
||||||
let password = i.state.form.password;
|
const password = i.state.form.password;
|
||||||
let password_verify = i.state.form.password_verify;
|
const password_verify = i.state.form.password_verify;
|
||||||
|
|
||||||
if (password && password_verify) {
|
if (password && password_verify) {
|
||||||
let form: PasswordChangeAfterReset = {
|
i.setState({
|
||||||
token: i.state.form.token,
|
passwordChangeRes: await HttpService.client.passwordChangeAfterReset({
|
||||||
password,
|
token: i.state.form.token,
|
||||||
password_verify,
|
password,
|
||||||
};
|
password_verify,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.passwordChange(form));
|
if (i.state.passwordChangeRes.state === "success") {
|
||||||
}
|
const data = i.state.passwordChangeRes.data;
|
||||||
}
|
UserService.Instance.login(data);
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
const site = await HttpService.client.getSite({ auth: myAuth() });
|
||||||
let op = wsUserOp(msg);
|
if (site.state === "success") {
|
||||||
console.log(msg);
|
UserService.Instance.myUserInfo = site.data.my_user;
|
||||||
if (msg.error) {
|
}
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ loading: false });
|
this.props.history.replace("/");
|
||||||
return;
|
}
|
||||||
} else if (op == UserOperation.PasswordChangeAfterReset) {
|
|
||||||
let data = wsJsonToRes<LoginResponse>(msg);
|
|
||||||
UserService.Instance.login(data);
|
|
||||||
this.props.history.push("/");
|
|
||||||
location.reload();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,40 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
AddAdmin,
|
||||||
|
AddModToCommunity,
|
||||||
|
BanFromCommunity,
|
||||||
|
BanPerson,
|
||||||
|
BlockPerson,
|
||||||
|
CommentId,
|
||||||
CommentView,
|
CommentView,
|
||||||
|
CreateComment,
|
||||||
|
CreateCommentLike,
|
||||||
|
CreateCommentReport,
|
||||||
|
CreatePostLike,
|
||||||
|
CreatePostReport,
|
||||||
|
DeleteComment,
|
||||||
|
DeletePost,
|
||||||
|
DistinguishComment,
|
||||||
|
EditComment,
|
||||||
|
EditPost,
|
||||||
|
FeaturePost,
|
||||||
|
GetComments,
|
||||||
GetPersonDetailsResponse,
|
GetPersonDetailsResponse,
|
||||||
Language,
|
Language,
|
||||||
|
LockPost,
|
||||||
|
MarkCommentReplyAsRead,
|
||||||
|
MarkPersonMentionAsRead,
|
||||||
PersonView,
|
PersonView,
|
||||||
PostView,
|
PostView,
|
||||||
|
PurgeComment,
|
||||||
|
PurgePerson,
|
||||||
|
PurgePost,
|
||||||
|
RemoveComment,
|
||||||
|
RemovePost,
|
||||||
|
SaveComment,
|
||||||
|
SavePost,
|
||||||
SortType,
|
SortType,
|
||||||
|
TransferCommunity,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { CommentViewType, PersonDetailsView } from "../../interfaces";
|
import { CommentViewType, PersonDetailsView } from "../../interfaces";
|
||||||
import { commentsToFlatNodes, setupTippy } from "../../utils";
|
import { commentsToFlatNodes, setupTippy } from "../../utils";
|
||||||
|
@ -15,6 +44,7 @@ import { PostListing } from "../post/post-listing";
|
||||||
|
|
||||||
interface PersonDetailsProps {
|
interface PersonDetailsProps {
|
||||||
personRes: GetPersonDetailsResponse;
|
personRes: GetPersonDetailsResponse;
|
||||||
|
finished: Map<CommentId, boolean | undefined>;
|
||||||
admins: PersonView[];
|
admins: PersonView[];
|
||||||
allLanguages: Language[];
|
allLanguages: Language[];
|
||||||
siteLanguages: number[];
|
siteLanguages: number[];
|
||||||
|
@ -25,6 +55,34 @@ interface PersonDetailsProps {
|
||||||
enableNsfw: boolean;
|
enableNsfw: boolean;
|
||||||
view: PersonDetailsView;
|
view: PersonDetailsView;
|
||||||
onPageChange(page: number): number | any;
|
onPageChange(page: number): number | any;
|
||||||
|
onSaveComment(form: SaveComment): void;
|
||||||
|
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
|
||||||
|
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
|
||||||
|
onCreateComment(form: CreateComment): void;
|
||||||
|
onEditComment(form: EditComment): void;
|
||||||
|
onCommentVote(form: CreateCommentLike): void;
|
||||||
|
onBlockPerson(form: BlockPerson): void;
|
||||||
|
onDeleteComment(form: DeleteComment): void;
|
||||||
|
onRemoveComment(form: RemoveComment): void;
|
||||||
|
onDistinguishComment(form: DistinguishComment): void;
|
||||||
|
onAddModToCommunity(form: AddModToCommunity): void;
|
||||||
|
onAddAdmin(form: AddAdmin): void;
|
||||||
|
onBanPersonFromCommunity(form: BanFromCommunity): void;
|
||||||
|
onBanPerson(form: BanPerson): void;
|
||||||
|
onTransferCommunity(form: TransferCommunity): void;
|
||||||
|
onFetchChildren?(form: GetComments): void;
|
||||||
|
onCommentReport(form: CreateCommentReport): void;
|
||||||
|
onPurgePerson(form: PurgePerson): void;
|
||||||
|
onPurgeComment(form: PurgeComment): void;
|
||||||
|
onPostEdit(form: EditPost): void;
|
||||||
|
onPostVote(form: CreatePostLike): void;
|
||||||
|
onPostReport(form: CreatePostReport): void;
|
||||||
|
onLockPost(form: LockPost): void;
|
||||||
|
onDeletePost(form: DeletePost): void;
|
||||||
|
onRemovePost(form: RemovePost): void;
|
||||||
|
onSavePost(form: SavePost): void;
|
||||||
|
onFeaturePost(form: FeaturePost): void;
|
||||||
|
onPurgePost(form: PurgePost): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ItemEnum {
|
enum ItemEnum {
|
||||||
|
@ -87,12 +145,13 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
renderItemType(i: ItemType) {
|
renderItemType(i: ItemType) {
|
||||||
switch (i.type_) {
|
switch (i.type_) {
|
||||||
case ItemEnum.Comment: {
|
case ItemEnum.Comment: {
|
||||||
let c = i.view as CommentView;
|
const c = i.view as CommentView;
|
||||||
return (
|
return (
|
||||||
<CommentNodes
|
<CommentNodes
|
||||||
key={i.id}
|
key={i.id}
|
||||||
nodes={[{ comment_view: c, children: [], depth: 0 }]}
|
nodes={[{ comment_view: c, children: [], depth: 0 }]}
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
|
finished={this.props.finished}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
noBorder
|
noBorder
|
||||||
noIndent
|
noIndent
|
||||||
|
@ -101,11 +160,30 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
onCommentReplyRead={this.props.onCommentReplyRead}
|
||||||
|
onPersonMentionRead={this.props.onPersonMentionRead}
|
||||||
|
onCreateComment={this.props.onCreateComment}
|
||||||
|
onEditComment={this.props.onEditComment}
|
||||||
|
onCommentVote={this.props.onCommentVote}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onSaveComment={this.props.onSaveComment}
|
||||||
|
onDeleteComment={this.props.onDeleteComment}
|
||||||
|
onRemoveComment={this.props.onRemoveComment}
|
||||||
|
onDistinguishComment={this.props.onDistinguishComment}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
|
onFetchChildren={this.props.onFetchChildren}
|
||||||
|
onCommentReport={this.props.onCommentReport}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgeComment={this.props.onPurgeComment}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case ItemEnum.Post: {
|
case ItemEnum.Post: {
|
||||||
let p = i.view as PostView;
|
const p = i.view as PostView;
|
||||||
return (
|
return (
|
||||||
<PostListing
|
<PostListing
|
||||||
key={i.id}
|
key={i.id}
|
||||||
|
@ -116,6 +194,22 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableNsfw={this.props.enableNsfw}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
onPostEdit={this.props.onPostEdit}
|
||||||
|
onPostVote={this.props.onPostVote}
|
||||||
|
onPostReport={this.props.onPostReport}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onLockPost={this.props.onLockPost}
|
||||||
|
onDeletePost={this.props.onDeletePost}
|
||||||
|
onRemovePost={this.props.onRemovePost}
|
||||||
|
onSavePost={this.props.onSavePost}
|
||||||
|
onFeaturePost={this.props.onFeaturePost}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgePost={this.props.onPurgePost}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -126,14 +220,14 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
|
|
||||||
overview() {
|
overview() {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
let comments: ItemType[] = this.props.personRes.comments.map(r => ({
|
const comments: ItemType[] = this.props.personRes.comments.map(r => ({
|
||||||
id: id++,
|
id: id++,
|
||||||
type_: ItemEnum.Comment,
|
type_: ItemEnum.Comment,
|
||||||
view: r,
|
view: r,
|
||||||
published: r.comment.published,
|
published: r.comment.published,
|
||||||
score: r.counts.score,
|
score: r.counts.score,
|
||||||
}));
|
}));
|
||||||
let posts: ItemType[] = this.props.personRes.posts.map(r => ({
|
const posts: ItemType[] = this.props.personRes.posts.map(r => ({
|
||||||
id: id++,
|
id: id++,
|
||||||
type_: ItemEnum.Post,
|
type_: ItemEnum.Post,
|
||||||
view: r,
|
view: r,
|
||||||
|
@ -141,7 +235,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
score: r.counts.score,
|
score: r.counts.score,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let combined = [...comments, ...posts];
|
const combined = [...comments, ...posts];
|
||||||
|
|
||||||
// Sort it
|
// Sort it
|
||||||
if (this.props.sort === "New") {
|
if (this.props.sort === "New") {
|
||||||
|
@ -167,12 +261,32 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
nodes={commentsToFlatNodes(this.props.personRes.comments)}
|
nodes={commentsToFlatNodes(this.props.personRes.comments)}
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
|
finished={this.props.finished}
|
||||||
noIndent
|
noIndent
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
onCommentReplyRead={this.props.onCommentReplyRead}
|
||||||
|
onPersonMentionRead={this.props.onPersonMentionRead}
|
||||||
|
onCreateComment={this.props.onCreateComment}
|
||||||
|
onEditComment={this.props.onEditComment}
|
||||||
|
onCommentVote={this.props.onCommentVote}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onSaveComment={this.props.onSaveComment}
|
||||||
|
onDeleteComment={this.props.onDeleteComment}
|
||||||
|
onRemoveComment={this.props.onRemoveComment}
|
||||||
|
onDistinguishComment={this.props.onDistinguishComment}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
|
onFetchChildren={this.props.onFetchChildren}
|
||||||
|
onCommentReport={this.props.onCommentReport}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgeComment={this.props.onPurgeComment}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -191,6 +305,22 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableNsfw={this.props.enableNsfw}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
onPostEdit={this.props.onPostEdit}
|
||||||
|
onPostVote={this.props.onPostVote}
|
||||||
|
onPostReport={this.props.onPostReport}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onLockPost={this.props.onLockPost}
|
||||||
|
onDeletePost={this.props.onDeletePost}
|
||||||
|
onRemovePost={this.props.onRemovePost}
|
||||||
|
onSavePost={this.props.onSavePost}
|
||||||
|
onFeaturePost={this.props.onFeaturePost}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgePost={this.props.onPurgePost}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
/>
|
/>
|
||||||
<hr className="my-3" />
|
<hr className="my-3" />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -20,15 +20,15 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let person = this.props.person;
|
const person = this.props.person;
|
||||||
let local = person.local;
|
const local = person.local;
|
||||||
let apubName: string, link: string;
|
let apubName: string, link: string;
|
||||||
|
|
||||||
if (local) {
|
if (local) {
|
||||||
apubName = `@${person.name}`;
|
apubName = `@${person.name}`;
|
||||||
link = `/u/${person.name}`;
|
link = `/u/${person.name}`;
|
||||||
} else {
|
} else {
|
||||||
let domain = hostname(person.actor_id);
|
const domain = hostname(person.actor_id);
|
||||||
apubName = `@${person.name}@${domain}`;
|
apubName = `@${person.name}@${domain}`;
|
||||||
link = !this.props.realLink
|
link = !this.props.realLink
|
||||||
? `/u/${person.name}@${domain}`
|
? `/u/${person.name}@${domain}`
|
||||||
|
@ -70,7 +70,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarAndName(displayName: string) {
|
avatarAndName(displayName: string) {
|
||||||
let avatar = this.props.person.avatar;
|
const avatar = this.props.person.avatar;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{avatar &&
|
{avatar &&
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,27 +1,22 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
ApproveRegistrationApplication,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListRegistrationApplications,
|
ListRegistrationApplications,
|
||||||
ListRegistrationApplicationsResponse,
|
ListRegistrationApplicationsResponse,
|
||||||
RegistrationApplicationResponse,
|
RegistrationApplicationView,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
|
editRegistrationApplication,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
isBrowser,
|
myAuthRequired,
|
||||||
myAuth,
|
|
||||||
setIsoData,
|
setIsoData,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
toast,
|
|
||||||
updateRegistrationApplicationRes,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
@ -34,11 +29,11 @@ enum UnreadOrAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegistrationApplicationsState {
|
interface RegistrationApplicationsState {
|
||||||
listRegistrationApplicationsResponse?: ListRegistrationApplicationsResponse;
|
appsRes: RequestState<ListRegistrationApplicationsResponse>;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
unreadOrAll: UnreadOrAll;
|
unreadOrAll: UnreadOrAll;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegistrationApplications extends Component<
|
export class RegistrationApplications extends Component<
|
||||||
|
@ -46,47 +41,39 @@ export class RegistrationApplications extends Component<
|
||||||
RegistrationApplicationsState
|
RegistrationApplicationsState
|
||||||
> {
|
> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: RegistrationApplicationsState = {
|
state: RegistrationApplicationsState = {
|
||||||
|
appsRes: { state: "empty" },
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
unreadOrAll: UnreadOrAll.Unread,
|
unreadOrAll: UnreadOrAll.Unread,
|
||||||
page: 1,
|
page: 1,
|
||||||
loading: true,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
this.handleApproveApplication = this.handleApproveApplication.bind(this);
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path == this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listRegistrationApplicationsResponse: this.isoData
|
appsRes: this.isoData.routeData[0],
|
||||||
.routeData[0] as ListRegistrationApplicationsResponse,
|
isIsomorphic: true,
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.refetch();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
|
if (!this.state.isIsomorphic) {
|
||||||
|
await this.refetch();
|
||||||
|
}
|
||||||
setupTippy();
|
setupTippy();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
let mui = UserService.Instance.myUserInfo;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
return mui
|
return mui
|
||||||
? `@${mui.local_user_view.person.name} ${i18n.t(
|
? `@${mui.local_user_view.person.name} ${i18n.t(
|
||||||
"registration_applications"
|
"registration_applications"
|
||||||
|
@ -94,14 +81,17 @@ export class RegistrationApplications extends Component<
|
||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderApps() {
|
||||||
return (
|
switch (this.state.appsRes.state) {
|
||||||
<div className="container-lg">
|
case "loading":
|
||||||
{this.state.loading ? (
|
return (
|
||||||
<h5>
|
<h5>
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
);
|
||||||
|
case "success": {
|
||||||
|
const apps = this.state.appsRes.data.registration_applications;
|
||||||
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
|
@ -110,16 +100,20 @@ export class RegistrationApplications extends Component<
|
||||||
/>
|
/>
|
||||||
<h5 className="mb-2">{i18n.t("registration_applications")}</h5>
|
<h5 className="mb-2">{i18n.t("registration_applications")}</h5>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.applicationList()}
|
{this.applicationList(apps)}
|
||||||
<Paginator
|
<Paginator
|
||||||
page={this.state.page}
|
page={this.state.page}
|
||||||
onChange={this.handlePageChange}
|
onChange={this.handlePageChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
}
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="container-lg">{this.renderApps()}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
unreadOrAllRadios() {
|
unreadOrAllRadios() {
|
||||||
|
@ -163,22 +157,20 @@ export class RegistrationApplications extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationList() {
|
applicationList(apps: RegistrationApplicationView[]) {
|
||||||
let res = this.state.listRegistrationApplicationsResponse;
|
|
||||||
return (
|
return (
|
||||||
res && (
|
<div>
|
||||||
<div>
|
{apps.map(ra => (
|
||||||
{res.registration_applications.map(ra => (
|
<>
|
||||||
<>
|
<hr />
|
||||||
<hr />
|
<RegistrationApplication
|
||||||
<RegistrationApplication
|
key={ra.registration_application.id}
|
||||||
key={ra.registration_application.id}
|
application={ra}
|
||||||
application={ra}
|
onApproveApplication={this.handleApproveApplication}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,65 +184,54 @@ export class RegistrationApplications extends Component<
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
static fetchInitialData({
|
||||||
let promises: Promise<any>[] = [];
|
auth,
|
||||||
|
client,
|
||||||
|
}: InitialFetchRequest): Promise<any>[] {
|
||||||
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
|
|
||||||
let auth = req.auth;
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
let form: ListRegistrationApplications = {
|
const form: ListRegistrationApplications = {
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(req.client.listRegistrationApplications(form));
|
promises.push(client.listRegistrationApplications(form));
|
||||||
|
} else {
|
||||||
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
async refetch() {
|
||||||
let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
|
const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
|
||||||
let auth = myAuth();
|
this.setState({
|
||||||
if (auth) {
|
appsRes: { state: "loading" },
|
||||||
let form: ListRegistrationApplications = {
|
});
|
||||||
|
this.setState({
|
||||||
|
appsRes: await HttpService.client.listRegistrationApplications({
|
||||||
unread_only: unread_only,
|
unread_only: unread_only,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
}),
|
||||||
WebSocketService.Instance.send(
|
});
|
||||||
wsClient.listRegistrationApplications(form)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
async handleApproveApplication(form: ApproveRegistrationApplication) {
|
||||||
let op = wsUserOp(msg);
|
const approveRes = await HttpService.client.approveRegistrationApplication(
|
||||||
console.log(msg);
|
form
|
||||||
if (msg.error) {
|
);
|
||||||
toast(i18n.t(msg.error), "danger");
|
this.setState(s => {
|
||||||
return;
|
if (s.appsRes.state == "success" && approveRes.state == "success") {
|
||||||
} else if (msg.reconnect) {
|
s.appsRes.data.registration_applications = editRegistrationApplication(
|
||||||
this.refetch();
|
approveRes.data.registration_application,
|
||||||
} else if (op == UserOperation.ListRegistrationApplications) {
|
s.appsRes.data.registration_applications
|
||||||
let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg);
|
);
|
||||||
this.setState({
|
}
|
||||||
listRegistrationApplicationsResponse: data,
|
return s;
|
||||||
loading: false,
|
});
|
||||||
});
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
} else if (op == UserOperation.ApproveRegistrationApplication) {
|
|
||||||
let data = wsJsonToRes<RegistrationApplicationResponse>(msg);
|
|
||||||
updateRegistrationApplicationRes(
|
|
||||||
data.registration_application,
|
|
||||||
this.state.listRegistrationApplicationsResponse
|
|
||||||
?.registration_applications
|
|
||||||
);
|
|
||||||
let uacs = UserService.Instance.unreadApplicationCountSub;
|
|
||||||
// Minor bug, where if the application switches from deny to approve, the count will still go down
|
|
||||||
uacs.next(uacs.getValue() - 1);
|
|
||||||
this.setState(this.state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,27 +13,23 @@ import {
|
||||||
PostReportView,
|
PostReportView,
|
||||||
PrivateMessageReportResponse,
|
PrivateMessageReportResponse,
|
||||||
PrivateMessageReportView,
|
PrivateMessageReportView,
|
||||||
UserOperation,
|
ResolveCommentReport,
|
||||||
wsJsonToRes,
|
ResolvePostReport,
|
||||||
wsUserOp,
|
ResolvePrivateMessageReport,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { HttpService, UserService } from "../../services";
|
||||||
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import { RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
amAdmin,
|
amAdmin,
|
||||||
|
editCommentReport,
|
||||||
|
editPostReport,
|
||||||
|
editPrivateMessageReport,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
isBrowser,
|
myAuthRequired,
|
||||||
myAuth,
|
|
||||||
setIsoData,
|
setIsoData,
|
||||||
setupTippy,
|
|
||||||
toast,
|
|
||||||
updateCommentReportRes,
|
|
||||||
updatePostReportRes,
|
|
||||||
updatePrivateMessageReportRes,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { CommentReport } from "../comment/comment-report";
|
import { CommentReport } from "../comment/comment-report";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
|
@ -68,71 +64,67 @@ type ItemType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ReportsState {
|
interface ReportsState {
|
||||||
listCommentReportsResponse?: ListCommentReportsResponse;
|
commentReportsRes: RequestState<ListCommentReportsResponse>;
|
||||||
listPostReportsResponse?: ListPostReportsResponse;
|
postReportsRes: RequestState<ListPostReportsResponse>;
|
||||||
listPrivateMessageReportsResponse?: ListPrivateMessageReportsResponse;
|
messageReportsRes: RequestState<ListPrivateMessageReportsResponse>;
|
||||||
unreadOrAll: UnreadOrAll;
|
unreadOrAll: UnreadOrAll;
|
||||||
messageType: MessageType;
|
messageType: MessageType;
|
||||||
combined: ItemType[];
|
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Reports extends Component<any, ReportsState> {
|
export class Reports extends Component<any, ReportsState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: ReportsState = {
|
state: ReportsState = {
|
||||||
|
commentReportsRes: { state: "empty" },
|
||||||
|
postReportsRes: { state: "empty" },
|
||||||
|
messageReportsRes: { state: "empty" },
|
||||||
unreadOrAll: UnreadOrAll.Unread,
|
unreadOrAll: UnreadOrAll.Unread,
|
||||||
messageType: MessageType.All,
|
messageType: MessageType.All,
|
||||||
combined: [],
|
|
||||||
page: 1,
|
page: 1,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
loading: true,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
this.handleResolveCommentReport =
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.handleResolveCommentReport.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.handleResolvePostReport = this.handleResolvePostReport.bind(this);
|
||||||
|
this.handleResolvePrivateMessageReport =
|
||||||
|
this.handleResolvePrivateMessageReport.bind(this);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path == this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
|
const [commentReportsRes, postReportsRes, messageReportsRes] =
|
||||||
|
this.isoData.routeData;
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listCommentReportsResponse: this.isoData
|
commentReportsRes,
|
||||||
.routeData[0] as ListCommentReportsResponse,
|
postReportsRes,
|
||||||
listPostReportsResponse: this.isoData
|
isIsomorphic: true,
|
||||||
.routeData[1] as ListPostReportsResponse,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (amAdmin()) {
|
if (amAdmin()) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listPrivateMessageReportsResponse: this.isoData
|
messageReportsRes,
|
||||||
.routeData[2] as ListPrivateMessageReportsResponse,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
combined: this.buildCombined(),
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.refetch();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentDidMount() {
|
||||||
if (isBrowser()) {
|
if (!this.state.isIsomorphic) {
|
||||||
this.subscription?.unsubscribe();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
let mui = UserService.Instance.myUserInfo;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
return mui
|
return mui
|
||||||
? `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
|
? `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
|
||||||
this.state.siteRes.site_view.site.name
|
this.state.siteRes.site_view.site.name
|
||||||
|
@ -143,37 +135,46 @@ export class Reports extends Component<any, ReportsState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
{this.state.loading ? (
|
<div className="row">
|
||||||
<h5>
|
<div className="col-12">
|
||||||
<Spinner large />
|
<HtmlTags
|
||||||
</h5>
|
title={this.documentTitle}
|
||||||
) : (
|
path={this.context.router.route.match.url}
|
||||||
<div className="row">
|
/>
|
||||||
<div className="col-12">
|
<h5 className="mb-2">{i18n.t("reports")}</h5>
|
||||||
<HtmlTags
|
{this.selects()}
|
||||||
title={this.documentTitle}
|
{this.section}
|
||||||
path={this.context.router.route.match.url}
|
<Paginator
|
||||||
/>
|
page={this.state.page}
|
||||||
<h5 className="mb-2">{i18n.t("reports")}</h5>
|
onChange={this.handlePageChange}
|
||||||
{this.selects()}
|
/>
|
||||||
{this.state.messageType == MessageType.All && this.all()}
|
|
||||||
{this.state.messageType == MessageType.CommentReport &&
|
|
||||||
this.commentReports()}
|
|
||||||
{this.state.messageType == MessageType.PostReport &&
|
|
||||||
this.postReports()}
|
|
||||||
{this.state.messageType == MessageType.PrivateMessageReport &&
|
|
||||||
this.privateMessageReports()}
|
|
||||||
<Paginator
|
|
||||||
page={this.state.page}
|
|
||||||
onChange={this.handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get section() {
|
||||||
|
switch (this.state.messageType) {
|
||||||
|
case MessageType.All: {
|
||||||
|
return this.all();
|
||||||
|
}
|
||||||
|
case MessageType.CommentReport: {
|
||||||
|
return this.commentReports();
|
||||||
|
}
|
||||||
|
case MessageType.PostReport: {
|
||||||
|
return this.postReports();
|
||||||
|
}
|
||||||
|
case MessageType.PrivateMessageReport: {
|
||||||
|
return this.privateMessageReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unreadOrAllRadios() {
|
unreadOrAllRadios() {
|
||||||
return (
|
return (
|
||||||
<div className="btn-group btn-group-toggle flex-wrap mb-2">
|
<div className="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
|
@ -309,23 +310,25 @@ export class Reports extends Component<any, ReportsState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCombined(): ItemType[] {
|
get buildCombined(): ItemType[] {
|
||||||
// let comments: ItemType[] = this.state.listCommentReportsResponse
|
const commentRes = this.state.commentReportsRes;
|
||||||
// .map(r => r.comment_reports)
|
const comments =
|
||||||
// .unwrapOr([])
|
commentRes.state == "success"
|
||||||
// .map(r => this.commentReportToItemType(r));
|
? commentRes.data.comment_reports.map(this.commentReportToItemType)
|
||||||
let comments =
|
: [];
|
||||||
this.state.listCommentReportsResponse?.comment_reports.map(
|
|
||||||
this.commentReportToItemType
|
const postRes = this.state.postReportsRes;
|
||||||
) ?? [];
|
const posts =
|
||||||
let posts =
|
postRes.state == "success"
|
||||||
this.state.listPostReportsResponse?.post_reports.map(
|
? postRes.data.post_reports.map(this.postReportToItemType)
|
||||||
this.postReportToItemType
|
: [];
|
||||||
) ?? [];
|
const pmRes = this.state.messageReportsRes;
|
||||||
let privateMessages =
|
const privateMessages =
|
||||||
this.state.listPrivateMessageReportsResponse?.private_message_reports.map(
|
pmRes.state == "success"
|
||||||
this.privateMessageReportToItemType
|
? pmRes.data.private_message_reports.map(
|
||||||
) ?? [];
|
this.privateMessageReportToItemType
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
return [...comments, ...posts, ...privateMessages].sort((a, b) =>
|
return [...comments, ...posts, ...privateMessages].sort((a, b) =>
|
||||||
b.published.localeCompare(a.published)
|
b.published.localeCompare(a.published)
|
||||||
|
@ -336,15 +339,26 @@ export class Reports extends Component<any, ReportsState> {
|
||||||
switch (i.type_) {
|
switch (i.type_) {
|
||||||
case MessageEnum.CommentReport:
|
case MessageEnum.CommentReport:
|
||||||
return (
|
return (
|
||||||
<CommentReport key={i.id} report={i.view as CommentReportView} />
|
<CommentReport
|
||||||
|
key={i.id}
|
||||||
|
report={i.view as CommentReportView}
|
||||||
|
onResolveReport={this.handleResolveCommentReport}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
case MessageEnum.PostReport:
|
case MessageEnum.PostReport:
|
||||||
return <PostReport key={i.id} report={i.view as PostReportView} />;
|
return (
|
||||||
|
<PostReport
|
||||||
|
key={i.id}
|
||||||
|
report={i.view as PostReportView}
|
||||||
|
onResolveReport={this.handleResolvePostReport}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case MessageEnum.PrivateMessageReport:
|
case MessageEnum.PrivateMessageReport:
|
||||||
return (
|
return (
|
||||||
<PrivateMessageReport
|
<PrivateMessageReport
|
||||||
key={i.id}
|
key={i.id}
|
||||||
report={i.view as PrivateMessageReportView}
|
report={i.view as PrivateMessageReportView}
|
||||||
|
onResolveReport={this.handleResolvePrivateMessageReport}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
@ -355,7 +369,7 @@ export class Reports extends Component<any, ReportsState> {
|
||||||
all() {
|
all() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.combined.map(i => (
|
{this.buildCombined.map(i => (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
{this.renderItemType(i)}
|
{this.renderItemType(i)}
|
||||||
|
@ -366,219 +380,245 @@ export class Reports extends Component<any, ReportsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
commentReports() {
|
commentReports() {
|
||||||
let reports = this.state.listCommentReportsResponse?.comment_reports;
|
const res = this.state.commentReportsRes;
|
||||||
return (
|
switch (res.state) {
|
||||||
reports && (
|
case "loading":
|
||||||
<div>
|
return (
|
||||||
{reports.map(cr => (
|
<h5>
|
||||||
<>
|
<Spinner large />
|
||||||
<hr />
|
</h5>
|
||||||
<CommentReport key={cr.comment_report.id} report={cr} />
|
);
|
||||||
</>
|
case "success": {
|
||||||
))}
|
const reports = res.data.comment_reports;
|
||||||
</div>
|
return (
|
||||||
)
|
<div>
|
||||||
);
|
{reports.map(cr => (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<CommentReport
|
||||||
|
key={cr.comment_report.id}
|
||||||
|
report={cr}
|
||||||
|
onResolveReport={this.handleResolveCommentReport}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postReports() {
|
postReports() {
|
||||||
let reports = this.state.listPostReportsResponse?.post_reports;
|
const res = this.state.postReportsRes;
|
||||||
return (
|
switch (res.state) {
|
||||||
reports && (
|
case "loading":
|
||||||
<div>
|
return (
|
||||||
{reports.map(pr => (
|
<h5>
|
||||||
<>
|
<Spinner large />
|
||||||
<hr />
|
</h5>
|
||||||
<PostReport key={pr.post_report.id} report={pr} />
|
);
|
||||||
</>
|
case "success": {
|
||||||
))}
|
const reports = res.data.post_reports;
|
||||||
</div>
|
return (
|
||||||
)
|
<div>
|
||||||
);
|
{reports.map(pr => (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<PostReport
|
||||||
|
key={pr.post_report.id}
|
||||||
|
report={pr}
|
||||||
|
onResolveReport={this.handleResolvePostReport}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
privateMessageReports() {
|
privateMessageReports() {
|
||||||
let reports =
|
const res = this.state.messageReportsRes;
|
||||||
this.state.listPrivateMessageReportsResponse?.private_message_reports;
|
switch (res.state) {
|
||||||
return (
|
case "loading":
|
||||||
reports && (
|
return (
|
||||||
<div>
|
<h5>
|
||||||
{reports.map(pmr => (
|
<Spinner large />
|
||||||
<>
|
</h5>
|
||||||
<hr />
|
);
|
||||||
<PrivateMessageReport
|
case "success": {
|
||||||
key={pmr.private_message_report.id}
|
const reports = res.data.private_message_reports;
|
||||||
report={pmr}
|
return (
|
||||||
/>
|
<div>
|
||||||
</>
|
{reports.map(pmr => (
|
||||||
))}
|
<>
|
||||||
</div>
|
<hr />
|
||||||
)
|
<PrivateMessageReport
|
||||||
);
|
key={pmr.private_message_report.id}
|
||||||
|
report={pmr}
|
||||||
|
onResolveReport={this.handleResolvePrivateMessageReport}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePageChange(page: number) {
|
async handlePageChange(page: number) {
|
||||||
this.setState({ page });
|
this.setState({ page });
|
||||||
this.refetch();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnreadOrAllChange(i: Reports, event: any) {
|
async handleUnreadOrAllChange(i: Reports, event: any) {
|
||||||
i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
|
i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
|
||||||
i.refetch();
|
await i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessageTypeChange(i: Reports, event: any) {
|
async handleMessageTypeChange(i: Reports, event: any) {
|
||||||
i.setState({ messageType: Number(event.target.value), page: 1 });
|
i.setState({ messageType: Number(event.target.value), page: 1 });
|
||||||
i.refetch();
|
await i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
static fetchInitialData({
|
||||||
let promises: Promise<any>[] = [];
|
auth,
|
||||||
|
client,
|
||||||
|
}: InitialFetchRequest): Promise<any>[] {
|
||||||
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
|
|
||||||
let unresolved_only = true;
|
const unresolved_only = true;
|
||||||
let page = 1;
|
const page = 1;
|
||||||
let limit = fetchLimit;
|
const limit = fetchLimit;
|
||||||
let auth = req.auth;
|
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
let commentReportsForm: ListCommentReports = {
|
const commentReportsForm: ListCommentReports = {
|
||||||
unresolved_only,
|
unresolved_only,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(req.client.listCommentReports(commentReportsForm));
|
promises.push(client.listCommentReports(commentReportsForm));
|
||||||
|
|
||||||
let postReportsForm: ListPostReports = {
|
const postReportsForm: ListPostReports = {
|
||||||
unresolved_only,
|
unresolved_only,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(req.client.listPostReports(postReportsForm));
|
promises.push(client.listPostReports(postReportsForm));
|
||||||
|
|
||||||
if (amAdmin()) {
|
if (amAdmin()) {
|
||||||
let privateMessageReportsForm: ListPrivateMessageReports = {
|
const privateMessageReportsForm: ListPrivateMessageReports = {
|
||||||
unresolved_only,
|
unresolved_only,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(
|
promises.push(
|
||||||
req.client.listPrivateMessageReports(privateMessageReportsForm)
|
client.listPrivateMessageReports(privateMessageReportsForm)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
promises.push(
|
||||||
|
Promise.resolve({ state: "empty" }),
|
||||||
|
Promise.resolve({ state: "empty" }),
|
||||||
|
Promise.resolve({ state: "empty" })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
async refetch() {
|
||||||
let unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
|
const unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
|
||||||
let page = this.state.page;
|
const page = this.state.page;
|
||||||
let limit = fetchLimit;
|
const limit = fetchLimit;
|
||||||
let auth = myAuth();
|
const auth = myAuthRequired();
|
||||||
if (auth) {
|
|
||||||
let commentReportsForm: ListCommentReports = {
|
|
||||||
unresolved_only,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.listCommentReports(commentReportsForm)
|
|
||||||
);
|
|
||||||
|
|
||||||
let postReportsForm: ListPostReports = {
|
this.setState({
|
||||||
unresolved_only,
|
commentReportsRes: { state: "loading" },
|
||||||
page,
|
postReportsRes: { state: "loading" },
|
||||||
limit,
|
messageReportsRes: { state: "loading" },
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
|
|
||||||
|
|
||||||
if (amAdmin()) {
|
const form:
|
||||||
let privateMessageReportsForm: ListPrivateMessageReports = {
|
| ListCommentReports
|
||||||
unresolved_only,
|
| ListPostReports
|
||||||
page,
|
| ListPrivateMessageReports = {
|
||||||
limit,
|
unresolved_only,
|
||||||
auth,
|
page,
|
||||||
};
|
limit,
|
||||||
WebSocketService.Instance.send(
|
auth,
|
||||||
wsClient.listPrivateMessageReports(privateMessageReportsForm)
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
commentReportsRes: await HttpService.client.listCommentReports(form),
|
||||||
|
postReportsRes: await HttpService.client.listPostReports(form),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (amAdmin()) {
|
||||||
|
this.setState({
|
||||||
|
messageReportsRes: await HttpService.client.listPrivateMessageReports(
|
||||||
|
form
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleResolveCommentReport(form: ResolveCommentReport) {
|
||||||
|
const res = await HttpService.client.resolveCommentReport(form);
|
||||||
|
this.findAndUpdateCommentReport(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleResolvePostReport(form: ResolvePostReport) {
|
||||||
|
const res = await HttpService.client.resolvePostReport(form);
|
||||||
|
this.findAndUpdatePostReport(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) {
|
||||||
|
const res = await HttpService.client.resolvePrivateMessageReport(form);
|
||||||
|
this.findAndUpdatePrivateMessageReport(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) {
|
||||||
|
this.setState(s => {
|
||||||
|
if (s.commentReportsRes.state == "success" && res.state == "success") {
|
||||||
|
s.commentReportsRes.data.comment_reports = editCommentReport(
|
||||||
|
res.data.comment_report_view,
|
||||||
|
s.commentReportsRes.data.comment_reports
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
return s;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
findAndUpdatePostReport(res: RequestState<PostReportResponse>) {
|
||||||
let op = wsUserOp(msg);
|
this.setState(s => {
|
||||||
console.log(msg);
|
if (s.postReportsRes.state == "success" && res.state == "success") {
|
||||||
if (msg.error) {
|
s.postReportsRes.data.post_reports = editPostReport(
|
||||||
toast(i18n.t(msg.error), "danger");
|
res.data.post_report_view,
|
||||||
return;
|
s.postReportsRes.data.post_reports
|
||||||
} else if (msg.reconnect) {
|
);
|
||||||
this.refetch();
|
|
||||||
} else if (op == UserOperation.ListCommentReports) {
|
|
||||||
let data = wsJsonToRes<ListCommentReportsResponse>(msg);
|
|
||||||
this.setState({ listCommentReportsResponse: data });
|
|
||||||
this.setState({ combined: this.buildCombined(), loading: false });
|
|
||||||
// this.sendUnreadCount();
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
setupTippy();
|
|
||||||
} else if (op == UserOperation.ListPostReports) {
|
|
||||||
let data = wsJsonToRes<ListPostReportsResponse>(msg);
|
|
||||||
this.setState({ listPostReportsResponse: data });
|
|
||||||
this.setState({ combined: this.buildCombined(), loading: false });
|
|
||||||
// this.sendUnreadCount();
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
setupTippy();
|
|
||||||
} else if (op == UserOperation.ListPrivateMessageReports) {
|
|
||||||
let data = wsJsonToRes<ListPrivateMessageReportsResponse>(msg);
|
|
||||||
this.setState({ listPrivateMessageReportsResponse: data });
|
|
||||||
this.setState({ combined: this.buildCombined(), loading: false });
|
|
||||||
// this.sendUnreadCount();
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
setupTippy();
|
|
||||||
} else if (op == UserOperation.ResolvePostReport) {
|
|
||||||
let data = wsJsonToRes<PostReportResponse>(msg);
|
|
||||||
updatePostReportRes(
|
|
||||||
data.post_report_view,
|
|
||||||
this.state.listPostReportsResponse?.post_reports
|
|
||||||
);
|
|
||||||
let urcs = UserService.Instance.unreadReportCountSub;
|
|
||||||
if (data.post_report_view.post_report.resolved) {
|
|
||||||
urcs.next(urcs.getValue() - 1);
|
|
||||||
} else {
|
|
||||||
urcs.next(urcs.getValue() + 1);
|
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
return s;
|
||||||
} else if (op == UserOperation.ResolveCommentReport) {
|
});
|
||||||
let data = wsJsonToRes<CommentReportResponse>(msg);
|
}
|
||||||
updateCommentReportRes(
|
|
||||||
data.comment_report_view,
|
findAndUpdatePrivateMessageReport(
|
||||||
this.state.listCommentReportsResponse?.comment_reports
|
res: RequestState<PrivateMessageReportResponse>
|
||||||
);
|
) {
|
||||||
let urcs = UserService.Instance.unreadReportCountSub;
|
this.setState(s => {
|
||||||
if (data.comment_report_view.comment_report.resolved) {
|
if (s.messageReportsRes.state == "success" && res.state == "success") {
|
||||||
urcs.next(urcs.getValue() - 1);
|
s.messageReportsRes.data.private_message_reports =
|
||||||
} else {
|
editPrivateMessageReport(
|
||||||
urcs.next(urcs.getValue() + 1);
|
res.data.private_message_report_view,
|
||||||
|
s.messageReportsRes.data.private_message_reports
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
return s;
|
||||||
} else if (op == UserOperation.ResolvePrivateMessageReport) {
|
});
|
||||||
let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
|
|
||||||
updatePrivateMessageReportRes(
|
|
||||||
data.private_message_report_view,
|
|
||||||
this.state.listPrivateMessageReportsResponse?.private_message_reports
|
|
||||||
);
|
|
||||||
let urcs = UserService.Instance.unreadReportCountSub;
|
|
||||||
if (data.private_message_report_view.private_message_report.resolved) {
|
|
||||||
urcs.next(urcs.getValue() - 1);
|
|
||||||
} else {
|
|
||||||
urcs.next(urcs.getValue() + 1);
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,19 @@
|
||||||
import { NoOptionI18nKeys } from "i18next";
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
BlockCommunity,
|
|
||||||
BlockCommunityResponse,
|
BlockCommunityResponse,
|
||||||
BlockPerson,
|
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
ChangePassword,
|
|
||||||
CommunityBlockView,
|
CommunityBlockView,
|
||||||
DeleteAccount,
|
DeleteAccountResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
PersonBlockView,
|
PersonBlockView,
|
||||||
SaveUserSettings,
|
|
||||||
SortType,
|
SortType,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n, languages } from "../../i18next";
|
import { i18n, languages } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
Choice,
|
Choice,
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
|
@ -34,6 +27,7 @@ import {
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
getLanguages,
|
getLanguages,
|
||||||
myAuth,
|
myAuth,
|
||||||
|
myAuthRequired,
|
||||||
personToChoice,
|
personToChoice,
|
||||||
relTags,
|
relTags,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
|
@ -43,8 +37,6 @@ import {
|
||||||
toast,
|
toast,
|
||||||
updateCommunityBlock,
|
updateCommunityBlock,
|
||||||
updatePersonBlock,
|
updatePersonBlock,
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
|
@ -59,6 +51,9 @@ import { CommunityLink } from "../community/community-link";
|
||||||
import { PersonListing } from "./person-listing";
|
import { PersonListing } from "./person-listing";
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
|
saveRes: RequestState<LoginResponse>;
|
||||||
|
changePasswordRes: RequestState<LoginResponse>;
|
||||||
|
deleteAccountRes: RequestState<DeleteAccountResponse>;
|
||||||
// TODO redo these forms
|
// TODO redo these forms
|
||||||
saveUserSettingsForm: {
|
saveUserSettingsForm: {
|
||||||
show_nsfw?: boolean;
|
show_nsfw?: boolean;
|
||||||
|
@ -94,9 +89,6 @@ interface SettingsState {
|
||||||
communityBlocks: CommunityBlockView[];
|
communityBlocks: CommunityBlockView[];
|
||||||
currentTab: string;
|
currentTab: string;
|
||||||
themeList: string[];
|
themeList: string[];
|
||||||
saveUserSettingsLoading: boolean;
|
|
||||||
changePasswordLoading: boolean;
|
|
||||||
deleteAccountLoading: boolean;
|
|
||||||
deleteAccountShowConfirm: boolean;
|
deleteAccountShowConfirm: boolean;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
searchCommunityLoading: boolean;
|
searchCommunityLoading: boolean;
|
||||||
|
@ -143,13 +135,12 @@ const Filter = ({
|
||||||
|
|
||||||
export class Settings extends Component<any, SettingsState> {
|
export class Settings extends Component<any, SettingsState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: SettingsState = {
|
state: SettingsState = {
|
||||||
|
saveRes: { state: "empty" },
|
||||||
|
deleteAccountRes: { state: "empty" },
|
||||||
|
changePasswordRes: { state: "empty" },
|
||||||
saveUserSettingsForm: {},
|
saveUserSettingsForm: {},
|
||||||
changePasswordForm: {},
|
changePasswordForm: {},
|
||||||
saveUserSettingsLoading: false,
|
|
||||||
changePasswordLoading: false,
|
|
||||||
deleteAccountLoading: false,
|
|
||||||
deleteAccountShowConfirm: false,
|
deleteAccountShowConfirm: false,
|
||||||
deleteAccountForm: {},
|
deleteAccountForm: {},
|
||||||
personBlocks: [],
|
personBlocks: [],
|
||||||
|
@ -180,8 +171,8 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
this.userSettings = this.userSettings.bind(this);
|
this.userSettings = this.userSettings.bind(this);
|
||||||
this.blockCards = this.blockCards.bind(this);
|
this.blockCards = this.blockCards.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.handleBlockPerson = this.handleBlockPerson.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
|
||||||
|
|
||||||
const mui = UserService.Instance.myUserInfo;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
if (mui) {
|
if (mui) {
|
||||||
|
@ -245,10 +236,6 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
this.setState({ themeList: await fetchThemeList() });
|
this.setState({ themeList: await fetchThemeList() });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return i18n.t("settings");
|
return i18n.t("settings");
|
||||||
}
|
}
|
||||||
|
@ -375,7 +362,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-block btn-secondary mr-4">
|
<button type="submit" className="btn btn-block btn-secondary mr-4">
|
||||||
{this.state.changePasswordLoading ? (
|
{this.state.changePasswordRes.state === "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
capitalizeFirstLetter(i18n.t("save"))
|
capitalizeFirstLetter(i18n.t("save"))
|
||||||
|
@ -476,7 +463,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveUserSettingsHtmlForm() {
|
saveUserSettingsHtmlForm() {
|
||||||
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
|
const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -604,6 +591,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
selectedLanguageIds={selectedLangs}
|
selectedLanguageIds={selectedLangs}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
|
showLanguageWarning={true}
|
||||||
showSite
|
showSite
|
||||||
onChange={this.handleDiscussionLanguageChange}
|
onChange={this.handleDiscussionLanguageChange}
|
||||||
/>
|
/>
|
||||||
|
@ -790,7 +778,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
{this.totpSection()}
|
{this.totpSection()}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-block btn-secondary mr-4">
|
<button type="submit" className="btn btn-block btn-secondary mr-4">
|
||||||
{this.state.saveUserSettingsLoading ? (
|
{this.state.saveRes.state === "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
capitalizeFirstLetter(i18n.t("save"))
|
capitalizeFirstLetter(i18n.t("save"))
|
||||||
|
@ -829,7 +817,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
disabled={!this.state.deleteAccountForm.password}
|
disabled={!this.state.deleteAccountForm.password}
|
||||||
onClick={linkEvent(this, this.handleDeleteAccount)}
|
onClick={linkEvent(this, this.handleDeleteAccount)}
|
||||||
>
|
>
|
||||||
{this.state.deleteAccountLoading ? (
|
{this.state.deleteAccountRes.state === "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
capitalizeFirstLetter(i18n.t("delete"))
|
capitalizeFirstLetter(i18n.t("delete"))
|
||||||
|
@ -853,7 +841,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
totpSection() {
|
totpSection() {
|
||||||
let totpUrl =
|
const totpUrl =
|
||||||
UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
|
UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -910,9 +898,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
const searchPersonOptions: Choice[] = [];
|
const searchPersonOptions: Choice[] = [];
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
searchPersonOptions.push(
|
searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice));
|
||||||
...(await fetchUsers(text)).users.map(personToChoice)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -928,7 +914,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
searchCommunityOptions.push(
|
searchCommunityOptions.push(
|
||||||
...(await fetchCommunities(text)).communities.map(communityToChoice)
|
...(await fetchCommunities(text)).map(communityToChoice)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -938,137 +924,146 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
handleBlockPerson({ value }: Choice) {
|
async handleBlockPerson({ value }: Choice) {
|
||||||
const auth = myAuth();
|
if (value !== "0") {
|
||||||
if (auth && value !== "0") {
|
const res = await HttpService.client.blockPerson({
|
||||||
const blockUserForm: BlockPerson = {
|
|
||||||
person_id: Number(value),
|
person_id: Number(value),
|
||||||
block: true,
|
block: true,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
|
this.personBlock(res);
|
||||||
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
|
async handleUnblockPerson({
|
||||||
const auth = myAuth();
|
ctx,
|
||||||
if (auth) {
|
recipientId,
|
||||||
const blockUserForm: BlockPerson = {
|
}: {
|
||||||
person_id: i.recipientId,
|
ctx: Settings;
|
||||||
block: false,
|
recipientId: number;
|
||||||
auth,
|
}) {
|
||||||
};
|
const res = await HttpService.client.blockPerson({
|
||||||
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
|
person_id: recipientId,
|
||||||
}
|
block: false,
|
||||||
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
ctx.personBlock(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlockCommunity({ value }: Choice) {
|
async handleBlockCommunity({ value }: Choice) {
|
||||||
const auth = myAuth();
|
if (value !== "0") {
|
||||||
if (auth && value !== "0") {
|
const res = await HttpService.client.blockCommunity({
|
||||||
const blockCommunityForm: BlockCommunity = {
|
|
||||||
community_id: Number(value),
|
community_id: Number(value),
|
||||||
block: true,
|
block: true,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(
|
this.communityBlock(res);
|
||||||
wsClient.blockCommunity(blockCommunityForm)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
|
async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
|
||||||
const auth = myAuth();
|
const auth = myAuth();
|
||||||
if (auth) {
|
if (auth) {
|
||||||
const blockCommunityForm: BlockCommunity = {
|
const res = await HttpService.client.blockCommunity({
|
||||||
community_id: i.communityId,
|
community_id: i.communityId,
|
||||||
block: false,
|
block: false,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(
|
i.ctx.communityBlock(res);
|
||||||
wsClient.blockCommunity(blockCommunityForm)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowNsfwChange(i: Settings, event: any) {
|
handleShowNsfwChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowAvatarsChange(i: Settings, event: any) {
|
handleShowAvatarsChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_avatars = event.target.checked;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
if (mui) {
|
||||||
mui.local_user_view.local_user.show_avatars = event.target.checked;
|
mui.local_user_view.local_user.show_avatars = event.target.checked;
|
||||||
}
|
}
|
||||||
i.setState(i.state);
|
i.setState(
|
||||||
|
s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBotAccount(i: Settings, event: any) {
|
handleBotAccount(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.bot_account = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowBotAccounts(i: Settings, event: any) {
|
handleShowBotAccounts(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => (
|
||||||
|
(s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReadPosts(i: Settings, event: any) {
|
handleReadPosts(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowNewPostNotifs(i: Settings, event: any) {
|
handleShowNewPostNotifs(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => (
|
||||||
|
(s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowScoresChange(i: Settings, event: any) {
|
handleShowScoresChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.show_scores = event.target.checked;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
if (mui) {
|
||||||
mui.local_user_view.local_user.show_scores = event.target.checked;
|
mui.local_user_view.local_user.show_scores = event.target.checked;
|
||||||
}
|
}
|
||||||
i.setState(i.state);
|
i.setState(
|
||||||
|
s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGenerateTotp(i: Settings, event: any) {
|
handleGenerateTotp(i: Settings, event: any) {
|
||||||
// Coerce false to undefined here, so it won't generate it.
|
// Coerce false to undefined here, so it won't generate it.
|
||||||
let checked: boolean | undefined = event.target.checked || undefined;
|
const checked: boolean | undefined = event.target.checked || undefined;
|
||||||
if (checked) {
|
if (checked) {
|
||||||
toast(i18n.t("two_factor_setup_instructions"));
|
toast(i18n.t("two_factor_setup_instructions"));
|
||||||
}
|
}
|
||||||
i.state.saveUserSettingsForm.generate_totp_2fa = checked;
|
i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveTotp(i: Settings, event: any) {
|
handleRemoveTotp(i: Settings, event: any) {
|
||||||
// Coerce true to undefined here, so it won't generate it.
|
// Coerce true to undefined here, so it won't generate it.
|
||||||
let checked: boolean | undefined = !event.target.checked && undefined;
|
const checked: boolean | undefined = !event.target.checked && undefined;
|
||||||
i.state.saveUserSettingsForm.generate_totp_2fa = checked;
|
i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSendNotificationsToEmailChange(i: Settings, event: any) {
|
handleSendNotificationsToEmailChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.send_notifications_to_email =
|
i.setState(
|
||||||
event.target.checked;
|
s => (
|
||||||
i.setState(i.state);
|
(s.saveUserSettingsForm.send_notifications_to_email =
|
||||||
|
event.target.checked),
|
||||||
|
s
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleThemeChange(i: Settings, event: any) {
|
handleThemeChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.theme = event.target.value;
|
i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s));
|
||||||
setTheme(event.target.value, true);
|
setTheme(event.target.value, true);
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInterfaceLangChange(i: Settings, event: any) {
|
handleInterfaceLangChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.interface_language = event.target.value;
|
i.setState(
|
||||||
|
s => ((s.saveUserSettingsForm.interface_language = event.target.value), s)
|
||||||
|
);
|
||||||
i18n.changeLanguage(
|
i18n.changeLanguage(
|
||||||
getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
|
getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
|
||||||
);
|
);
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDiscussionLanguageChange(val: number[]) {
|
handleDiscussionLanguageChange(val: number[]) {
|
||||||
|
@ -1088,8 +1083,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEmailChange(i: Settings, event: any) {
|
handleEmailChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.email = event.target.value;
|
i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBioChange(val: string) {
|
handleBioChange(val: string) {
|
||||||
|
@ -1113,90 +1107,100 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisplayNameChange(i: Settings, event: any) {
|
handleDisplayNameChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.display_name = event.target.value;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.saveUserSettingsForm.display_name = event.target.value), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMatrixUserIdChange(i: Settings, event: any) {
|
handleMatrixUserIdChange(i: Settings, event: any) {
|
||||||
i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
|
i.setState(
|
||||||
i.setState(i.state);
|
s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewPasswordChange(i: Settings, event: any) {
|
handleNewPasswordChange(i: Settings, event: any) {
|
||||||
i.state.changePasswordForm.new_password = event.target.value;
|
const newPass: string | undefined =
|
||||||
if (i.state.changePasswordForm.new_password == "") {
|
event.target.value == "" ? undefined : event.target.value;
|
||||||
i.state.changePasswordForm.new_password = undefined;
|
i.setState(s => ((s.changePasswordForm.new_password = newPass), s));
|
||||||
}
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewPasswordVerifyChange(i: Settings, event: any) {
|
handleNewPasswordVerifyChange(i: Settings, event: any) {
|
||||||
i.state.changePasswordForm.new_password_verify = event.target.value;
|
const newPassVerify: string | undefined =
|
||||||
if (i.state.changePasswordForm.new_password_verify == "") {
|
event.target.value == "" ? undefined : event.target.value;
|
||||||
i.state.changePasswordForm.new_password_verify = undefined;
|
i.setState(
|
||||||
}
|
s => ((s.changePasswordForm.new_password_verify = newPassVerify), s)
|
||||||
i.setState(i.state);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOldPasswordChange(i: Settings, event: any) {
|
handleOldPasswordChange(i: Settings, event: any) {
|
||||||
i.state.changePasswordForm.old_password = event.target.value;
|
const oldPass: string | undefined =
|
||||||
if (i.state.changePasswordForm.old_password == "") {
|
event.target.value == "" ? undefined : event.target.value;
|
||||||
i.state.changePasswordForm.old_password = undefined;
|
i.setState(s => ((s.changePasswordForm.old_password = oldPass), s));
|
||||||
}
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveSettingsSubmit(i: Settings, event: any) {
|
async handleSaveSettingsSubmit(i: Settings, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ saveUserSettingsLoading: true });
|
i.setState({ saveRes: { state: "loading" } });
|
||||||
let auth = myAuth();
|
|
||||||
if (auth) {
|
const saveRes = await HttpService.client.saveUserSettings({
|
||||||
let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
|
...i.state.saveUserSettingsForm,
|
||||||
WebSocketService.Instance.send(wsClient.saveUserSettings(form));
|
auth: myAuthRequired(),
|
||||||
|
});
|
||||||
|
if (saveRes.state === "success") {
|
||||||
|
UserService.Instance.login(saveRes.data);
|
||||||
|
location.reload();
|
||||||
|
toast(i18n.t("saved"));
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.setState({ saveRes });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangePasswordSubmit(i: Settings, event: any) {
|
async handleChangePasswordSubmit(i: Settings, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ changePasswordLoading: true });
|
const { new_password, new_password_verify, old_password } =
|
||||||
let auth = myAuth();
|
i.state.changePasswordForm;
|
||||||
let pForm = i.state.changePasswordForm;
|
|
||||||
let new_password = pForm.new_password;
|
if (new_password && old_password && new_password_verify) {
|
||||||
let new_password_verify = pForm.new_password_verify;
|
i.setState({ changePasswordRes: { state: "loading" } });
|
||||||
let old_password = pForm.old_password;
|
const changePasswordRes = await HttpService.client.changePassword({
|
||||||
if (auth && new_password && old_password && new_password_verify) {
|
|
||||||
let form: ChangePassword = {
|
|
||||||
new_password,
|
new_password,
|
||||||
new_password_verify,
|
new_password_verify,
|
||||||
old_password,
|
old_password,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
|
if (changePasswordRes.state === "success") {
|
||||||
|
UserService.Instance.login(changePasswordRes.data);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
toast(i18n.t("password_changed"));
|
||||||
|
}
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.changePassword(form));
|
i.setState({ changePasswordRes });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
|
handleDeleteAccountShowConfirmToggle(i: Settings) {
|
||||||
event.preventDefault();
|
|
||||||
i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
|
i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteAccountPasswordChange(i: Settings, event: any) {
|
handleDeleteAccountPasswordChange(i: Settings, event: any) {
|
||||||
i.state.deleteAccountForm.password = event.target.value;
|
i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteAccount(i: Settings, event: any) {
|
async handleDeleteAccount(i: Settings) {
|
||||||
event.preventDefault();
|
const password = i.state.deleteAccountForm.password;
|
||||||
i.setState({ deleteAccountLoading: true });
|
if (password) {
|
||||||
let auth = myAuth();
|
i.setState({ deleteAccountRes: { state: "loading" } });
|
||||||
let password = i.state.deleteAccountForm.password;
|
const deleteAccountRes = await HttpService.client.deleteAccount({
|
||||||
if (auth && password) {
|
|
||||||
let form: DeleteAccount = {
|
|
||||||
password,
|
password,
|
||||||
auth,
|
auth: myAuthRequired(),
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.deleteAccount(form));
|
if (deleteAccountRes.state === "success") {
|
||||||
|
UserService.Instance.logout();
|
||||||
|
this.context.router.history.replace("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ deleteAccountRes });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1204,45 +1208,20 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
i.ctx.setState({ currentTab: i.tab });
|
i.ctx.setState({ currentTab: i.tab });
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
personBlock(res: RequestState<BlockPersonResponse>) {
|
||||||
let op = wsUserOp(msg);
|
if (res.state === "success") {
|
||||||
console.log(msg);
|
updatePersonBlock(res.data);
|
||||||
if (msg.error) {
|
const mui = UserService.Instance.myUserInfo;
|
||||||
this.setState({
|
|
||||||
saveUserSettingsLoading: false,
|
|
||||||
changePasswordLoading: false,
|
|
||||||
deleteAccountLoading: false,
|
|
||||||
});
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.SaveUserSettings) {
|
|
||||||
this.setState({ saveUserSettingsLoading: false });
|
|
||||||
toast(i18n.t("saved"));
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
} else if (op == UserOperation.ChangePassword) {
|
|
||||||
let data = wsJsonToRes<LoginResponse>(msg);
|
|
||||||
UserService.Instance.login(data);
|
|
||||||
this.setState({ changePasswordLoading: false });
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
toast(i18n.t("password_changed"));
|
|
||||||
} else if (op == UserOperation.DeleteAccount) {
|
|
||||||
this.setState({
|
|
||||||
deleteAccountLoading: false,
|
|
||||||
deleteAccountShowConfirm: false,
|
|
||||||
});
|
|
||||||
UserService.Instance.logout();
|
|
||||||
window.location.href = "/";
|
|
||||||
} else if (op == UserOperation.BlockPerson) {
|
|
||||||
let data = wsJsonToRes<BlockPersonResponse>(msg);
|
|
||||||
updatePersonBlock(data);
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (mui) {
|
if (mui) {
|
||||||
this.setState({ personBlocks: mui.person_blocks });
|
this.setState({ personBlocks: mui.person_blocks });
|
||||||
}
|
}
|
||||||
} else if (op == UserOperation.BlockCommunity) {
|
}
|
||||||
let data = wsJsonToRes<BlockCommunityResponse>(msg);
|
}
|
||||||
updateCommunityBlock(data);
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
communityBlock(res: RequestState<BlockCommunityResponse>) {
|
||||||
|
if (res.state === "success") {
|
||||||
|
updateCommunityBlock(res.data);
|
||||||
|
const mui = UserService.Instance.myUserInfo;
|
||||||
if (mui) {
|
if (mui) {
|
||||||
this.setState({ communityBlocks: mui.community_blocks });
|
this.setState({ communityBlocks: mui.community_blocks });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,49 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import {
|
import { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client";
|
||||||
GetSiteResponse,
|
|
||||||
UserOperation,
|
|
||||||
VerifyEmail as VerifyEmailForm,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import { setIsoData, toast } from "../../utils";
|
||||||
isBrowser,
|
|
||||||
setIsoData,
|
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
|
import { Spinner } from "../common/icon";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
verifyEmailForm: VerifyEmailForm;
|
verifyRes: RequestState<VerifyEmailResponse>;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VerifyEmail extends Component<any, State> {
|
export class VerifyEmail extends Component<any, State> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
verifyEmailForm: {
|
verifyRes: { state: "empty" },
|
||||||
token: this.props.match.params.token,
|
|
||||||
},
|
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async verify() {
|
||||||
WebSocketService.Instance.send(
|
this.setState({
|
||||||
wsClient.verifyEmail(this.state.verifyEmailForm)
|
verifyRes: { state: "loading" },
|
||||||
);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
this.setState({
|
||||||
if (isBrowser()) {
|
verifyRes: await HttpService.client.verifyEmail({
|
||||||
this.subscription?.unsubscribe();
|
token: this.props.match.params.token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.verifyRes.state == "success") {
|
||||||
|
toast(i18n.t("email_verified"));
|
||||||
|
this.props.history.push("/login");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
await this.verify();
|
||||||
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return `${i18n.t("verify_email")} - ${
|
return `${i18n.t("verify_email")} - ${
|
||||||
this.state.siteRes.site_view.site.name
|
this.state.siteRes.site_view.site.name
|
||||||
|
@ -69,26 +60,14 @@ export class VerifyEmail extends Component<any, State> {
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t("verify_email")}</h5>
|
<h5>{i18n.t("verify_email")}</h5>
|
||||||
|
{this.state.verifyRes.state == "loading" && (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState(this.state);
|
|
||||||
this.props.history.push("/");
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.VerifyEmail) {
|
|
||||||
let data = wsJsonToRes(msg);
|
|
||||||
if (data) {
|
|
||||||
toast(i18n.t("email_verified"));
|
|
||||||
this.props.history.push("/login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import {
|
import {
|
||||||
|
CreatePost as CreatePostI,
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
GetCommunityResponse,
|
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
PostView,
|
ListCommunitiesResponse,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { InitialFetchRequest, PostFormParams } from "../../interfaces";
|
||||||
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import {
|
||||||
|
HttpService,
|
||||||
|
RequestState,
|
||||||
|
WrappedLemmyHttp,
|
||||||
|
} from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
Choice,
|
Choice,
|
||||||
QueryParams,
|
QueryParams,
|
||||||
|
@ -20,12 +21,8 @@ import {
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
getIdFromString,
|
getIdFromString,
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
isBrowser,
|
|
||||||
myAuth,
|
myAuth,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
@ -41,10 +38,16 @@ function getCreatePostQueryParams() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchCommunitiesForOptions(client: WrappedLemmyHttp) {
|
||||||
|
return client.listCommunities({ limit: 30, sort: "TopMonth", type_: "All" });
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePostState {
|
interface CreatePostState {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
selectedCommunityChoice?: Choice;
|
selectedCommunityChoice?: Choice;
|
||||||
|
initialCommunitiesRes: RequestState<ListCommunitiesResponse>;
|
||||||
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreatePost extends Component<
|
export class CreatePost extends Component<
|
||||||
|
@ -52,10 +55,11 @@ export class CreatePost extends Component<
|
||||||
CreatePostState
|
CreatePostState
|
||||||
> {
|
> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: CreatePostState = {
|
state: CreatePostState = {
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
initialCommunitiesRes: { state: "empty" },
|
||||||
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
|
constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
|
||||||
|
@ -65,19 +69,14 @@ export class CreatePost extends Component<
|
||||||
this.handleSelectedCommunityChange =
|
this.handleSelectedCommunityChange =
|
||||||
this.handleSelectedCommunityChange.bind(this);
|
this.handleSelectedCommunityChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path === this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
const communityRes = this.isoData.routeData[0] as
|
const [communityRes, listCommunitiesRes] = this.isoData.routeData;
|
||||||
| GetCommunityResponse
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (communityRes) {
|
if (communityRes?.state === "success") {
|
||||||
const communityChoice: Choice = {
|
const communityChoice: Choice = {
|
||||||
label: communityRes.community_view.community.title,
|
label: communityRes.data.community_view.community.title,
|
||||||
value: communityRes.community_view.community.id.toString(),
|
value: communityRes.data.community_view.community.id.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -89,42 +88,56 @@ export class CreatePost extends Component<
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
initialCommunitiesRes: listCommunitiesRes,
|
||||||
|
isIsomorphic: true,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.fetchCommunity();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCommunity() {
|
async fetchCommunity() {
|
||||||
const { communityId } = getCreatePostQueryParams();
|
const { communityId } = getCreatePostQueryParams();
|
||||||
const auth = myAuth(false);
|
const auth = myAuth();
|
||||||
|
|
||||||
if (communityId) {
|
if (communityId) {
|
||||||
const form: GetCommunity = {
|
const res = await HttpService.client.getCommunity({
|
||||||
id: communityId,
|
id: communityId,
|
||||||
auth,
|
auth,
|
||||||
};
|
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.getCommunity(form));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
const { communityId } = getCreatePostQueryParams();
|
|
||||||
|
|
||||||
if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
|
|
||||||
this.fetchCommunity();
|
|
||||||
} else if (!communityId) {
|
|
||||||
this.setState({
|
|
||||||
selectedCommunityChoice: undefined,
|
|
||||||
loading: false,
|
|
||||||
});
|
});
|
||||||
|
if (res.state === "success") {
|
||||||
|
this.setState({
|
||||||
|
selectedCommunityChoice: {
|
||||||
|
label: res.data.community_view.community.name,
|
||||||
|
value: res.data.community_view.community.id.toString(),
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentDidMount() {
|
||||||
if (isBrowser()) {
|
// TODO test this
|
||||||
this.subscription?.unsubscribe();
|
if (!this.state.isIsomorphic) {
|
||||||
|
const { communityId } = getCreatePostQueryParams();
|
||||||
|
|
||||||
|
const initialCommunitiesRes = await fetchCommunitiesForOptions(
|
||||||
|
HttpService.client
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
initialCommunitiesRes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
communityId?.toString() !== this.state.selectedCommunityChoice?.value
|
||||||
|
) {
|
||||||
|
await this.fetchCommunity();
|
||||||
|
} else if (!communityId) {
|
||||||
|
this.setState({
|
||||||
|
selectedCommunityChoice: undefined,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +177,11 @@ export class CreatePost extends Component<
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
selectedCommunityChoice={selectedCommunityChoice}
|
selectedCommunityChoice={selectedCommunityChoice}
|
||||||
onSelectCommunity={this.handleSelectedCommunityChange}
|
onSelectCommunity={this.handleSelectedCommunityChange}
|
||||||
|
initialCommunities={
|
||||||
|
this.state.initialCommunitiesRes.state === "success"
|
||||||
|
? this.state.initialCommunitiesRes.data.communities
|
||||||
|
: []
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,7 +190,7 @@ export class CreatePost extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl({ communityId }: Partial<CreatePostProps>) {
|
async updateUrl({ communityId }: Partial<CreatePostProps>) {
|
||||||
const { communityId: urlCommunityId } = getCreatePostQueryParams();
|
const { communityId: urlCommunityId } = getCreatePostQueryParams();
|
||||||
|
|
||||||
const locationState = this.props.history.location.state as
|
const locationState = this.props.history.location.state as
|
||||||
|
@ -191,7 +209,7 @@ export class CreatePost extends Component<
|
||||||
|
|
||||||
history.replaceState(locationState, "", url);
|
history.replaceState(locationState, "", url);
|
||||||
|
|
||||||
this.fetchCommunity();
|
await this.fetchCommunity();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectedCommunityChange(choice: Choice) {
|
handleSelectedCommunityChange(choice: Choice) {
|
||||||
|
@ -200,16 +218,23 @@ export class CreatePost extends Component<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostCreate(post_view: PostView) {
|
async handlePostCreate(form: CreatePostI) {
|
||||||
this.props.history.replace(`/post/${post_view.post.id}`);
|
const res = await HttpService.client.createPost(form);
|
||||||
|
|
||||||
|
if (res.state === "success") {
|
||||||
|
const postId = res.data.post_view.post.id;
|
||||||
|
this.props.history.replace(`/post/${postId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData({
|
static fetchInitialData({
|
||||||
client,
|
client,
|
||||||
query: { communityId },
|
query: { communityId },
|
||||||
auth,
|
auth,
|
||||||
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<any>[] {
|
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
|
||||||
const promises: Promise<any>[] = [];
|
RequestState<any>
|
||||||
|
>[] {
|
||||||
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
|
|
||||||
if (communityId) {
|
if (communityId) {
|
||||||
const form: GetCommunity = {
|
const form: GetCommunity = {
|
||||||
|
@ -219,31 +244,11 @@ export class CreatePost extends Component<
|
||||||
|
|
||||||
promises.push(client.getCommunity(form));
|
promises.push(client.getCommunity(form));
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promises.push(fetchCommunitiesForOptions(client));
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
const op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === UserOperation.GetCommunity) {
|
|
||||||
const {
|
|
||||||
community_view: {
|
|
||||||
community: { title, id },
|
|
||||||
},
|
|
||||||
} = wsJsonToRes<GetCommunityResponse>(msg);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selectedCommunityChoice: { label: title, value: id.toString() },
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class MetadataCard extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let post = this.props.post;
|
const post = this.props.post;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!this.state.expanded && post.embed_title && post.url && (
|
{!this.state.expanded && post.embed_title && post.url && (
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,26 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import { Link } from "inferno-router";
|
import { Link } from "inferno-router";
|
||||||
import { Language, PostView } from "lemmy-js-client";
|
import {
|
||||||
|
AddAdmin,
|
||||||
|
AddModToCommunity,
|
||||||
|
BanFromCommunity,
|
||||||
|
BanPerson,
|
||||||
|
BlockPerson,
|
||||||
|
CreatePostLike,
|
||||||
|
CreatePostReport,
|
||||||
|
DeletePost,
|
||||||
|
EditPost,
|
||||||
|
FeaturePost,
|
||||||
|
Language,
|
||||||
|
LockPost,
|
||||||
|
PostView,
|
||||||
|
PurgePerson,
|
||||||
|
PurgePost,
|
||||||
|
RemovePost,
|
||||||
|
SavePost,
|
||||||
|
TransferCommunity,
|
||||||
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { PostListing } from "./post-listing";
|
import { PostListing } from "./post-listing";
|
||||||
|
|
||||||
|
@ -13,6 +32,23 @@ interface PostListingsProps {
|
||||||
removeDuplicates?: boolean;
|
removeDuplicates?: boolean;
|
||||||
enableDownvotes?: boolean;
|
enableDownvotes?: boolean;
|
||||||
enableNsfw?: boolean;
|
enableNsfw?: boolean;
|
||||||
|
viewOnly?: boolean;
|
||||||
|
onPostEdit(form: EditPost): void;
|
||||||
|
onPostVote(form: CreatePostLike): void;
|
||||||
|
onPostReport(form: CreatePostReport): void;
|
||||||
|
onBlockPerson(form: BlockPerson): void;
|
||||||
|
onLockPost(form: LockPost): void;
|
||||||
|
onDeletePost(form: DeletePost): void;
|
||||||
|
onRemovePost(form: RemovePost): void;
|
||||||
|
onSavePost(form: SavePost): void;
|
||||||
|
onFeaturePost(form: FeaturePost): void;
|
||||||
|
onPurgePerson(form: PurgePerson): void;
|
||||||
|
onPurgePost(form: PurgePost): void;
|
||||||
|
onBanPersonFromCommunity(form: BanFromCommunity): void;
|
||||||
|
onBanPerson(form: BanPerson): void;
|
||||||
|
onAddModToCommunity(form: AddModToCommunity): void;
|
||||||
|
onAddAdmin(form: AddAdmin): void;
|
||||||
|
onTransferCommunity(form: TransferCommunity): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostListings extends Component<PostListingsProps, any> {
|
export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
@ -36,12 +72,29 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
<>
|
<>
|
||||||
<PostListing
|
<PostListing
|
||||||
post_view={post_view}
|
post_view={post_view}
|
||||||
duplicates={this.duplicatesMap.get(post_view.post.id)}
|
crossPosts={this.duplicatesMap.get(post_view.post.id)}
|
||||||
showCommunity={this.props.showCommunity}
|
showCommunity={this.props.showCommunity}
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableNsfw={this.props.enableNsfw}
|
||||||
|
viewOnly={this.props.viewOnly}
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
onPostEdit={this.props.onPostEdit}
|
||||||
|
onPostVote={this.props.onPostVote}
|
||||||
|
onPostReport={this.props.onPostReport}
|
||||||
|
onBlockPerson={this.props.onBlockPerson}
|
||||||
|
onLockPost={this.props.onLockPost}
|
||||||
|
onDeletePost={this.props.onDeletePost}
|
||||||
|
onRemovePost={this.props.onRemovePost}
|
||||||
|
onSavePost={this.props.onSavePost}
|
||||||
|
onFeaturePost={this.props.onFeaturePost}
|
||||||
|
onPurgePerson={this.props.onPurgePerson}
|
||||||
|
onPurgePost={this.props.onPurgePost}
|
||||||
|
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||||
|
onBanPerson={this.props.onBanPerson}
|
||||||
|
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||||
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
/>
|
/>
|
||||||
<hr className="my-3" />
|
<hr className="my-3" />
|
||||||
</>
|
</>
|
||||||
|
@ -62,14 +115,14 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
|
||||||
removeDuplicates(): PostView[] {
|
removeDuplicates(): PostView[] {
|
||||||
// Must use a spread to clone the props, because splice will fail below otherwise.
|
// Must use a spread to clone the props, because splice will fail below otherwise.
|
||||||
let posts = [...this.props.posts];
|
const posts = [...this.props.posts].filter(empty => empty);
|
||||||
|
|
||||||
// A map from post url to list of posts (dupes)
|
// A map from post url to list of posts (dupes)
|
||||||
let urlMap = new Map<string, PostView[]>();
|
const urlMap = new Map<string, PostView[]>();
|
||||||
|
|
||||||
// Loop over the posts, find ones with same urls
|
// Loop over the posts, find ones with same urls
|
||||||
for (let pv of posts) {
|
for (const pv of posts) {
|
||||||
let url = pv.post.url;
|
const url = pv.post.url;
|
||||||
if (
|
if (
|
||||||
!pv.post.deleted &&
|
!pv.post.deleted &&
|
||||||
!pv.post.removed &&
|
!pv.post.removed &&
|
||||||
|
@ -87,7 +140,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
|
||||||
// Sort by oldest
|
// Sort by oldest
|
||||||
// Remove the ones that have no length
|
// Remove the ones that have no length
|
||||||
for (let e of urlMap.entries()) {
|
for (const e of urlMap.entries()) {
|
||||||
if (e[1].length == 1) {
|
if (e[1].length == 1) {
|
||||||
urlMap.delete(e[0]);
|
urlMap.delete(e[0]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,10 +149,10 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < posts.length; i++) {
|
for (let i = 0; i < posts.length; i++) {
|
||||||
let pv = posts[i];
|
const pv = posts[i];
|
||||||
let url = pv.post.url;
|
const url = pv.post.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
let found = urlMap.get(url);
|
const found = urlMap.get(url);
|
||||||
if (found) {
|
if (found) {
|
||||||
// If its the oldest, add
|
// If its the oldest, add
|
||||||
if (pv.post.id == found[0].post.id) {
|
if (pv.post.id == found[0].post.id) {
|
||||||
|
|
|
@ -1,27 +1,43 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client";
|
import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { myAuthRequired } from "../../utils";
|
||||||
import { myAuth, wsClient } from "../../utils";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { Icon } from "../common/icon";
|
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { PostListing } from "./post-listing";
|
import { PostListing } from "./post-listing";
|
||||||
|
|
||||||
interface PostReportProps {
|
interface PostReportProps {
|
||||||
report: PostReportView;
|
report: PostReportView;
|
||||||
|
onResolveReport(form: ResolvePostReport): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostReport extends Component<PostReportProps, any> {
|
interface PostReportState {
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostReport extends Component<PostReportProps, PostReportState> {
|
||||||
|
state: PostReportState = {
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & PostReportProps>
|
||||||
|
): void {
|
||||||
|
if (this.props != nextProps) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let r = this.props.report;
|
const r = this.props.report;
|
||||||
let resolver = r.resolver;
|
const resolver = r.resolver;
|
||||||
let post = r.post;
|
const post = r.post;
|
||||||
let tippyContent = i18n.t(
|
const tippyContent = i18n.t(
|
||||||
r.post_report.resolved ? "unresolve_report" : "resolve_report"
|
r.post_report.resolved ? "unresolve_report" : "resolve_report"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -29,7 +45,7 @@ export class PostReport extends Component<PostReportProps, any> {
|
||||||
post.name = r.post_report.original_post_name;
|
post.name = r.post_report.original_post_name;
|
||||||
post.url = r.post_report.original_post_url;
|
post.url = r.post_report.original_post_url;
|
||||||
post.body = r.post_report.original_post_body;
|
post.body = r.post_report.original_post_body;
|
||||||
let pv: PostView = {
|
const pv: PostView = {
|
||||||
post,
|
post,
|
||||||
creator: r.post_creator,
|
creator: r.post_creator,
|
||||||
community: r.community,
|
community: r.community,
|
||||||
|
@ -54,6 +70,23 @@ export class PostReport extends Component<PostReportProps, any> {
|
||||||
allLanguages={[]}
|
allLanguages={[]}
|
||||||
siteLanguages={[]}
|
siteLanguages={[]}
|
||||||
hideImage
|
hideImage
|
||||||
|
// All of these are unused, since its view only
|
||||||
|
onPostEdit={() => {}}
|
||||||
|
onPostVote={() => {}}
|
||||||
|
onPostReport={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onLockPost={() => {}}
|
||||||
|
onDeletePost={() => {}}
|
||||||
|
onRemovePost={() => {}}
|
||||||
|
onSavePost={() => {}}
|
||||||
|
onFeaturePost={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onPurgePost={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{i18n.t("reporter")}: <PersonListing person={r.creator} />
|
{i18n.t("reporter")}: <PersonListing person={r.creator} />
|
||||||
|
@ -82,26 +115,27 @@ export class PostReport extends Component<PostReportProps, any> {
|
||||||
data-tippy-content={tippyContent}
|
data-tippy-content={tippyContent}
|
||||||
aria-label={tippyContent}
|
aria-label={tippyContent}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.loading ? (
|
||||||
icon="check"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
r.post_report.resolved ? "text-success" : "text-danger"
|
<Icon
|
||||||
}`}
|
icon="check"
|
||||||
/>
|
classes={`icon-inline ${
|
||||||
|
r.post_report.resolved ? "text-success" : "text-danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResolveReport(i: PostReport) {
|
handleResolveReport(i: PostReport) {
|
||||||
let auth = myAuth();
|
i.setState({ loading: true });
|
||||||
if (auth) {
|
i.props.onResolveReport({
|
||||||
let form: ResolvePostReport = {
|
report_id: i.props.report.post_report.id,
|
||||||
report_id: i.props.report.post_report.id,
|
resolved: !i.props.report.post_report.resolved,
|
||||||
resolved: !i.props.report.post_report.resolved,
|
auth: myAuthRequired(),
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.resolvePostReport(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,24 +1,19 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
CreatePrivateMessage as CreatePrivateMessageI,
|
||||||
GetPersonDetails,
|
GetPersonDetails,
|
||||||
GetPersonDetailsResponse,
|
GetPersonDetailsResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { WebSocketService } from "../../services";
|
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import {
|
import {
|
||||||
getRecipientIdFromProps,
|
getRecipientIdFromProps,
|
||||||
isBrowser,
|
|
||||||
myAuth,
|
myAuth,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
toast,
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
@ -26,9 +21,9 @@ import { PrivateMessageForm } from "./private-message-form";
|
||||||
|
|
||||||
interface CreatePrivateMessageState {
|
interface CreatePrivateMessageState {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
recipientDetailsRes?: GetPersonDetailsResponse;
|
recipientRes: RequestState<GetPersonDetailsResponse>;
|
||||||
recipient_id: number;
|
recipientId: number;
|
||||||
loading: boolean;
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreatePrivateMessage extends Component<
|
export class CreatePrivateMessage extends Component<
|
||||||
|
@ -36,11 +31,11 @@ export class CreatePrivateMessage extends Component<
|
||||||
CreatePrivateMessageState
|
CreatePrivateMessageState
|
||||||
> {
|
> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: CreatePrivateMessageState = {
|
state: CreatePrivateMessageState = {
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
recipient_id: getRecipientIdFromProps(this.props),
|
recipientRes: { state: "empty" },
|
||||||
loading: true,
|
recipientId: getRecipientIdFromProps(this.props),
|
||||||
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -48,35 +43,42 @@ export class CreatePrivateMessage extends Component<
|
||||||
this.handlePrivateMessageCreate =
|
this.handlePrivateMessageCreate =
|
||||||
this.handlePrivateMessageCreate.bind(this);
|
this.handlePrivateMessageCreate.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path == this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
recipientDetailsRes: this.isoData
|
recipientRes: this.isoData.routeData[0],
|
||||||
.routeData[0] as GetPersonDetailsResponse,
|
isIsomorphic: true,
|
||||||
loading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.fetchPersonDetails();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPersonDetails() {
|
async componentDidMount() {
|
||||||
let form: GetPersonDetails = {
|
if (!this.state.isIsomorphic) {
|
||||||
person_id: this.state.recipient_id,
|
await this.fetchPersonDetails();
|
||||||
sort: "New",
|
}
|
||||||
saved_only: false,
|
|
||||||
auth: myAuth(false),
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
async fetchPersonDetails() {
|
||||||
let person_id = Number(req.path.split("/").pop());
|
this.setState({
|
||||||
let form: GetPersonDetails = {
|
recipientRes: { state: "loading" },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
recipientRes: await HttpService.client.getPersonDetails({
|
||||||
|
person_id: this.state.recipientId,
|
||||||
|
sort: "New",
|
||||||
|
saved_only: false,
|
||||||
|
auth: myAuth(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static fetchInitialData(
|
||||||
|
req: InitialFetchRequest
|
||||||
|
): Promise<RequestState<any>>[] {
|
||||||
|
const person_id = Number(req.path.split("/").pop());
|
||||||
|
const form: GetPersonDetails = {
|
||||||
person_id,
|
person_id,
|
||||||
sort: "New",
|
sort: "New",
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
|
@ -86,62 +88,59 @@ export class CreatePrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
let name_ = this.state.recipientDetailsRes?.person_view.person.name;
|
if (this.state.recipientRes.state == "success") {
|
||||||
return name_ ? `${i18n.t("create_private_message")} - ${name_}` : "";
|
const name_ = this.state.recipientRes.data.person_view.person.name;
|
||||||
|
return `${i18n.t("create_private_message")} - ${name_}`;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
renderRecipientRes() {
|
||||||
if (isBrowser()) {
|
switch (this.state.recipientRes.state) {
|
||||||
this.subscription?.unsubscribe();
|
case "loading":
|
||||||
|
return (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
case "success": {
|
||||||
|
const res = this.state.recipientRes.data;
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
|
<h5>{i18n.t("create_private_message")}</h5>
|
||||||
|
<PrivateMessageForm
|
||||||
|
onCreate={this.handlePrivateMessageCreate}
|
||||||
|
recipient={res.person_view.person}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let res = this.state.recipientDetailsRes;
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
/>
|
/>
|
||||||
{this.state.loading ? (
|
{this.renderRecipientRes()}
|
||||||
<h5>
|
|
||||||
<Spinner large />
|
|
||||||
</h5>
|
|
||||||
) : (
|
|
||||||
res && (
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
|
||||||
<h5>{i18n.t("create_private_message")}</h5>
|
|
||||||
<PrivateMessageForm
|
|
||||||
onCreate={this.handlePrivateMessageCreate}
|
|
||||||
recipient={res.person_view.person}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrivateMessageCreate() {
|
async handlePrivateMessageCreate(form: CreatePrivateMessageI) {
|
||||||
toast(i18n.t("message_sent"));
|
const res = await HttpService.client.createPrivateMessage(form);
|
||||||
|
|
||||||
// Navigate to the front
|
if (res.state == "success") {
|
||||||
this.context.router.history.push("/");
|
toast(i18n.t("message_sent"));
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
// Navigate to the front
|
||||||
let op = wsUserOp(msg);
|
this.context.router.history.push("/");
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ loading: false });
|
|
||||||
return;
|
|
||||||
} else if (op == UserOperation.GetPersonDetails) {
|
|
||||||
let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
|
|
||||||
this.setState({ recipientDetailsRes: data, loading: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,29 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import { Prompt } from "inferno-router";
|
|
||||||
import {
|
import {
|
||||||
CreatePrivateMessage,
|
CreatePrivateMessage,
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
Person,
|
Person,
|
||||||
PrivateMessageResponse,
|
|
||||||
PrivateMessageView,
|
PrivateMessageView,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
isBrowser,
|
myAuthRequired,
|
||||||
myAuth,
|
|
||||||
relTags,
|
relTags,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import NavigationPrompt from "../common/navigation-prompt";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
|
||||||
interface PrivateMessageFormProps {
|
interface PrivateMessageFormProps {
|
||||||
recipient: Person;
|
recipient: Person;
|
||||||
privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit
|
privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit
|
||||||
onCancel?(): any;
|
onCancel?(): any;
|
||||||
onCreate?(message: PrivateMessageView): any;
|
onCreate?(form: CreatePrivateMessage): void;
|
||||||
onEdit?(message: PrivateMessageView): any;
|
onEdit?(form: EditPrivateMessage): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PrivateMessageFormState {
|
interface PrivateMessageFormState {
|
||||||
|
@ -41,168 +31,157 @@ interface PrivateMessageFormState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
previewMode: boolean;
|
previewMode: boolean;
|
||||||
showDisclaimer: boolean;
|
showDisclaimer: boolean;
|
||||||
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrivateMessageForm extends Component<
|
export class PrivateMessageForm extends Component<
|
||||||
PrivateMessageFormProps,
|
PrivateMessageFormProps,
|
||||||
PrivateMessageFormState
|
PrivateMessageFormState
|
||||||
> {
|
> {
|
||||||
private subscription?: Subscription;
|
|
||||||
state: PrivateMessageFormState = {
|
state: PrivateMessageFormState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
previewMode: false,
|
previewMode: false,
|
||||||
showDisclaimer: false,
|
showDisclaimer: false,
|
||||||
|
content: this.props.privateMessageView
|
||||||
|
? this.props.privateMessageView.private_message.content
|
||||||
|
: undefined,
|
||||||
|
submitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleContentChange = this.handleContentChange.bind(this);
|
this.handleContentChange = this.handleContentChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
// Its an edit
|
|
||||||
if (this.props.privateMessageView) {
|
|
||||||
this.state.content =
|
|
||||||
this.props.privateMessageView.private_message.content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setupTippy();
|
setupTippy();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentWillReceiveProps(
|
||||||
if (!this.state.loading && this.state.content) {
|
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageFormProps>
|
||||||
window.onbeforeunload = () => true;
|
): void {
|
||||||
} else {
|
if (this.props != nextProps) {
|
||||||
window.onbeforeunload = null;
|
this.setState({ loading: false, content: undefined, previewMode: false });
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO
|
||||||
|
// <Prompt
|
||||||
|
// when={!this.state.loading && this.state.content}
|
||||||
|
// message={i18n.t("block_leaving")}
|
||||||
|
// />
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
|
||||||
<Prompt
|
<NavigationPrompt
|
||||||
when={!this.state.loading && this.state.content}
|
when={
|
||||||
message={i18n.t("block_leaving")}
|
!this.state.loading && !!this.state.content && !this.state.submitted
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
|
{!this.props.privateMessageView && (
|
||||||
{!this.props.privateMessageView && (
|
|
||||||
<div className="form-group row">
|
|
||||||
<label className="col-sm-2 col-form-label">
|
|
||||||
{capitalizeFirstLetter(i18n.t("to"))}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="col-sm-10 form-control-plaintext">
|
|
||||||
<PersonListing person={this.props.recipient} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<label className="col-sm-2 col-form-label">
|
<label className="col-sm-2 col-form-label">
|
||||||
{i18n.t("message")}
|
{capitalizeFirstLetter(i18n.t("to"))}
|
||||||
<button
|
|
||||||
className="btn btn-link text-warning d-inline-block"
|
|
||||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
|
||||||
data-tippy-content={i18n.t("private_message_disclaimer")}
|
|
||||||
aria-label={i18n.t("private_message_disclaimer")}
|
|
||||||
>
|
|
||||||
<Icon icon="alert-triangle" classes="icon-inline" />
|
|
||||||
</button>
|
|
||||||
</label>
|
</label>
|
||||||
<div className="col-sm-10">
|
|
||||||
<MarkdownTextArea
|
<div className="col-sm-10 form-control-plaintext">
|
||||||
initialContent={this.state.content}
|
<PersonListing person={this.props.recipient} />
|
||||||
onContentChange={this.handleContentChange}
|
|
||||||
allLanguages={[]}
|
|
||||||
siteLanguages={[]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="form-group row">
|
||||||
|
<label className="col-sm-2 col-form-label">
|
||||||
|
{i18n.t("message")}
|
||||||
|
<button
|
||||||
|
className="btn btn-link text-warning d-inline-block"
|
||||||
|
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||||
|
data-tippy-content={i18n.t("private_message_disclaimer")}
|
||||||
|
aria-label={i18n.t("private_message_disclaimer")}
|
||||||
|
>
|
||||||
|
<Icon icon="alert-triangle" classes="icon-inline" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-10">
|
||||||
|
<MarkdownTextArea
|
||||||
|
initialContent={this.state.content}
|
||||||
|
onContentChange={this.handleContentChange}
|
||||||
|
allLanguages={[]}
|
||||||
|
siteLanguages={[]}
|
||||||
|
hideNavigationWarnings
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{this.state.showDisclaimer && (
|
{this.state.showDisclaimer && (
|
||||||
<div className="form-group row">
|
|
||||||
<div className="offset-sm-2 col-sm-10">
|
|
||||||
<div className="alert alert-danger" role="alert">
|
|
||||||
<T i18nKey="private_message_disclaimer">
|
|
||||||
#
|
|
||||||
<a
|
|
||||||
className="alert-link"
|
|
||||||
rel={relTags}
|
|
||||||
href="https://element.io/get-started"
|
|
||||||
>
|
|
||||||
#
|
|
||||||
</a>
|
|
||||||
</T>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<div className="offset-sm-2 col-sm-10">
|
<div className="offset-sm-2 col-sm-10">
|
||||||
<button
|
<div className="alert alert-danger" role="alert">
|
||||||
type="submit"
|
<T i18nKey="private_message_disclaimer">
|
||||||
className="btn btn-secondary mr-2"
|
#
|
||||||
disabled={this.state.loading}
|
<a
|
||||||
>
|
className="alert-link"
|
||||||
{this.state.loading ? (
|
rel={relTags}
|
||||||
<Spinner />
|
href="https://element.io/get-started"
|
||||||
) : this.props.privateMessageView ? (
|
>
|
||||||
capitalizeFirstLetter(i18n.t("save"))
|
#
|
||||||
) : (
|
</a>
|
||||||
capitalizeFirstLetter(i18n.t("send_message"))
|
</T>
|
||||||
)}
|
</div>
|
||||||
</button>
|
|
||||||
{this.props.privateMessageView && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={linkEvent(this, this.handleCancel)}
|
|
||||||
>
|
|
||||||
{i18n.t("cancel")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<ul className="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
|
|
||||||
<li className="list-inline-item"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
)}
|
||||||
</div>
|
<div className="form-group row">
|
||||||
|
<div className="offset-sm-2 col-sm-10">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-secondary mr-2"
|
||||||
|
disabled={this.state.loading}
|
||||||
|
>
|
||||||
|
{this.state.loading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : this.props.privateMessageView ? (
|
||||||
|
capitalizeFirstLetter(i18n.t("save"))
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t("send_message"))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{this.props.privateMessageView && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleCancel)}
|
||||||
|
>
|
||||||
|
{i18n.t("cancel")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<ul className="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
|
||||||
|
<li className="list-inline-item"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
|
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let pm = i.props.privateMessageView;
|
i.setState({ loading: true, submitted: true });
|
||||||
let auth = myAuth();
|
const pm = i.props.privateMessageView;
|
||||||
let content = i.state.content;
|
const auth = myAuthRequired();
|
||||||
if (auth && content) {
|
const content = i.state.content ?? "";
|
||||||
if (pm) {
|
if (pm) {
|
||||||
let form: EditPrivateMessage = {
|
i.props.onEdit?.({
|
||||||
private_message_id: pm.private_message.id,
|
private_message_id: pm.private_message.id,
|
||||||
content,
|
content,
|
||||||
auth,
|
auth,
|
||||||
};
|
});
|
||||||
WebSocketService.Instance.send(wsClient.editPrivateMessage(form));
|
} else {
|
||||||
} else {
|
i.props.onCreate?.({
|
||||||
let form: CreatePrivateMessage = {
|
content,
|
||||||
content,
|
recipient_id: i.props.recipient.id,
|
||||||
recipient_id: i.props.recipient.id,
|
auth,
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.createPrivateMessage(form));
|
|
||||||
}
|
|
||||||
i.setState({ loading: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,25 +201,4 @@ export class PrivateMessageForm extends Component<
|
||||||
handleShowDisclaimer(i: PrivateMessageForm) {
|
handleShowDisclaimer(i: PrivateMessageForm) {
|
||||||
i.setState({ showDisclaimer: !i.state.showDisclaimer });
|
i.setState({ showDisclaimer: !i.state.showDisclaimer });
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
let op = wsUserOp(msg);
|
|
||||||
console.log(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
this.setState({ loading: false });
|
|
||||||
return;
|
|
||||||
} else if (
|
|
||||||
op == UserOperation.EditPrivateMessage ||
|
|
||||||
op == UserOperation.DeletePrivateMessage ||
|
|
||||||
op == UserOperation.MarkPrivateMessageAsRead
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<PrivateMessageResponse>(msg);
|
|
||||||
this.setState({ loading: false });
|
|
||||||
this.props.onEdit?.(data.private_message_view);
|
|
||||||
} else if (op == UserOperation.CreatePrivateMessage) {
|
|
||||||
let data = wsJsonToRes<PrivateMessageResponse>(msg);
|
|
||||||
this.props.onCreate?.(data.private_message_view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,44 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import {
|
import {
|
||||||
PrivateMessageReportView,
|
PrivateMessageReportView,
|
||||||
ResolvePrivateMessageReport,
|
ResolvePrivateMessageReport,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { mdToHtml, myAuthRequired } from "../../utils";
|
||||||
import { mdToHtml, myAuth, wsClient } from "../../utils";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { Icon } from "../common/icon";
|
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
report: PrivateMessageReportView;
|
report: PrivateMessageReportView;
|
||||||
|
onResolveReport(form: ResolvePrivateMessageReport): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrivateMessageReport extends Component<Props, any> {
|
interface State {
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivateMessageReport extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & Props>
|
||||||
|
): void {
|
||||||
|
if (this.props != nextProps) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let r = this.props.report;
|
const r = this.props.report;
|
||||||
let pmr = r.private_message_report;
|
const pmr = r.private_message_report;
|
||||||
let tippyContent = i18n.t(
|
const tippyContent = i18n.t(
|
||||||
r.private_message_report.resolved ? "unresolve_report" : "resolve_report"
|
r.private_message_report.resolved ? "unresolve_report" : "resolve_report"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -66,29 +82,28 @@ export class PrivateMessageReport extends Component<Props, any> {
|
||||||
data-tippy-content={tippyContent}
|
data-tippy-content={tippyContent}
|
||||||
aria-label={tippyContent}
|
aria-label={tippyContent}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.loading ? (
|
||||||
icon="check"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
pmr.resolved ? "text-success" : "text-danger"
|
<Icon
|
||||||
}`}
|
icon="check"
|
||||||
/>
|
classes={`icon-inline ${
|
||||||
|
pmr.resolved ? "text-success" : "text-danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResolveReport(i: PrivateMessageReport) {
|
handleResolveReport(i: PrivateMessageReport) {
|
||||||
let pmr = i.props.report.private_message_report;
|
i.setState({ loading: true });
|
||||||
let auth = myAuth();
|
const pmr = i.props.report.private_message_report;
|
||||||
if (auth) {
|
i.props.onResolveReport({
|
||||||
let form: ResolvePrivateMessageReport = {
|
report_id: pmr.id,
|
||||||
report_id: pmr.id,
|
resolved: !pmr.resolved,
|
||||||
resolved: !pmr.resolved,
|
auth: myAuthRequired(),
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.resolvePrivateMessageReport(form)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
CreatePrivateMessage,
|
||||||
CreatePrivateMessageReport,
|
CreatePrivateMessageReport,
|
||||||
DeletePrivateMessage,
|
DeletePrivateMessage,
|
||||||
|
EditPrivateMessage,
|
||||||
MarkPrivateMessageAsRead,
|
MarkPrivateMessageAsRead,
|
||||||
Person,
|
Person,
|
||||||
PrivateMessageView,
|
PrivateMessageView,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
import { mdToHtml, myAuth, toast, wsClient } from "../../utils";
|
import { mdToHtml, myAuthRequired } from "../../utils";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MomentTime } from "../common/moment-time";
|
import { MomentTime } from "../common/moment-time";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { PrivateMessageForm } from "./private-message-form";
|
import { PrivateMessageForm } from "./private-message-form";
|
||||||
|
@ -21,10 +23,18 @@ interface PrivateMessageState {
|
||||||
viewSource: boolean;
|
viewSource: boolean;
|
||||||
showReportDialog: boolean;
|
showReportDialog: boolean;
|
||||||
reportReason?: string;
|
reportReason?: string;
|
||||||
|
deleteLoading: boolean;
|
||||||
|
readLoading: boolean;
|
||||||
|
reportLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PrivateMessageProps {
|
interface PrivateMessageProps {
|
||||||
private_message_view: PrivateMessageView;
|
private_message_view: PrivateMessageView;
|
||||||
|
onDelete(form: DeletePrivateMessage): void;
|
||||||
|
onMarkRead(form: MarkPrivateMessageAsRead): void;
|
||||||
|
onReport(form: CreatePrivateMessageReport): void;
|
||||||
|
onCreate(form: CreatePrivateMessage): void;
|
||||||
|
onEdit(form: EditPrivateMessage): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrivateMessage extends Component<
|
export class PrivateMessage extends Component<
|
||||||
|
@ -37,15 +47,14 @@ export class PrivateMessage extends Component<
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
showReportDialog: false,
|
showReportDialog: false,
|
||||||
|
deleteLoading: false,
|
||||||
|
readLoading: false,
|
||||||
|
reportLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||||
this.handlePrivateMessageCreate =
|
|
||||||
this.handlePrivateMessageCreate.bind(this);
|
|
||||||
this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get mine(): boolean {
|
get mine(): boolean {
|
||||||
|
@ -55,9 +64,26 @@ export class PrivateMessage extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageProps>
|
||||||
|
): void {
|
||||||
|
if (this.props != nextProps) {
|
||||||
|
this.setState({
|
||||||
|
showReply: false,
|
||||||
|
showEdit: false,
|
||||||
|
collapsed: false,
|
||||||
|
viewSource: false,
|
||||||
|
showReportDialog: false,
|
||||||
|
deleteLoading: false,
|
||||||
|
readLoading: false,
|
||||||
|
reportLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let message_view = this.props.private_message_view;
|
const message_view = this.props.private_message_view;
|
||||||
let otherPerson: Person = this.mine
|
const otherPerson: Person = this.mine
|
||||||
? message_view.recipient
|
? message_view.recipient
|
||||||
: message_view.creator;
|
: message_view.creator;
|
||||||
|
|
||||||
|
@ -98,8 +124,7 @@ export class PrivateMessage extends Component<
|
||||||
<PrivateMessageForm
|
<PrivateMessageForm
|
||||||
recipient={otherPerson}
|
recipient={otherPerson}
|
||||||
privateMessageView={message_view}
|
privateMessageView={message_view}
|
||||||
onEdit={this.handlePrivateMessageEdit}
|
onEdit={this.props.onEdit}
|
||||||
onCreate={this.handlePrivateMessageCreate}
|
|
||||||
onCancel={this.handleReplyCancel}
|
onCancel={this.handleReplyCancel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -131,12 +156,17 @@ export class PrivateMessage extends Component<
|
||||||
: i18n.t("mark_as_read")
|
: i18n.t("mark_as_read")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.readLoading ? (
|
||||||
icon="check"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
message_view.private_message.read && "text-success"
|
<Icon
|
||||||
}`}
|
icon="check"
|
||||||
/>
|
classes={`icon-inline ${
|
||||||
|
message_view.private_message.read &&
|
||||||
|
"text-success"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">{this.reportButton}</li>
|
<li className="list-inline-item">{this.reportButton}</li>
|
||||||
|
@ -179,13 +209,17 @@ export class PrivateMessage extends Component<
|
||||||
: i18n.t("restore")
|
: i18n.t("restore")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
{this.state.deleteLoading ? (
|
||||||
icon="trash"
|
<Spinner />
|
||||||
classes={`icon-inline ${
|
) : (
|
||||||
message_view.private_message.deleted &&
|
<Icon
|
||||||
"text-danger"
|
icon="trash"
|
||||||
}`}
|
classes={`icon-inline ${
|
||||||
/>
|
message_view.private_message.deleted &&
|
||||||
|
"text-danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
|
@ -231,14 +265,14 @@ export class PrivateMessage extends Component<
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
aria-label={i18n.t("create_report")}
|
aria-label={i18n.t("create_report")}
|
||||||
>
|
>
|
||||||
{i18n.t("create_report")}
|
{this.state.reportLoading ? <Spinner /> : i18n.t("create_report")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
{this.state.showReply && (
|
{this.state.showReply && (
|
||||||
<PrivateMessageForm
|
<PrivateMessageForm
|
||||||
recipient={otherPerson}
|
recipient={otherPerson}
|
||||||
onCreate={this.handlePrivateMessageCreate}
|
onCreate={this.props.onCreate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* A collapsed clearfix */}
|
{/* A collapsed clearfix */}
|
||||||
|
@ -261,7 +295,7 @@ export class PrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
get messageUnlessRemoved(): string {
|
get messageUnlessRemoved(): string {
|
||||||
let message = this.props.private_message_view.private_message;
|
const message = this.props.private_message_view.private_message;
|
||||||
return message.deleted ? `*${i18n.t("deleted")}*` : message.content;
|
return message.deleted ? `*${i18n.t("deleted")}*` : message.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,15 +309,12 @@ export class PrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick(i: PrivateMessage) {
|
handleDeleteClick(i: PrivateMessage) {
|
||||||
let auth = myAuth();
|
i.setState({ deleteLoading: true });
|
||||||
if (auth) {
|
i.props.onDelete({
|
||||||
let form: DeletePrivateMessage = {
|
private_message_id: i.props.private_message_view.private_message.id,
|
||||||
private_message_id: i.props.private_message_view.private_message.id,
|
deleted: !i.props.private_message_view.private_message.deleted,
|
||||||
deleted: !i.props.private_message_view.private_message.deleted,
|
auth: myAuthRequired(),
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.deletePrivateMessage(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyCancel() {
|
handleReplyCancel() {
|
||||||
|
@ -291,15 +322,12 @@ export class PrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkRead(i: PrivateMessage) {
|
handleMarkRead(i: PrivateMessage) {
|
||||||
let auth = myAuth();
|
i.setState({ readLoading: true });
|
||||||
if (auth) {
|
i.props.onMarkRead({
|
||||||
let form: MarkPrivateMessageAsRead = {
|
private_message_id: i.props.private_message_view.private_message.id,
|
||||||
private_message_id: i.props.private_message_view.private_message.id,
|
read: !i.props.private_message_view.private_message.read,
|
||||||
read: !i.props.private_message_view.private_message.read,
|
auth: myAuthRequired(),
|
||||||
auth,
|
});
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessageCollapse(i: PrivateMessage) {
|
handleMessageCollapse(i: PrivateMessage) {
|
||||||
|
@ -320,31 +348,11 @@ export class PrivateMessage extends Component<
|
||||||
|
|
||||||
handleReportSubmit(i: PrivateMessage, event: any) {
|
handleReportSubmit(i: PrivateMessage, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let auth = myAuth();
|
i.setState({ reportLoading: true });
|
||||||
let reason = i.state.reportReason;
|
i.props.onReport({
|
||||||
if (auth && reason) {
|
private_message_id: i.props.private_message_view.private_message.id,
|
||||||
let form: CreatePrivateMessageReport = {
|
reason: i.state.reportReason ?? "",
|
||||||
private_message_id: i.props.private_message_view.private_message.id,
|
auth: myAuthRequired(),
|
||||||
reason,
|
});
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.createPrivateMessageReport(form));
|
|
||||||
|
|
||||||
i.setState({ showReportDialog: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePrivateMessageEdit() {
|
|
||||||
this.setState({ showEdit: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePrivateMessageCreate(message: PrivateMessageView) {
|
|
||||||
if (
|
|
||||||
message.creator.id ==
|
|
||||||
UserService.Instance.myUserInfo?.local_user_view.person.id
|
|
||||||
) {
|
|
||||||
this.setState({ showReply: false });
|
|
||||||
toast(i18n.t("message_sent"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { NoOptionI18nKeys } from "i18next";
|
import type { NoOptionI18nKeys } from "i18next";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
CommentResponse,
|
|
||||||
CommentView,
|
CommentView,
|
||||||
CommunityView,
|
CommunityView,
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
|
@ -13,7 +12,6 @@ import {
|
||||||
ListCommunitiesResponse,
|
ListCommunitiesResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
PersonView,
|
PersonView,
|
||||||
PostResponse,
|
|
||||||
PostView,
|
PostView,
|
||||||
ResolveObject,
|
ResolveObject,
|
||||||
ResolveObjectResponse,
|
ResolveObjectResponse,
|
||||||
|
@ -21,22 +19,17 @@ import {
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
SearchType,
|
SearchType,
|
||||||
SortType,
|
SortType,
|
||||||
UserOperation,
|
|
||||||
wsJsonToRes,
|
|
||||||
wsUserOp,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { i18n } from "../i18next";
|
import { i18n } from "../i18next";
|
||||||
import { CommentViewType, InitialFetchRequest } from "../interfaces";
|
import { CommentViewType, InitialFetchRequest } from "../interfaces";
|
||||||
import { WebSocketService } from "../services";
|
import { FirstLoadService } from "../services/FirstLoadService";
|
||||||
|
import { HttpService, RequestState } from "../services/HttpService";
|
||||||
import {
|
import {
|
||||||
Choice,
|
Choice,
|
||||||
QueryParams,
|
QueryParams,
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
commentsToFlatNodes,
|
commentsToFlatNodes,
|
||||||
communityToChoice,
|
communityToChoice,
|
||||||
createCommentLikeRes,
|
|
||||||
createPostLikeFindRes,
|
|
||||||
debounce,
|
debounce,
|
||||||
enableDownvotes,
|
enableDownvotes,
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
|
@ -55,9 +48,6 @@ import {
|
||||||
saveScrollPosition,
|
saveScrollPosition,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
showLocal,
|
showLocal,
|
||||||
toast,
|
|
||||||
wsClient,
|
|
||||||
wsSubscribe,
|
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { CommentNodes } from "./comment/comment-nodes";
|
import { CommentNodes } from "./comment/comment-nodes";
|
||||||
import { HtmlTags } from "./common/html-tags";
|
import { HtmlTags } from "./common/html-tags";
|
||||||
|
@ -83,17 +73,18 @@ interface SearchProps {
|
||||||
type FilterType = "creator" | "community";
|
type FilterType = "creator" | "community";
|
||||||
|
|
||||||
interface SearchState {
|
interface SearchState {
|
||||||
searchResponse?: SearchResponse;
|
searchRes: RequestState<SearchResponse>;
|
||||||
communities: CommunityView[];
|
resolveObjectRes: RequestState<ResolveObjectResponse>;
|
||||||
creatorDetails?: GetPersonDetailsResponse;
|
creatorDetailsRes: RequestState<GetPersonDetailsResponse>;
|
||||||
searchLoading: boolean;
|
communitiesRes: RequestState<ListCommunitiesResponse>;
|
||||||
searchCommunitiesLoading: boolean;
|
communityRes: RequestState<GetCommunityResponse>;
|
||||||
searchCreatorLoading: boolean;
|
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
searchText?: string;
|
searchText?: string;
|
||||||
resolveObjectResponse?: ResolveObjectResponse;
|
|
||||||
communitySearchOptions: Choice[];
|
communitySearchOptions: Choice[];
|
||||||
creatorSearchOptions: Choice[];
|
creatorSearchOptions: Choice[];
|
||||||
|
searchCreatorLoading: boolean;
|
||||||
|
searchCommunitiesLoading: boolean;
|
||||||
|
isIsomorphic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Combined {
|
interface Combined {
|
||||||
|
@ -238,15 +229,18 @@ function getListing(
|
||||||
|
|
||||||
export class Search extends Component<any, SearchState> {
|
export class Search extends Component<any, SearchState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
|
||||||
state: SearchState = {
|
state: SearchState = {
|
||||||
searchLoading: false,
|
resolveObjectRes: { state: "empty" },
|
||||||
|
creatorDetailsRes: { state: "empty" },
|
||||||
|
communitiesRes: { state: "empty" },
|
||||||
|
communityRes: { state: "empty" },
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
communities: [],
|
|
||||||
searchCommunitiesLoading: false,
|
|
||||||
searchCreatorLoading: false,
|
|
||||||
creatorSearchOptions: [],
|
creatorSearchOptions: [],
|
||||||
communitySearchOptions: [],
|
communitySearchOptions: [],
|
||||||
|
searchRes: { state: "empty" },
|
||||||
|
searchCreatorLoading: false,
|
||||||
|
searchCommunitiesLoading: false,
|
||||||
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -259,9 +253,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
this.handleCommunityFilterChange.bind(this);
|
this.handleCommunityFilterChange.bind(this);
|
||||||
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
|
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
|
||||||
|
|
||||||
const { q } = getSearchQueryParams();
|
const { q } = getSearchQueryParams();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -270,71 +261,70 @@ export class Search extends Component<any, SearchState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (this.isoData.path === this.context.router.route.match.url) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
const communityRes = this.isoData.routeData[0] as
|
const [
|
||||||
| GetCommunityResponse
|
communityRes,
|
||||||
| undefined;
|
communitiesRes,
|
||||||
const communitiesRes = this.isoData.routeData[1] as
|
creatorDetailsRes,
|
||||||
| ListCommunitiesResponse
|
searchRes,
|
||||||
| undefined;
|
resolveObjectRes,
|
||||||
// This can be single or multiple communities given
|
] = this.isoData.routeData;
|
||||||
if (communitiesRes) {
|
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
communitiesRes,
|
||||||
|
communityRes,
|
||||||
|
creatorDetailsRes,
|
||||||
|
creatorSearchOptions:
|
||||||
|
creatorDetailsRes.state == "success"
|
||||||
|
? [personToChoice(creatorDetailsRes.data.person_view)]
|
||||||
|
: [],
|
||||||
|
isIsomorphic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (communityRes.state === "success") {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
communities: communitiesRes.communities,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (communityRes) {
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
communities: [communityRes.community_view],
|
|
||||||
communitySearchOptions: [
|
communitySearchOptions: [
|
||||||
communityToChoice(communityRes.community_view),
|
communityToChoice(communityRes.data.community_view),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const creatorRes = this.isoData.routeData[2] as GetPersonDetailsResponse;
|
if (q) {
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...this.state,
|
|
||||||
creatorDetails: creatorRes,
|
|
||||||
creatorSearchOptions: creatorRes
|
|
||||||
? [personToChoice(creatorRes.person_view)]
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (q !== "") {
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
searchResponse: this.isoData.routeData[3] as SearchResponse,
|
searchRes,
|
||||||
resolveObjectResponse: this.isoData
|
resolveObjectRes,
|
||||||
.routeData[4] as ResolveObjectResponse,
|
|
||||||
searchLoading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const listCommunitiesForm: ListCommunities = {
|
|
||||||
type_: defaultListingType,
|
|
||||||
sort: defaultSortType,
|
|
||||||
limit: fetchLimit,
|
|
||||||
auth: myAuth(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.listCommunities(listCommunitiesForm)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (q) {
|
|
||||||
this.search();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
if (!this.state.isIsomorphic) {
|
||||||
|
const promises = [this.fetchCommunities()];
|
||||||
|
if (this.state.searchText) {
|
||||||
|
promises.push(this.search());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCommunities() {
|
||||||
|
this.setState({ communitiesRes: { state: "loading" } });
|
||||||
|
this.setState({
|
||||||
|
communitiesRes: await HttpService.client.listCommunities({
|
||||||
|
type_: defaultListingType,
|
||||||
|
sort: defaultSortType,
|
||||||
|
limit: fetchLimit,
|
||||||
|
auth: myAuth(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
saveScrollPosition(this.context);
|
saveScrollPosition(this.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,8 +332,10 @@ export class Search extends Component<any, SearchState> {
|
||||||
client,
|
client,
|
||||||
auth,
|
auth,
|
||||||
query: { communityId, creatorId, q, type, sort, listingType, page },
|
query: { communityId, creatorId, q, type, sort, listingType, page },
|
||||||
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<any>[] {
|
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
|
||||||
const promises: Promise<any>[] = [];
|
RequestState<any>
|
||||||
|
>[] {
|
||||||
|
const promises: Promise<RequestState<any>>[] = [];
|
||||||
|
|
||||||
const community_id = getIdFromString(communityId);
|
const community_id = getIdFromString(communityId);
|
||||||
if (community_id) {
|
if (community_id) {
|
||||||
|
@ -352,7 +344,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(client.getCommunity(getCommunityForm));
|
promises.push(client.getCommunity(getCommunityForm));
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
} else {
|
} else {
|
||||||
const listCommunitiesForm: ListCommunities = {
|
const listCommunitiesForm: ListCommunities = {
|
||||||
type_: defaultListingType,
|
type_: defaultListingType,
|
||||||
|
@ -360,7 +352,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
promises.push(client.listCommunities(listCommunitiesForm));
|
promises.push(client.listCommunities(listCommunitiesForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,7 +364,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
};
|
};
|
||||||
promises.push(client.getPersonDetails(getCreatorForm));
|
promises.push(client.getPersonDetails(getCreatorForm));
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = getSearchQueryFromQuery(q);
|
const query = getSearchQueryFromQuery(q);
|
||||||
|
@ -400,8 +392,8 @@ export class Search extends Component<any, SearchState> {
|
||||||
promises.push(client.resolveObject(resolveObjectForm));
|
promises.push(client.resolveObject(resolveObjectForm));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve({ state: "empty" }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,9 +419,10 @@ export class Search extends Component<any, SearchState> {
|
||||||
{this.selects}
|
{this.selects}
|
||||||
{this.searchForm}
|
{this.searchForm}
|
||||||
{this.displayResults(type)}
|
{this.displayResults(type)}
|
||||||
{this.resultsCount === 0 && !this.state.searchLoading && (
|
{this.resultsCount === 0 &&
|
||||||
<span>{i18n.t("no_results")}</span>
|
this.state.searchRes.state === "success" && (
|
||||||
)}
|
<span>{i18n.t("no_results")}</span>
|
||||||
|
)}
|
||||||
<Paginator page={page} onChange={this.handlePageChange} />
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -470,7 +463,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
minLength={1}
|
minLength={1}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="btn btn-secondary mr-2 mb-2">
|
<button type="submit" className="btn btn-secondary mr-2 mb-2">
|
||||||
{this.state.searchLoading ? (
|
{this.state.searchRes.state == "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<span>{i18n.t("search")}</span>
|
<span>{i18n.t("search")}</span>
|
||||||
|
@ -488,8 +481,13 @@ export class Search extends Component<any, SearchState> {
|
||||||
creatorSearchOptions,
|
creatorSearchOptions,
|
||||||
searchCommunitiesLoading,
|
searchCommunitiesLoading,
|
||||||
searchCreatorLoading,
|
searchCreatorLoading,
|
||||||
|
communitiesRes,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const hasCommunities =
|
||||||
|
communitiesRes.state == "success" &&
|
||||||
|
communitiesRes.data.communities.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select
|
<select
|
||||||
|
@ -524,14 +522,14 @@ export class Search extends Component<any, SearchState> {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
{this.state.communities.length > 0 && (
|
{hasCommunities && (
|
||||||
<Filter
|
<Filter
|
||||||
filterType="community"
|
filterType="community"
|
||||||
onChange={this.handleCommunityFilterChange}
|
onChange={this.handleCommunityFilterChange}
|
||||||
onSearch={this.handleCommunitySearch}
|
onSearch={this.handleCommunitySearch}
|
||||||
options={communitySearchOptions}
|
options={communitySearchOptions}
|
||||||
loading={searchCommunitiesLoading}
|
|
||||||
value={communityId}
|
value={communityId}
|
||||||
|
loading={searchCommunitiesLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Filter
|
<Filter
|
||||||
|
@ -539,8 +537,8 @@ export class Search extends Component<any, SearchState> {
|
||||||
onChange={this.handleCreatorFilterChange}
|
onChange={this.handleCreatorFilterChange}
|
||||||
onSearch={this.handleCreatorSearch}
|
onSearch={this.handleCreatorSearch}
|
||||||
options={creatorSearchOptions}
|
options={creatorSearchOptions}
|
||||||
loading={searchCreatorLoading}
|
|
||||||
value={creatorId}
|
value={creatorId}
|
||||||
|
loading={searchCreatorLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -549,11 +547,14 @@ export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
buildCombined(): Combined[] {
|
buildCombined(): Combined[] {
|
||||||
const combined: Combined[] = [];
|
const combined: Combined[] = [];
|
||||||
const { resolveObjectResponse, searchResponse } = this.state;
|
const {
|
||||||
|
resolveObjectRes: resolveObjectResponse,
|
||||||
|
searchRes: searchResponse,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
// Push the possible resolve / federated objects first
|
// Push the possible resolve / federated objects first
|
||||||
if (resolveObjectResponse) {
|
if (resolveObjectResponse.state == "success") {
|
||||||
const { comment, post, community, person } = resolveObjectResponse;
|
const { comment, post, community, person } = resolveObjectResponse.data;
|
||||||
|
|
||||||
if (comment) {
|
if (comment) {
|
||||||
combined.push(commentViewToCombined(comment));
|
combined.push(commentViewToCombined(comment));
|
||||||
|
@ -570,8 +571,8 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the search results
|
// Push the search results
|
||||||
if (searchResponse) {
|
if (searchResponse.state === "success") {
|
||||||
const { comments, posts, communities, users } = searchResponse;
|
const { comments, posts, communities, users } = searchResponse.data;
|
||||||
|
|
||||||
combined.push(
|
combined.push(
|
||||||
...[
|
...[
|
||||||
|
@ -622,6 +623,23 @@ export class Search extends Component<any, SearchState> {
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
allLanguages={this.state.siteRes.all_languages}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
viewOnly
|
viewOnly
|
||||||
|
// All of these are unused, since its view only
|
||||||
|
onPostEdit={() => {}}
|
||||||
|
onPostVote={() => {}}
|
||||||
|
onPostReport={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onLockPost={() => {}}
|
||||||
|
onDeletePost={() => {}}
|
||||||
|
onRemovePost={() => {}}
|
||||||
|
onSavePost={() => {}}
|
||||||
|
onFeaturePost={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onPurgePost={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{i.type_ === "comments" && (
|
{i.type_ === "comments" && (
|
||||||
|
@ -641,6 +659,26 @@ export class Search extends Component<any, SearchState> {
|
||||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
allLanguages={this.state.siteRes.all_languages}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
|
// All of these are unused, since its viewonly
|
||||||
|
finished={new Map()}
|
||||||
|
onSaveComment={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onDeleteComment={() => {}}
|
||||||
|
onRemoveComment={() => {}}
|
||||||
|
onCommentVote={() => {}}
|
||||||
|
onCommentReport={() => {}}
|
||||||
|
onDistinguishComment={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
|
onPurgeComment={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onCommentReplyRead={() => {}}
|
||||||
|
onPersonMentionRead={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onCreateComment={() => Promise.resolve({ state: "empty" })}
|
||||||
|
onEditComment={() => Promise.resolve({ state: "empty" })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{i.type_ === "communities" && (
|
{i.type_ === "communities" && (
|
||||||
|
@ -657,11 +695,19 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get comments() {
|
get comments() {
|
||||||
const { searchResponse, resolveObjectResponse, siteRes } = this.state;
|
const {
|
||||||
const comments = searchResponse?.comments ?? [];
|
searchRes: searchResponse,
|
||||||
|
resolveObjectRes: resolveObjectResponse,
|
||||||
|
siteRes,
|
||||||
|
} = this.state;
|
||||||
|
const comments =
|
||||||
|
searchResponse.state === "success" ? searchResponse.data.comments : [];
|
||||||
|
|
||||||
if (resolveObjectResponse?.comment) {
|
if (
|
||||||
comments.unshift(resolveObjectResponse?.comment);
|
resolveObjectResponse.state === "success" &&
|
||||||
|
resolveObjectResponse.data.comment
|
||||||
|
) {
|
||||||
|
comments.unshift(resolveObjectResponse.data.comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -674,16 +720,44 @@ export class Search extends Component<any, SearchState> {
|
||||||
enableDownvotes={enableDownvotes(siteRes)}
|
enableDownvotes={enableDownvotes(siteRes)}
|
||||||
allLanguages={siteRes.all_languages}
|
allLanguages={siteRes.all_languages}
|
||||||
siteLanguages={siteRes.discussion_languages}
|
siteLanguages={siteRes.discussion_languages}
|
||||||
|
// All of these are unused, since its viewonly
|
||||||
|
finished={new Map()}
|
||||||
|
onSaveComment={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onDeleteComment={() => {}}
|
||||||
|
onRemoveComment={() => {}}
|
||||||
|
onCommentVote={() => {}}
|
||||||
|
onCommentReport={() => {}}
|
||||||
|
onDistinguishComment={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
|
onPurgeComment={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onCommentReplyRead={() => {}}
|
||||||
|
onPersonMentionRead={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onCreateComment={() => Promise.resolve({ state: "empty" })}
|
||||||
|
onEditComment={() => Promise.resolve({ state: "empty" })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get posts() {
|
get posts() {
|
||||||
const { searchResponse, resolveObjectResponse, siteRes } = this.state;
|
const {
|
||||||
const posts = searchResponse?.posts ?? [];
|
searchRes: searchResponse,
|
||||||
|
resolveObjectRes: resolveObjectResponse,
|
||||||
|
siteRes,
|
||||||
|
} = this.state;
|
||||||
|
const posts =
|
||||||
|
searchResponse.state === "success" ? searchResponse.data.posts : [];
|
||||||
|
|
||||||
if (resolveObjectResponse?.post) {
|
if (
|
||||||
posts.unshift(resolveObjectResponse.post);
|
resolveObjectResponse.state === "success" &&
|
||||||
|
resolveObjectResponse.data.post
|
||||||
|
) {
|
||||||
|
posts.unshift(resolveObjectResponse.data.post);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -699,6 +773,23 @@ export class Search extends Component<any, SearchState> {
|
||||||
allLanguages={siteRes.all_languages}
|
allLanguages={siteRes.all_languages}
|
||||||
siteLanguages={siteRes.discussion_languages}
|
siteLanguages={siteRes.discussion_languages}
|
||||||
viewOnly
|
viewOnly
|
||||||
|
// All of these are unused, since its view only
|
||||||
|
onPostEdit={() => {}}
|
||||||
|
onPostVote={() => {}}
|
||||||
|
onPostReport={() => {}}
|
||||||
|
onBlockPerson={() => {}}
|
||||||
|
onLockPost={() => {}}
|
||||||
|
onDeletePost={() => {}}
|
||||||
|
onRemovePost={() => {}}
|
||||||
|
onSavePost={() => {}}
|
||||||
|
onFeaturePost={() => {}}
|
||||||
|
onPurgePerson={() => {}}
|
||||||
|
onPurgePost={() => {}}
|
||||||
|
onBanPersonFromCommunity={() => {}}
|
||||||
|
onBanPerson={() => {}}
|
||||||
|
onAddModToCommunity={() => {}}
|
||||||
|
onAddAdmin={() => {}}
|
||||||
|
onTransferCommunity={() => {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -708,11 +799,18 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get communities() {
|
get communities() {
|
||||||
const { searchResponse, resolveObjectResponse } = this.state;
|
const {
|
||||||
const communities = searchResponse?.communities ?? [];
|
searchRes: searchResponse,
|
||||||
|
resolveObjectRes: resolveObjectResponse,
|
||||||
|
} = this.state;
|
||||||
|
const communities =
|
||||||
|
searchResponse.state === "success" ? searchResponse.data.communities : [];
|
||||||
|
|
||||||
if (resolveObjectResponse?.community) {
|
if (
|
||||||
communities.unshift(resolveObjectResponse.community);
|
resolveObjectResponse.state === "success" &&
|
||||||
|
resolveObjectResponse.data.community
|
||||||
|
) {
|
||||||
|
communities.unshift(resolveObjectResponse.data.community);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -727,11 +825,18 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get users() {
|
get users() {
|
||||||
const { searchResponse, resolveObjectResponse } = this.state;
|
const {
|
||||||
const users = searchResponse?.users ?? [];
|
searchRes: searchResponse,
|
||||||
|
resolveObjectRes: resolveObjectResponse,
|
||||||
|
} = this.state;
|
||||||
|
const users =
|
||||||
|
searchResponse.state === "success" ? searchResponse.data.users : [];
|
||||||
|
|
||||||
if (resolveObjectResponse?.person) {
|
if (
|
||||||
users.unshift(resolveObjectResponse.person);
|
resolveObjectResponse.state === "success" &&
|
||||||
|
resolveObjectResponse.data.person
|
||||||
|
) {
|
||||||
|
users.unshift(resolveObjectResponse.data.person);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -746,75 +851,72 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get resultsCount(): number {
|
get resultsCount(): number {
|
||||||
const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
|
const { searchRes: r, resolveObjectRes: resolveRes } = this.state;
|
||||||
|
|
||||||
const searchCount = r
|
const searchCount =
|
||||||
? r.posts.length +
|
r.state === "success"
|
||||||
r.comments.length +
|
? r.data.posts.length +
|
||||||
r.communities.length +
|
r.data.comments.length +
|
||||||
r.users.length
|
r.data.communities.length +
|
||||||
: 0;
|
r.data.users.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
const resObjCount = resolveRes
|
const resObjCount =
|
||||||
? resolveRes.post ||
|
resolveRes.state === "success"
|
||||||
resolveRes.person ||
|
? resolveRes.data.post ||
|
||||||
resolveRes.community ||
|
resolveRes.data.person ||
|
||||||
resolveRes.comment
|
resolveRes.data.community ||
|
||||||
? 1
|
resolveRes.data.comment
|
||||||
: 0
|
? 1
|
||||||
: 0;
|
: 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
return resObjCount + searchCount;
|
return resObjCount + searchCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
search() {
|
async search() {
|
||||||
const auth = myAuth(false);
|
const auth = myAuth();
|
||||||
const { searchText: q } = this.state;
|
const { searchText: q } = this.state;
|
||||||
const { communityId, creatorId, type, sort, listingType, page } =
|
const { communityId, creatorId, type, sort, listingType, page } =
|
||||||
getSearchQueryParams();
|
getSearchQueryParams();
|
||||||
|
|
||||||
if (q && q !== "") {
|
if (q) {
|
||||||
const form: SearchForm = {
|
this.setState({ searchRes: { state: "loading" } });
|
||||||
q,
|
this.setState({
|
||||||
community_id: communityId ?? undefined,
|
searchRes: await HttpService.client.search({
|
||||||
creator_id: creatorId ?? undefined,
|
q,
|
||||||
type_: type,
|
community_id: communityId ?? undefined,
|
||||||
sort,
|
creator_id: creatorId ?? undefined,
|
||||||
listing_type: listingType,
|
type_: type,
|
||||||
page,
|
sort,
|
||||||
limit: fetchLimit,
|
listing_type: listingType,
|
||||||
auth,
|
page,
|
||||||
};
|
limit: fetchLimit,
|
||||||
|
auth,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
restoreScrollPosition(this.context);
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
const resolveObjectForm: ResolveObject = {
|
this.setState({ resolveObjectRes: { state: "loading" } });
|
||||||
q,
|
this.setState({
|
||||||
auth,
|
resolveObjectRes: await HttpService.client.resolveObject({
|
||||||
};
|
q,
|
||||||
WebSocketService.Instance.send(
|
auth,
|
||||||
wsClient.resolveObject(resolveObjectForm)
|
}),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
|
||||||
searchResponse: undefined,
|
|
||||||
resolveObjectResponse: undefined,
|
|
||||||
searchLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.search(form));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCreatorSearch = debounce(async (text: string) => {
|
handleCreatorSearch = debounce(async (text: string) => {
|
||||||
const { creatorId } = getSearchQueryParams();
|
const { creatorId } = getSearchQueryParams();
|
||||||
const { creatorSearchOptions } = this.state;
|
const { creatorSearchOptions } = this.state;
|
||||||
this.setState({
|
|
||||||
searchCreatorLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newOptions: Choice[] = [];
|
const newOptions: Choice[] = [];
|
||||||
|
|
||||||
|
this.setState({ searchCreatorLoading: true });
|
||||||
|
|
||||||
const selectedChoice = creatorSearchOptions.find(
|
const selectedChoice = creatorSearchOptions.find(
|
||||||
choice => getIdFromString(choice.value) === creatorId
|
choice => getIdFromString(choice.value) === creatorId
|
||||||
);
|
);
|
||||||
|
@ -824,7 +926,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
|
newOptions.push(...(await fetchUsers(text)).map(personToChoice));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -851,9 +953,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
newOptions.push(
|
newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
|
||||||
...(await fetchCommunities(text)).communities.map(communityToChoice)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -913,7 +1013,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
i.setState({ searchText: event.target.value });
|
i.setState({ searchText: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl({
|
async updateUrl({
|
||||||
q,
|
q,
|
||||||
type,
|
type,
|
||||||
listingType,
|
listingType,
|
||||||
|
@ -950,71 +1050,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
this.props.history.push(`/search${getQueryString(queryParams)}`);
|
this.props.history.push(`/search${getQueryString(queryParams)}`);
|
||||||
|
|
||||||
this.search();
|
await this.search();
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
console.log(msg);
|
|
||||||
const op = wsUserOp(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
if (msg.error === "couldnt_find_object") {
|
|
||||||
this.setState({
|
|
||||||
resolveObjectResponse: {},
|
|
||||||
});
|
|
||||||
this.checkFinishedLoading();
|
|
||||||
} else {
|
|
||||||
toast(i18n.t(msg.error), "danger");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (op) {
|
|
||||||
case UserOperation.Search: {
|
|
||||||
const searchResponse = wsJsonToRes<SearchResponse>(msg);
|
|
||||||
this.setState({ searchResponse });
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
this.checkFinishedLoading();
|
|
||||||
restoreScrollPosition(this.context);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserOperation.CreateCommentLike: {
|
|
||||||
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
createCommentLikeRes(
|
|
||||||
comment_view,
|
|
||||||
this.state.searchResponse?.comments
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserOperation.CreatePostLike: {
|
|
||||||
const { post_view } = wsJsonToRes<PostResponse>(msg);
|
|
||||||
createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserOperation.ListCommunities: {
|
|
||||||
const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
|
|
||||||
this.setState({ communities });
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserOperation.ResolveObject: {
|
|
||||||
const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
|
|
||||||
this.setState({ resolveObjectResponse });
|
|
||||||
this.checkFinishedLoading();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkFinishedLoading() {
|
|
||||||
if (this.state.searchResponse || this.state.resolveObjectResponse) {
|
|
||||||
this.setState({ searchLoading: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,6 @@ function getHost() {
|
||||||
return isBrowser() ? getExternalHost() : getInternalHost();
|
return isBrowser() ? getExternalHost() : getInternalHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWsHost() {
|
|
||||||
return isBrowser()
|
|
||||||
? window.lemmyConfig?.wsHost ?? getHost()
|
|
||||||
: process.env.LEMMY_UI_LEMMY_WS_HOST ?? getExternalHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBaseLocal(s = "") {
|
function getBaseLocal(s = "") {
|
||||||
return `http${s}://${getHost()}`;
|
return `http${s}://${getHost()}`;
|
||||||
}
|
}
|
||||||
|
@ -47,18 +41,20 @@ function getBaseLocal(s = "") {
|
||||||
export function getHttpBaseInternal() {
|
export function getHttpBaseInternal() {
|
||||||
return getBaseLocal(); // Don't use secure here
|
return getBaseLocal(); // Don't use secure here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHttpBaseExternal() {
|
||||||
|
return `http${getSecure()}://${getExternalHost()}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getHttpBase() {
|
export function getHttpBase() {
|
||||||
return getBaseLocal(getSecure());
|
return getBaseLocal(getSecure());
|
||||||
}
|
}
|
||||||
export function getWsUri() {
|
|
||||||
return `ws${getSecure()}://${getWsHost()}/api/v3/ws`;
|
|
||||||
}
|
|
||||||
export function isHttps() {
|
export function isHttps() {
|
||||||
return getSecure() === "s";
|
return getSecure() === "s";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`httpbase: ${getHttpBase()}`);
|
console.log(`httpbase: ${getHttpBase()}`);
|
||||||
console.log(`wsUri: ${getWsUri()}`);
|
|
||||||
console.log(`isHttps: ${isHttps()}`);
|
console.log(`isHttps: ${isHttps()}`);
|
||||||
|
|
||||||
// This is for html tags, don't include port
|
// This is for html tags, don't include port
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { CommentView, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
import { CommentView, GetSiteResponse } from "lemmy-js-client";
|
||||||
import type { ParsedQs } from "qs";
|
import type { ParsedQs } from "qs";
|
||||||
|
import { RequestState, WrappedLemmyHttp } from "./services/HttpService";
|
||||||
import { ErrorPageData } from "./utils";
|
import { ErrorPageData } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +8,7 @@ import { ErrorPageData } from "./utils";
|
||||||
*/
|
*/
|
||||||
export interface IsoData {
|
export interface IsoData {
|
||||||
path: string;
|
path: string;
|
||||||
routeData: any[];
|
routeData: RequestState<any>[];
|
||||||
site_res: GetSiteResponse;
|
site_res: GetSiteResponse;
|
||||||
errorPageData?: ErrorPageData;
|
errorPageData?: ErrorPageData;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ declare global {
|
||||||
|
|
||||||
export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
|
export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
|
||||||
auth?: string;
|
auth?: string;
|
||||||
client: LemmyHttp;
|
client: WrappedLemmyHttp;
|
||||||
path: string;
|
path: string;
|
||||||
query: T;
|
query: T;
|
||||||
site: GetSiteResponse;
|
site: GetSiteResponse;
|
||||||
|
@ -69,6 +70,11 @@ export enum PurgeType {
|
||||||
Comment,
|
Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum VoteType {
|
||||||
|
Upvote,
|
||||||
|
Downvote,
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommentNodeI {
|
export interface CommentNodeI {
|
||||||
comment_view: CommentView;
|
comment_view: CommentView;
|
||||||
children: Array<CommentNodeI>;
|
children: Array<CommentNodeI>;
|
||||||
|
|
|
@ -22,10 +22,11 @@ import { Post } from "./components/post/post";
|
||||||
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
|
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
|
||||||
import { Search } from "./components/search";
|
import { Search } from "./components/search";
|
||||||
import { InitialFetchRequest } from "./interfaces";
|
import { InitialFetchRequest } from "./interfaces";
|
||||||
|
import { RequestState } from "./services/HttpService";
|
||||||
|
|
||||||
interface IRoutePropsWithFetch extends IRouteProps {
|
interface IRoutePropsWithFetch extends IRouteProps {
|
||||||
// TODO Make sure this one is good.
|
// TODO Make sure this one is good.
|
||||||
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
|
fetchInitialData?(req: InitialFetchRequest): Promise<RequestState<any>>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routes: IRoutePropsWithFetch[] = [
|
export const routes: IRoutePropsWithFetch[] = [
|
||||||
|
|
25
src/shared/services/FirstLoadService.ts
Normal file
25
src/shared/services/FirstLoadService.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export class FirstLoadService {
|
||||||
|
#isFirstLoad: boolean;
|
||||||
|
static #instance: FirstLoadService;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.#isFirstLoad = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFirstLoad() {
|
||||||
|
const isFirst = this.#isFirstLoad;
|
||||||
|
if (isFirst) {
|
||||||
|
this.#isFirstLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #Instance() {
|
||||||
|
return this.#instance ?? (this.#instance = new this());
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isFirstLoad() {
|
||||||
|
return this.#Instance.isFirstLoad;
|
||||||
|
}
|
||||||
|
}
|
18
src/shared/services/HistoryService.ts
Normal file
18
src/shared/services/HistoryService.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { History, createBrowserHistory } from "history";
|
||||||
|
|
||||||
|
export class HistoryService {
|
||||||
|
static #_instance: HistoryService;
|
||||||
|
#history: History;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.#history = createBrowserHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #Instance() {
|
||||||
|
return this.#_instance ?? (this.#_instance = new this());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get history() {
|
||||||
|
return this.#Instance.#history;
|
||||||
|
}
|
||||||
|
}
|
96
src/shared/services/HttpService.ts
Normal file
96
src/shared/services/HttpService.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { LemmyHttp } from "lemmy-js-client";
|
||||||
|
import { getHttpBase } from "../../shared/env";
|
||||||
|
import { i18n } from "../../shared/i18next";
|
||||||
|
import { toast } from "../../shared/utils";
|
||||||
|
|
||||||
|
type EmptyRequestState = {
|
||||||
|
state: "empty";
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoadingRequestState = {
|
||||||
|
state: "loading";
|
||||||
|
};
|
||||||
|
|
||||||
|
type FailedRequestState = {
|
||||||
|
state: "failed";
|
||||||
|
msg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SuccessRequestState<T> = {
|
||||||
|
state: "success";
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the state of an API request.
|
||||||
|
*
|
||||||
|
* Can be empty, loading, failed, or success
|
||||||
|
*/
|
||||||
|
export type RequestState<T> =
|
||||||
|
| EmptyRequestState
|
||||||
|
| LoadingRequestState
|
||||||
|
| FailedRequestState
|
||||||
|
| SuccessRequestState<T>;
|
||||||
|
|
||||||
|
export type WrappedLemmyHttp = {
|
||||||
|
[K in keyof LemmyHttp]: LemmyHttp[K] extends (...args: any[]) => any
|
||||||
|
? ReturnType<LemmyHttp[K]> extends Promise<infer U>
|
||||||
|
? (...args: Parameters<LemmyHttp[K]>) => Promise<RequestState<U>>
|
||||||
|
: (
|
||||||
|
...args: Parameters<LemmyHttp[K]>
|
||||||
|
) => Promise<RequestState<LemmyHttp[K]>>
|
||||||
|
: LemmyHttp[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
class WrappedLemmyHttpClient {
|
||||||
|
#client: LemmyHttp;
|
||||||
|
|
||||||
|
constructor(client: LemmyHttp) {
|
||||||
|
this.#client = client;
|
||||||
|
|
||||||
|
for (const key of Object.getOwnPropertyNames(
|
||||||
|
Object.getPrototypeOf(this.#client)
|
||||||
|
)) {
|
||||||
|
if (key !== "constructor") {
|
||||||
|
WrappedLemmyHttpClient.prototype[key] = async (...args) => {
|
||||||
|
try {
|
||||||
|
const res = await this.#client[key](...args);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: res,
|
||||||
|
state: "success",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API error: ${error}`);
|
||||||
|
toast(i18n.t(error), "danger");
|
||||||
|
return {
|
||||||
|
state: "failed",
|
||||||
|
msg: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapClient(client: LemmyHttp) {
|
||||||
|
return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpService {
|
||||||
|
static #_instance: HttpService;
|
||||||
|
#client: WrappedLemmyHttp;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.#client = wrapClient(new LemmyHttp(getHttpBase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static get #Instance() {
|
||||||
|
return this.#_instance ?? (this.#_instance = new this());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get client() {
|
||||||
|
return this.#Instance.#client;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
import IsomorphicCookie from "isomorphic-cookie";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
import { isHttps } from "../env";
|
import { isHttps } from "../env";
|
||||||
import { i18n } from "../i18next";
|
import { i18n } from "../i18next";
|
||||||
import { isAuthPath, isBrowser, toast } from "../utils";
|
import { isAuthPath, isBrowser, toast } from "../utils";
|
||||||
|
@ -19,27 +18,21 @@ interface JwtInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private static _instance: UserService;
|
static #instance: UserService;
|
||||||
public myUserInfo?: MyUserInfo;
|
public myUserInfo?: MyUserInfo;
|
||||||
public jwtInfo?: JwtInfo;
|
public jwtInfo?: JwtInfo;
|
||||||
public unreadInboxCountSub: BehaviorSubject<number> =
|
|
||||||
new BehaviorSubject<number>(0);
|
|
||||||
public unreadReportCountSub: BehaviorSubject<number> =
|
|
||||||
new BehaviorSubject<number>(0);
|
|
||||||
public unreadApplicationCountSub: BehaviorSubject<number> =
|
|
||||||
new BehaviorSubject<number>(0);
|
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.setJwtInfo();
|
this.#setJwtInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
let expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 365);
|
expires.setDate(expires.getDate() + 365);
|
||||||
if (res.jwt) {
|
if (res.jwt) {
|
||||||
toast(i18n.t("logged_in"));
|
toast(i18n.t("logged_in"));
|
||||||
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps() });
|
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps() });
|
||||||
this.setJwtInfo();
|
this.#setJwtInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +48,12 @@ export class UserService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public auth(throwErr = true): string | undefined {
|
public auth(throwErr = false): string | undefined {
|
||||||
let jwt = this.jwtInfo?.jwt;
|
const jwt = this.jwtInfo?.jwt;
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
return jwt;
|
return jwt;
|
||||||
} else {
|
} else {
|
||||||
let msg = "No JWT cookie found";
|
const msg = "No JWT cookie found";
|
||||||
if (throwErr && isBrowser()) {
|
if (throwErr && isBrowser()) {
|
||||||
console.error(msg);
|
console.error(msg);
|
||||||
toast(i18n.t("not_logged_in"), "danger");
|
toast(i18n.t("not_logged_in"), "danger");
|
||||||
|
@ -70,8 +63,8 @@ export class UserService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setJwtInfo() {
|
#setJwtInfo() {
|
||||||
let jwt: string | undefined = IsomorphicCookie.load("jwt");
|
const jwt: string | undefined = IsomorphicCookie.load("jwt");
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
||||||
|
@ -79,6 +72,6 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance() {
|
public static get Instance() {
|
||||||
return this._instance || (this._instance = new this());
|
return this.#instance || (this.#instance = new this());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { share } from "rxjs/operators";
|
|
||||||
import {
|
|
||||||
ExponentialBackoff,
|
|
||||||
LRUBuffer,
|
|
||||||
Websocket as WS,
|
|
||||||
WebsocketBuilder,
|
|
||||||
} from "websocket-ts";
|
|
||||||
import { getWsUri } from "../env";
|
|
||||||
import { isBrowser } from "../utils";
|
|
||||||
|
|
||||||
export class WebSocketService {
|
|
||||||
private static _instance: WebSocketService;
|
|
||||||
private ws: WS;
|
|
||||||
public subject: Observable<any>;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
let firstConnect = true;
|
|
||||||
|
|
||||||
this.subject = new Observable((obs: any) => {
|
|
||||||
this.ws = new WebsocketBuilder(getWsUri())
|
|
||||||
.onMessage((_i, e) => {
|
|
||||||
try {
|
|
||||||
obs.next(JSON.parse(e.data.toString()));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onOpen(() => {
|
|
||||||
console.log(`Connected to ${getWsUri()}`);
|
|
||||||
|
|
||||||
if (!firstConnect) {
|
|
||||||
let res = {
|
|
||||||
reconnect: true,
|
|
||||||
};
|
|
||||||
obs.next(res);
|
|
||||||
}
|
|
||||||
firstConnect = false;
|
|
||||||
})
|
|
||||||
.onRetry(() => {
|
|
||||||
console.log("Retrying websocket connection...");
|
|
||||||
})
|
|
||||||
.onClose(() => {
|
|
||||||
console.error("Websocket closed.");
|
|
||||||
})
|
|
||||||
.withBackoff(new ExponentialBackoff(100, 7))
|
|
||||||
.withBuffer(new LRUBuffer(1000))
|
|
||||||
.build();
|
|
||||||
}).pipe(share());
|
|
||||||
|
|
||||||
if (isBrowser()) {
|
|
||||||
window.onunload = () => {
|
|
||||||
this.ws.close();
|
|
||||||
|
|
||||||
// Clears out scroll positions.
|
|
||||||
sessionStorage.clear();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public send(data: string) {
|
|
||||||
this.ws.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get Instance() {
|
|
||||||
return this._instance || (this._instance = new this());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +1,2 @@
|
||||||
|
export { HttpService } from "./HttpService";
|
||||||
export { UserService } from "./UserService";
|
export { UserService } from "./UserService";
|
||||||
export { WebSocketService } from "./WebSocketService";
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ import emojiShortName from "emoji-short-name";
|
||||||
import {
|
import {
|
||||||
BlockCommunityResponse,
|
BlockCommunityResponse,
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
|
CommentAggregates,
|
||||||
Comment as CommentI,
|
Comment as CommentI,
|
||||||
|
CommentReplyView,
|
||||||
CommentReportView,
|
CommentReportView,
|
||||||
CommentSortType,
|
CommentSortType,
|
||||||
CommentView,
|
CommentView,
|
||||||
|
@ -14,9 +16,9 @@ import {
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Language,
|
Language,
|
||||||
LemmyHttp,
|
LemmyHttp,
|
||||||
LemmyWebsocket,
|
|
||||||
MyUserInfo,
|
MyUserInfo,
|
||||||
Person,
|
Person,
|
||||||
|
PersonMentionView,
|
||||||
PersonView,
|
PersonView,
|
||||||
PostReportView,
|
PostReportView,
|
||||||
PostView,
|
PostView,
|
||||||
|
@ -24,8 +26,8 @@ import {
|
||||||
PrivateMessageView,
|
PrivateMessageView,
|
||||||
RegistrationApplicationView,
|
RegistrationApplicationView,
|
||||||
Search,
|
Search,
|
||||||
|
SearchType,
|
||||||
SortType,
|
SortType,
|
||||||
UploadImageResponse,
|
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { default as MarkdownIt } from "markdown-it";
|
import { default as MarkdownIt } from "markdown-it";
|
||||||
import markdown_it_container from "markdown-it-container";
|
import markdown_it_container from "markdown-it-container";
|
||||||
|
@ -37,22 +39,18 @@ import markdown_it_sup from "markdown-it-sup";
|
||||||
import Renderer from "markdown-it/lib/renderer";
|
import Renderer from "markdown-it/lib/renderer";
|
||||||
import Token from "markdown-it/lib/token";
|
import Token from "markdown-it/lib/token";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Subscription } from "rxjs";
|
|
||||||
import { delay, retryWhen, take } from "rxjs/operators";
|
|
||||||
import tippy from "tippy.js";
|
import tippy from "tippy.js";
|
||||||
import Toastify from "toastify-js";
|
import Toastify from "toastify-js";
|
||||||
import { getHttpBase } from "./env";
|
import { getHttpBase } from "./env";
|
||||||
import { i18n, languages } from "./i18next";
|
import { i18n, languages } from "./i18next";
|
||||||
import { CommentNodeI, DataType, IsoData } from "./interfaces";
|
import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
|
||||||
import { UserService, WebSocketService } from "./services";
|
import { HttpService, UserService } from "./services";
|
||||||
|
|
||||||
let Tribute: any;
|
let Tribute: any;
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
Tribute = require("tributejs");
|
Tribute = require("tributejs");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wsClient = new LemmyWebsocket();
|
|
||||||
|
|
||||||
export const favIconUrl = "/static/assets/icons/favicon.svg";
|
export const favIconUrl = "/static/assets/icons/favicon.svg";
|
||||||
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
|
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -110,7 +108,7 @@ export interface ErrorPageData {
|
||||||
adminMatrixIds?: string[];
|
adminMatrixIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let customEmojis: EmojiMartCategory[] = [];
|
const customEmojis: EmojiMartCategory[] = [];
|
||||||
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
|
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
|
||||||
string,
|
string,
|
||||||
CustomEmojiView
|
CustomEmojiView
|
||||||
|
@ -192,11 +190,11 @@ export function hotRankPost(post_view: PostView): number {
|
||||||
|
|
||||||
export function hotRank(score: number, timeStr: string): number {
|
export function hotRank(score: number, timeStr: string): number {
|
||||||
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
|
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
|
||||||
let date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
|
const date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
|
||||||
let now: Date = new Date();
|
const now: Date = new Date();
|
||||||
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
|
const hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
|
||||||
|
|
||||||
let rank =
|
const rank =
|
||||||
(10000 * Math.log10(Math.max(1, 3 + Number(score)))) /
|
(10000 * Math.log10(Math.max(1, 3 + Number(score)))) /
|
||||||
Math.pow(hoursElapsed + 2, 1.8);
|
Math.pow(hoursElapsed + 2, 1.8);
|
||||||
|
|
||||||
|
@ -243,7 +241,7 @@ export function canMod(
|
||||||
.concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
|
.concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
|
||||||
|
|
||||||
if (myUserInfo) {
|
if (myUserInfo) {
|
||||||
let myIndex = adminsThenMods.findIndex(
|
const myIndex = adminsThenMods.findIndex(
|
||||||
id => id == myUserInfo.local_user_view.person.id
|
id => id == myUserInfo.local_user_view.person.id
|
||||||
);
|
);
|
||||||
if (myIndex == -1) {
|
if (myIndex == -1) {
|
||||||
|
@ -294,7 +292,7 @@ export function amCommunityCreator(
|
||||||
mods?: CommunityModeratorView[],
|
mods?: CommunityModeratorView[],
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): boolean {
|
): boolean {
|
||||||
let myId = myUserInfo?.local_user_view.person.id;
|
const myId = myUserInfo?.local_user_view.person.id;
|
||||||
// Don't allow mod actions on yourself
|
// Don't allow mod actions on yourself
|
||||||
return myId == mods?.at(0)?.moderator.id && myId != creator_id;
|
return myId == mods?.at(0)?.moderator.id && myId != creator_id;
|
||||||
}
|
}
|
||||||
|
@ -304,7 +302,7 @@ export function amSiteCreator(
|
||||||
admins?: PersonView[],
|
admins?: PersonView[],
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): boolean {
|
): boolean {
|
||||||
let myId = myUserInfo?.local_user_view.person.id;
|
const myId = myUserInfo?.local_user_view.person.id;
|
||||||
return myId == admins?.at(0)?.person.id && myId != creator_id;
|
return myId == admins?.at(0)?.person.id && myId != creator_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,12 +329,12 @@ export function validURL(str: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function communityRSSUrl(actorId: string, sort: string): string {
|
export function communityRSSUrl(actorId: string, sort: string): string {
|
||||||
let url = new URL(actorId);
|
const url = new URL(actorId);
|
||||||
return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
|
return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validEmail(email: string) {
|
export function validEmail(email: string) {
|
||||||
let re =
|
const re =
|
||||||
/^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
|
/^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
|
||||||
return re.test(String(email).toLowerCase());
|
return re.test(String(email).toLowerCase());
|
||||||
}
|
}
|
||||||
|
@ -346,8 +344,8 @@ export function capitalizeFirstLetter(str: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSiteMetadata(url: string) {
|
export async function getSiteMetadata(url: string) {
|
||||||
let form: GetSiteMetadata = { url };
|
const form: GetSiteMetadata = { url };
|
||||||
let client = new LemmyHttp(getHttpBase());
|
const client = new LemmyHttp(getHttpBase());
|
||||||
return client.getSiteMetadata(form);
|
return client.getSiteMetadata(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,8 +402,8 @@ export function getLanguages(
|
||||||
override?: string,
|
override?: string,
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): string[] {
|
): string[] {
|
||||||
let myLang = myUserInfo?.local_user_view.local_user.interface_language;
|
const myLang = myUserInfo?.local_user_view.local_user.interface_language;
|
||||||
let lang = override || myLang || "browser";
|
const lang = override || myLang || "browser";
|
||||||
|
|
||||||
if (lang == "browser" && isBrowser()) {
|
if (lang == "browser" && isBrowser()) {
|
||||||
return getBrowserLanguages();
|
return getBrowserLanguages();
|
||||||
|
@ -416,10 +414,10 @@ export function getLanguages(
|
||||||
|
|
||||||
function getBrowserLanguages(): string[] {
|
function getBrowserLanguages(): string[] {
|
||||||
// Intersect lemmy's langs, with the browser langs
|
// Intersect lemmy's langs, with the browser langs
|
||||||
let langs = languages ? languages.map(l => l.code) : ["en"];
|
const langs = languages ? languages.map(l => l.code) : ["en"];
|
||||||
|
|
||||||
// NOTE, mobile browsers seem to be missing this list, so append en
|
// NOTE, mobile browsers seem to be missing this list, so append en
|
||||||
let allowedLangs = navigator.languages
|
const allowedLangs = navigator.languages
|
||||||
.concat("en")
|
.concat("en")
|
||||||
.filter(v => langs.includes(v));
|
.filter(v => langs.includes(v));
|
||||||
return allowedLangs;
|
return allowedLangs;
|
||||||
|
@ -441,11 +439,11 @@ export async function setTheme(theme: string, forceReload = false) {
|
||||||
theme = "darkly";
|
theme = "darkly";
|
||||||
}
|
}
|
||||||
|
|
||||||
let themeList = await fetchThemeList();
|
const themeList = await fetchThemeList();
|
||||||
|
|
||||||
// Unload all the other themes
|
// Unload all the other themes
|
||||||
for (var i = 0; i < themeList.length; i++) {
|
for (var i = 0; i < themeList.length; i++) {
|
||||||
let styleSheet = document.getElementById(themeList[i]);
|
const styleSheet = document.getElementById(themeList[i]);
|
||||||
if (styleSheet) {
|
if (styleSheet) {
|
||||||
styleSheet.setAttribute("disabled", "disabled");
|
styleSheet.setAttribute("disabled", "disabled");
|
||||||
}
|
}
|
||||||
|
@ -457,7 +455,7 @@ export async function setTheme(theme: string, forceReload = false) {
|
||||||
document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
|
document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
|
||||||
|
|
||||||
// Load the theme dynamically
|
// Load the theme dynamically
|
||||||
let cssLoc = `/css/themes/${theme}.css`;
|
const cssLoc = `/css/themes/${theme}.css`;
|
||||||
|
|
||||||
loadCss(theme, cssLoc);
|
loadCss(theme, cssLoc);
|
||||||
document.getElementById(theme)?.removeAttribute("disabled");
|
document.getElementById(theme)?.removeAttribute("disabled");
|
||||||
|
@ -559,86 +557,6 @@ export function pictrsDeleteToast(filename: string, deleteUrl: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotifyInfo {
|
|
||||||
name: string;
|
|
||||||
icon?: string;
|
|
||||||
link: string;
|
|
||||||
body?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function messageToastify(info: NotifyInfo, router: any) {
|
|
||||||
if (isBrowser()) {
|
|
||||||
let htmlBody = info.body ? md.render(info.body) : "";
|
|
||||||
let backgroundColor = `var(--light)`;
|
|
||||||
|
|
||||||
let toast = Toastify({
|
|
||||||
text: `${htmlBody}<br />${info.name}`,
|
|
||||||
avatar: info.icon,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
className: "text-dark",
|
|
||||||
close: true,
|
|
||||||
gravity: "top",
|
|
||||||
position: "right",
|
|
||||||
duration: 5000,
|
|
||||||
escapeMarkup: false,
|
|
||||||
onClick: () => {
|
|
||||||
if (toast) {
|
|
||||||
toast.hideToast();
|
|
||||||
router.history.push(info.link);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.showToast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifyPost(post_view: PostView, router: any) {
|
|
||||||
let info: NotifyInfo = {
|
|
||||||
name: post_view.community.name,
|
|
||||||
icon: post_view.community.icon,
|
|
||||||
link: `/post/${post_view.post.id}`,
|
|
||||||
body: post_view.post.name,
|
|
||||||
};
|
|
||||||
notify(info, router);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifyComment(comment_view: CommentView, router: any) {
|
|
||||||
let info: NotifyInfo = {
|
|
||||||
name: comment_view.creator.name,
|
|
||||||
icon: comment_view.creator.avatar,
|
|
||||||
link: `/comment/${comment_view.comment.id}`,
|
|
||||||
body: comment_view.comment.content,
|
|
||||||
};
|
|
||||||
notify(info, router);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
|
|
||||||
let info: NotifyInfo = {
|
|
||||||
name: pmv.creator.name,
|
|
||||||
icon: pmv.creator.avatar,
|
|
||||||
link: `/inbox`,
|
|
||||||
body: pmv.private_message.content,
|
|
||||||
};
|
|
||||||
notify(info, router);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notify(info: NotifyInfo, router: any) {
|
|
||||||
messageToastify(info, router);
|
|
||||||
|
|
||||||
if (Notification.permission !== "granted") Notification.requestPermission();
|
|
||||||
else {
|
|
||||||
var notification = new Notification(info.name, {
|
|
||||||
...{ body: info.body },
|
|
||||||
...(info.icon && { icon: info.icon }),
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.onclick = (ev: Event): any => {
|
|
||||||
ev.preventDefault();
|
|
||||||
router.history.push(info.link);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupTribute() {
|
export function setupTribute() {
|
||||||
return new Tribute({
|
return new Tribute({
|
||||||
noMatchTemplate: function () {
|
noMatchTemplate: function () {
|
||||||
|
@ -649,11 +567,11 @@ export function setupTribute() {
|
||||||
{
|
{
|
||||||
trigger: ":",
|
trigger: ":",
|
||||||
menuItemTemplate: (item: any) => {
|
menuItemTemplate: (item: any) => {
|
||||||
let shortName = `:${item.original.key}:`;
|
const shortName = `:${item.original.key}:`;
|
||||||
return `${item.original.val} ${shortName}`;
|
return `${item.original.val} ${shortName}`;
|
||||||
},
|
},
|
||||||
selectTemplate: (item: any) => {
|
selectTemplate: (item: any) => {
|
||||||
let customEmoji = customEmojisLookup.get(
|
const customEmoji = customEmojisLookup.get(
|
||||||
item.original.key
|
item.original.key
|
||||||
)?.custom_emoji;
|
)?.custom_emoji;
|
||||||
if (customEmoji == undefined) return `${item.original.val}`;
|
if (customEmoji == undefined) return `${item.original.val}`;
|
||||||
|
@ -680,7 +598,7 @@ export function setupTribute() {
|
||||||
{
|
{
|
||||||
trigger: "@",
|
trigger: "@",
|
||||||
selectTemplate: (item: any) => {
|
selectTemplate: (item: any) => {
|
||||||
let it: PersonTribute = item.original;
|
const it: PersonTribute = item.original;
|
||||||
return `[${it.key}](${it.view.person.actor_id})`;
|
return `[${it.key}](${it.view.person.actor_id})`;
|
||||||
},
|
},
|
||||||
values: debounce(async (text: string, cb: any) => {
|
values: debounce(async (text: string, cb: any) => {
|
||||||
|
@ -697,7 +615,7 @@ export function setupTribute() {
|
||||||
{
|
{
|
||||||
trigger: "!",
|
trigger: "!",
|
||||||
selectTemplate: (item: any) => {
|
selectTemplate: (item: any) => {
|
||||||
let it: CommunityTribute = item.original;
|
const it: CommunityTribute = item.original;
|
||||||
return `[${it.key}](${it.view.community.actor_id})`;
|
return `[${it.key}](${it.view.community.actor_id})`;
|
||||||
},
|
},
|
||||||
values: debounce(async (text: string, cb: any) => {
|
values: debounce(async (text: string, cb: any) => {
|
||||||
|
@ -714,7 +632,10 @@ export function setupTribute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
|
function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
|
||||||
let groupedEmojis = groupBy(custom_emoji_views, x => x.custom_emoji.category);
|
const groupedEmojis = groupBy(
|
||||||
|
custom_emoji_views,
|
||||||
|
x => x.custom_emoji.category
|
||||||
|
);
|
||||||
for (const [category, emojis] of Object.entries(groupedEmojis)) {
|
for (const [category, emojis] of Object.entries(groupedEmojis)) {
|
||||||
customEmojis.push({
|
customEmojis.push({
|
||||||
id: category,
|
id: category,
|
||||||
|
@ -739,7 +660,7 @@ export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
|
||||||
keywords: custom_emoji_view.keywords.map(x => x.keyword),
|
keywords: custom_emoji_view.keywords.map(x => x.keyword),
|
||||||
skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
|
skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
|
||||||
};
|
};
|
||||||
let categoryIndex = customEmojis.findIndex(
|
const categoryIndex = customEmojis.findIndex(
|
||||||
x => x.id == custom_emoji_view.custom_emoji.category
|
x => x.id == custom_emoji_view.custom_emoji.category
|
||||||
);
|
);
|
||||||
if (categoryIndex == -1) {
|
if (categoryIndex == -1) {
|
||||||
|
@ -749,7 +670,7 @@ export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
|
||||||
emojis: [emoji],
|
emojis: [emoji],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
|
const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
|
||||||
x => x.id == custom_emoji_view.custom_emoji.shortcode
|
x => x.id == custom_emoji_view.custom_emoji.shortcode
|
||||||
);
|
);
|
||||||
if (emojiIndex == -1) {
|
if (emojiIndex == -1) {
|
||||||
|
@ -766,7 +687,7 @@ export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
|
||||||
|
|
||||||
export function removeFromEmojiDataModel(id: number) {
|
export function removeFromEmojiDataModel(id: number) {
|
||||||
let view: CustomEmojiView | undefined;
|
let view: CustomEmojiView | undefined;
|
||||||
for (let item of customEmojisLookup.values()) {
|
for (const item of customEmojisLookup.values()) {
|
||||||
if (item.custom_emoji.id === id) {
|
if (item.custom_emoji.id === id) {
|
||||||
view = item;
|
view = item;
|
||||||
break;
|
break;
|
||||||
|
@ -872,15 +793,12 @@ interface PersonTribute {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function personSearch(text: string): Promise<PersonTribute[]> {
|
async function personSearch(text: string): Promise<PersonTribute[]> {
|
||||||
let users = (await fetchUsers(text)).users;
|
const usersResponse = await fetchUsers(text);
|
||||||
let persons: PersonTribute[] = users.map(pv => {
|
|
||||||
let tribute: PersonTribute = {
|
return usersResponse.map(pv => ({
|
||||||
key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
|
key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
|
||||||
view: pv,
|
view: pv,
|
||||||
};
|
}));
|
||||||
return tribute;
|
|
||||||
});
|
|
||||||
return persons;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunityTribute {
|
interface CommunityTribute {
|
||||||
|
@ -889,15 +807,12 @@ interface CommunityTribute {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function communitySearch(text: string): Promise<CommunityTribute[]> {
|
async function communitySearch(text: string): Promise<CommunityTribute[]> {
|
||||||
let comms = (await fetchCommunities(text)).communities;
|
const communitiesResponse = await fetchCommunities(text);
|
||||||
let communities: CommunityTribute[] = comms.map(cv => {
|
|
||||||
let tribute: CommunityTribute = {
|
return communitiesResponse.map(cv => ({
|
||||||
key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
|
key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
|
||||||
view: cv,
|
view: cv,
|
||||||
};
|
}));
|
||||||
return tribute;
|
|
||||||
});
|
|
||||||
return communities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecipientIdFromProps(props: any): number {
|
export function getRecipientIdFromProps(props: any): number {
|
||||||
|
@ -907,51 +822,137 @@ export function getRecipientIdFromProps(props: any): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIdFromProps(props: any): number | undefined {
|
export function getIdFromProps(props: any): number | undefined {
|
||||||
let id = props.match.params.post_id;
|
const id = props.match.params.post_id;
|
||||||
return id ? Number(id) : undefined;
|
return id ? Number(id) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommentIdFromProps(props: any): number | undefined {
|
export function getCommentIdFromProps(props: any): number | undefined {
|
||||||
let id = props.match.params.comment_id;
|
const id = props.match.params.comment_id;
|
||||||
return id ? Number(id) : undefined;
|
return id ? Number(id) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editCommentRes(data: CommentView, comments?: CommentView[]) {
|
type ImmutableListKey =
|
||||||
let found = comments?.find(c => c.comment.id == data.comment.id);
|
| "comment"
|
||||||
if (found) {
|
| "comment_reply"
|
||||||
found.comment.content = data.comment.content;
|
| "person_mention"
|
||||||
found.comment.distinguished = data.comment.distinguished;
|
| "community"
|
||||||
found.comment.updated = data.comment.updated;
|
| "private_message"
|
||||||
found.comment.removed = data.comment.removed;
|
| "post"
|
||||||
found.comment.deleted = data.comment.deleted;
|
| "post_report"
|
||||||
found.counts.upvotes = data.counts.upvotes;
|
| "comment_report"
|
||||||
found.counts.downvotes = data.counts.downvotes;
|
| "private_message_report"
|
||||||
found.counts.score = data.counts.score;
|
| "registration_application";
|
||||||
}
|
|
||||||
|
function editListImmutable<
|
||||||
|
T extends { [key in F]: { id: number } },
|
||||||
|
F extends ImmutableListKey
|
||||||
|
>(fieldName: F, data: T, list: T[]): T[] {
|
||||||
|
return [
|
||||||
|
...list.map(c => (c[fieldName].id === data[fieldName].id ? data : c)),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCommentRes(data: CommentView, comments?: CommentView[]) {
|
export function editComment(
|
||||||
let found = comments?.find(c => c.comment.id == data.comment.id);
|
data: CommentView,
|
||||||
if (found) {
|
comments: CommentView[]
|
||||||
found.saved = data.saved;
|
): CommentView[] {
|
||||||
}
|
return editListImmutable("comment", data, comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editCommentReply(
|
||||||
|
data: CommentReplyView,
|
||||||
|
replies: CommentReplyView[]
|
||||||
|
): CommentReplyView[] {
|
||||||
|
return editListImmutable("comment_reply", data, replies);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WithComment {
|
||||||
|
comment: CommentI;
|
||||||
|
counts: CommentAggregates;
|
||||||
|
my_vote?: number;
|
||||||
|
saved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editMention(
|
||||||
|
data: PersonMentionView,
|
||||||
|
comments: PersonMentionView[]
|
||||||
|
): PersonMentionView[] {
|
||||||
|
return editListImmutable("person_mention", data, comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editCommunity(
|
||||||
|
data: CommunityView,
|
||||||
|
communities: CommunityView[]
|
||||||
|
): CommunityView[] {
|
||||||
|
return editListImmutable("community", data, communities);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPrivateMessage(
|
||||||
|
data: PrivateMessageView,
|
||||||
|
messages: PrivateMessageView[]
|
||||||
|
): PrivateMessageView[] {
|
||||||
|
return editListImmutable("private_message", data, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPost(data: PostView, posts: PostView[]): PostView[] {
|
||||||
|
return editListImmutable("post", data, posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPostReport(
|
||||||
|
data: PostReportView,
|
||||||
|
reports: PostReportView[]
|
||||||
|
) {
|
||||||
|
return editListImmutable("post_report", data, reports);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editCommentReport(
|
||||||
|
data: CommentReportView,
|
||||||
|
reports: CommentReportView[]
|
||||||
|
): CommentReportView[] {
|
||||||
|
return editListImmutable("comment_report", data, reports);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPrivateMessageReport(
|
||||||
|
data: PrivateMessageReportView,
|
||||||
|
reports: PrivateMessageReportView[]
|
||||||
|
): PrivateMessageReportView[] {
|
||||||
|
return editListImmutable("private_message_report", data, reports);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editRegistrationApplication(
|
||||||
|
data: RegistrationApplicationView,
|
||||||
|
apps: RegistrationApplicationView[]
|
||||||
|
): RegistrationApplicationView[] {
|
||||||
|
return editListImmutable("registration_application", data, apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editWith<D extends WithComment, L extends WithComment>(
|
||||||
|
{ comment, counts, saved, my_vote }: D,
|
||||||
|
list: L[]
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
...list.map(c =>
|
||||||
|
c.comment.id === comment.id
|
||||||
|
? { ...c, comment, counts, saved, my_vote }
|
||||||
|
: c
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePersonBlock(
|
export function updatePersonBlock(
|
||||||
data: BlockPersonResponse,
|
data: BlockPersonResponse,
|
||||||
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
|
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
|
||||||
) {
|
) {
|
||||||
let mui = myUserInfo;
|
if (myUserInfo) {
|
||||||
if (mui) {
|
|
||||||
if (data.blocked) {
|
if (data.blocked) {
|
||||||
mui.person_blocks.push({
|
myUserInfo.person_blocks.push({
|
||||||
person: mui.local_user_view.person,
|
person: myUserInfo.local_user_view.person,
|
||||||
target: data.person_view.person,
|
target: data.person_view.person,
|
||||||
});
|
});
|
||||||
toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
|
toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
|
||||||
} else {
|
} else {
|
||||||
mui.person_blocks = mui.person_blocks.filter(
|
myUserInfo.person_blocks = myUserInfo.person_blocks.filter(
|
||||||
i => i.target.id != data.person_view.person.id
|
i => i.target.id !== data.person_view.person.id
|
||||||
);
|
);
|
||||||
toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
|
toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
|
||||||
}
|
}
|
||||||
|
@ -962,128 +963,25 @@ export function updateCommunityBlock(
|
||||||
data: BlockCommunityResponse,
|
data: BlockCommunityResponse,
|
||||||
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
|
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
|
||||||
) {
|
) {
|
||||||
let mui = myUserInfo;
|
if (myUserInfo) {
|
||||||
if (mui) {
|
|
||||||
if (data.blocked) {
|
if (data.blocked) {
|
||||||
mui.community_blocks.push({
|
myUserInfo.community_blocks.push({
|
||||||
person: mui.local_user_view.person,
|
person: myUserInfo.local_user_view.person,
|
||||||
community: data.community_view.community,
|
community: data.community_view.community,
|
||||||
});
|
});
|
||||||
toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
|
toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
|
||||||
} else {
|
} else {
|
||||||
mui.community_blocks = mui.community_blocks.filter(
|
myUserInfo.community_blocks = myUserInfo.community_blocks.filter(
|
||||||
i => i.community.id != data.community_view.community.id
|
i => i.community.id !== data.community_view.community.id
|
||||||
);
|
);
|
||||||
toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
|
toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCommentLikeRes(
|
|
||||||
data: CommentView,
|
|
||||||
comments?: CommentView[]
|
|
||||||
) {
|
|
||||||
let found = comments?.find(c => c.comment.id === data.comment.id);
|
|
||||||
if (found) {
|
|
||||||
found.counts.score = data.counts.score;
|
|
||||||
found.counts.upvotes = data.counts.upvotes;
|
|
||||||
found.counts.downvotes = data.counts.downvotes;
|
|
||||||
if (data.my_vote !== null) {
|
|
||||||
found.my_vote = data.my_vote;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPostLikeFindRes(data: PostView, posts?: PostView[]) {
|
|
||||||
let found = posts?.find(p => p.post.id == data.post.id);
|
|
||||||
if (found) {
|
|
||||||
createPostLikeRes(data, found);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPostLikeRes(data: PostView, post_view?: PostView) {
|
|
||||||
if (post_view) {
|
|
||||||
post_view.counts.score = data.counts.score;
|
|
||||||
post_view.counts.upvotes = data.counts.upvotes;
|
|
||||||
post_view.counts.downvotes = data.counts.downvotes;
|
|
||||||
if (data.my_vote !== null) {
|
|
||||||
post_view.my_vote = data.my_vote;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function editPostFindRes(data: PostView, posts?: PostView[]) {
|
|
||||||
let found = posts?.find(p => p.post.id == data.post.id);
|
|
||||||
if (found) {
|
|
||||||
editPostRes(data, found);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function editPostRes(data: PostView, post: PostView) {
|
|
||||||
if (post) {
|
|
||||||
post.post.url = data.post.url;
|
|
||||||
post.post.name = data.post.name;
|
|
||||||
post.post.nsfw = data.post.nsfw;
|
|
||||||
post.post.deleted = data.post.deleted;
|
|
||||||
post.post.removed = data.post.removed;
|
|
||||||
post.post.featured_community = data.post.featured_community;
|
|
||||||
post.post.featured_local = data.post.featured_local;
|
|
||||||
post.post.body = data.post.body;
|
|
||||||
post.post.locked = data.post.locked;
|
|
||||||
post.saved = data.saved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO possible to make these generic?
|
|
||||||
export function updatePostReportRes(
|
|
||||||
data: PostReportView,
|
|
||||||
reports?: PostReportView[]
|
|
||||||
) {
|
|
||||||
let found = reports?.find(p => p.post_report.id == data.post_report.id);
|
|
||||||
if (found) {
|
|
||||||
found.post_report = data.post_report;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCommentReportRes(
|
|
||||||
data: CommentReportView,
|
|
||||||
reports?: CommentReportView[]
|
|
||||||
) {
|
|
||||||
let found = reports?.find(c => c.comment_report.id == data.comment_report.id);
|
|
||||||
if (found) {
|
|
||||||
found.comment_report = data.comment_report;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePrivateMessageReportRes(
|
|
||||||
data: PrivateMessageReportView,
|
|
||||||
reports?: PrivateMessageReportView[]
|
|
||||||
) {
|
|
||||||
let found = reports?.find(
|
|
||||||
c => c.private_message_report.id == data.private_message_report.id
|
|
||||||
);
|
|
||||||
if (found) {
|
|
||||||
found.private_message_report = data.private_message_report;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateRegistrationApplicationRes(
|
|
||||||
data: RegistrationApplicationView,
|
|
||||||
applications?: RegistrationApplicationView[]
|
|
||||||
) {
|
|
||||||
let found = applications?.find(
|
|
||||||
ra => ra.registration_application.id == data.registration_application.id
|
|
||||||
);
|
|
||||||
if (found) {
|
|
||||||
found.registration_application = data.registration_application;
|
|
||||||
found.admin = data.admin;
|
|
||||||
found.creator_local_user = data.creator_local_user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
|
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
|
||||||
let nodes: CommentNodeI[] = [];
|
const nodes: CommentNodeI[] = [];
|
||||||
for (let comment of comments) {
|
for (const comment of comments) {
|
||||||
nodes.push({ comment_view: comment, children: [], depth: 0 });
|
nodes.push({ comment_view: comment, children: [], depth: 0 });
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
|
@ -1111,15 +1009,15 @@ export function buildCommentsTree(
|
||||||
comments: CommentView[],
|
comments: CommentView[],
|
||||||
parentComment: boolean
|
parentComment: boolean
|
||||||
): CommentNodeI[] {
|
): CommentNodeI[] {
|
||||||
let map = new Map<number, CommentNodeI>();
|
const map = new Map<number, CommentNodeI>();
|
||||||
let depthOffset = !parentComment
|
const depthOffset = !parentComment
|
||||||
? 0
|
? 0
|
||||||
: getDepthFromComment(comments[0].comment) ?? 0;
|
: getDepthFromComment(comments[0].comment) ?? 0;
|
||||||
|
|
||||||
for (let comment_view of comments) {
|
for (const comment_view of comments) {
|
||||||
let depthI = getDepthFromComment(comment_view.comment) ?? 0;
|
const depthI = getDepthFromComment(comment_view.comment) ?? 0;
|
||||||
let depth = depthI ? depthI - depthOffset : 0;
|
const depth = depthI ? depthI - depthOffset : 0;
|
||||||
let node: CommentNodeI = {
|
const node: CommentNodeI = {
|
||||||
comment_view,
|
comment_view,
|
||||||
children: [],
|
children: [],
|
||||||
depth,
|
depth,
|
||||||
|
@ -1127,22 +1025,22 @@ export function buildCommentsTree(
|
||||||
map.set(comment_view.comment.id, { ...node });
|
map.set(comment_view.comment.id, { ...node });
|
||||||
}
|
}
|
||||||
|
|
||||||
let tree: CommentNodeI[] = [];
|
const tree: CommentNodeI[] = [];
|
||||||
|
|
||||||
// if its a parent comment fetch, then push the first comment to the top node.
|
// if its a parent comment fetch, then push the first comment to the top node.
|
||||||
if (parentComment) {
|
if (parentComment) {
|
||||||
let cNode = map.get(comments[0].comment.id);
|
const cNode = map.get(comments[0].comment.id);
|
||||||
if (cNode) {
|
if (cNode) {
|
||||||
tree.push(cNode);
|
tree.push(cNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let comment_view of comments) {
|
for (const comment_view of comments) {
|
||||||
let child = map.get(comment_view.comment.id);
|
const child = map.get(comment_view.comment.id);
|
||||||
if (child) {
|
if (child) {
|
||||||
let parent_id = getCommentParentId(comment_view.comment);
|
const parent_id = getCommentParentId(comment_view.comment);
|
||||||
if (parent_id) {
|
if (parent_id) {
|
||||||
let parent = map.get(parent_id);
|
const parent = map.get(parent_id);
|
||||||
// Necessary because blocked comment might not exist
|
// Necessary because blocked comment might not exist
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.children.push(child);
|
parent.children.push(child);
|
||||||
|
@ -1159,7 +1057,7 @@ export function buildCommentsTree(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommentParentId(comment?: CommentI): number | undefined {
|
export function getCommentParentId(comment?: CommentI): number | undefined {
|
||||||
let split = comment?.path.split(".");
|
const split = comment?.path.split(".");
|
||||||
// remove the 0
|
// remove the 0
|
||||||
split?.shift();
|
split?.shift();
|
||||||
|
|
||||||
|
@ -1169,25 +1067,26 @@ export function getCommentParentId(comment?: CommentI): number | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDepthFromComment(comment?: CommentI): number | undefined {
|
export function getDepthFromComment(comment?: CommentI): number | undefined {
|
||||||
let len = comment?.path.split(".").length;
|
const len = comment?.path.split(".").length;
|
||||||
return len ? len - 2 : undefined;
|
return len ? len - 2 : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO make immutable
|
||||||
export function insertCommentIntoTree(
|
export function insertCommentIntoTree(
|
||||||
tree: CommentNodeI[],
|
tree: CommentNodeI[],
|
||||||
cv: CommentView,
|
cv: CommentView,
|
||||||
parentComment: boolean
|
parentComment: boolean
|
||||||
) {
|
) {
|
||||||
// Building a fake node to be used for later
|
// Building a fake node to be used for later
|
||||||
let node: CommentNodeI = {
|
const node: CommentNodeI = {
|
||||||
comment_view: cv,
|
comment_view: cv,
|
||||||
children: [],
|
children: [],
|
||||||
depth: 0,
|
depth: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let parentId = getCommentParentId(cv.comment);
|
const parentId = getCommentParentId(cv.comment);
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
let parent_comment = searchCommentTree(tree, parentId);
|
const parent_comment = searchCommentTree(tree, parentId);
|
||||||
if (parent_comment) {
|
if (parent_comment) {
|
||||||
node.depth = parent_comment.depth + 1;
|
node.depth = parent_comment.depth + 1;
|
||||||
parent_comment.children.unshift(node);
|
parent_comment.children.unshift(node);
|
||||||
|
@ -1201,13 +1100,13 @@ export function searchCommentTree(
|
||||||
tree: CommentNodeI[],
|
tree: CommentNodeI[],
|
||||||
id: number
|
id: number
|
||||||
): CommentNodeI | undefined {
|
): CommentNodeI | undefined {
|
||||||
for (let node of tree) {
|
for (const node of tree) {
|
||||||
if (node.comment_view.comment.id === id) {
|
if (node.comment_view.comment.id === id) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
let res = searchCommentTree([child], id);
|
const res = searchCommentTree([child], id);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
return res;
|
return res;
|
||||||
|
@ -1232,7 +1131,7 @@ function hsl(num: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hostname(url: string): string {
|
export function hostname(url: string): string {
|
||||||
let cUrl = new URL(url);
|
const cUrl = new URL(url);
|
||||||
return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
|
return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1269,20 +1168,6 @@ export function setIsoData(context: any): IsoData {
|
||||||
} else return context.router.staticContext;
|
} else return context.router.staticContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wsSubscribe(parseMessage: any): Subscription | undefined {
|
|
||||||
if (isBrowser()) {
|
|
||||||
return WebSocketService.Instance.subject
|
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
|
||||||
.subscribe(
|
|
||||||
msg => parseMessage(msg),
|
|
||||||
err => console.error(err),
|
|
||||||
() => console.log("complete")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moment.updateLocale("en", {
|
moment.updateLocale("en", {
|
||||||
relativeTime: {
|
relativeTime: {
|
||||||
future: "in %s",
|
future: "in %s",
|
||||||
|
@ -1305,14 +1190,14 @@ moment.updateLocale("en", {
|
||||||
});
|
});
|
||||||
|
|
||||||
export function saveScrollPosition(context: any) {
|
export function saveScrollPosition(context: any) {
|
||||||
let path: string = context.router.route.location.pathname;
|
const path: string = context.router.route.location.pathname;
|
||||||
let y = window.scrollY;
|
const y = window.scrollY;
|
||||||
sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
|
sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreScrollPosition(context: any) {
|
export function restoreScrollPosition(context: any) {
|
||||||
let path: string = context.router.route.location.pathname;
|
const path: string = context.router.route.location.pathname;
|
||||||
let y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
|
const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
|
||||||
window.scrollTo(0, y);
|
window.scrollTo(0, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1346,32 +1231,30 @@ export function personToChoice(pvs: PersonView): Choice {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCommunities(q: string) {
|
function fetchSearchResults(q: string, type_: SearchType) {
|
||||||
let form: Search = {
|
const form: Search = {
|
||||||
q,
|
q,
|
||||||
type_: "Communities",
|
type_,
|
||||||
sort: "TopAll",
|
sort: "TopAll",
|
||||||
listing_type: "All",
|
listing_type: "All",
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth: myAuth(false),
|
auth: myAuth(),
|
||||||
};
|
};
|
||||||
let client = new LemmyHttp(getHttpBase());
|
|
||||||
return client.search(form);
|
return HttpService.client.search(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCommunities(q: string) {
|
||||||
|
const res = await fetchSearchResults(q, "Communities");
|
||||||
|
|
||||||
|
return res.state === "success" ? res.data.communities : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUsers(q: string) {
|
export async function fetchUsers(q: string) {
|
||||||
let form: Search = {
|
const res = await fetchSearchResults(q, "Users");
|
||||||
q,
|
|
||||||
type_: "Users",
|
return res.state === "success" ? res.data.users : [];
|
||||||
sort: "TopAll",
|
|
||||||
listing_type: "All",
|
|
||||||
page: 1,
|
|
||||||
limit: fetchLimit,
|
|
||||||
auth: myAuth(false),
|
|
||||||
};
|
|
||||||
let client = new LemmyHttp(getHttpBase());
|
|
||||||
return client.search(form);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function communitySelectName(cv: CommunityView): string {
|
export function communitySelectName(cv: CommunityView): string {
|
||||||
|
@ -1391,7 +1274,7 @@ export function initializeSite(site?: GetSiteResponse) {
|
||||||
UserService.Instance.myUserInfo = site?.my_user;
|
UserService.Instance.myUserInfo = site?.my_user;
|
||||||
i18n.changeLanguage(getLanguages()[0]);
|
i18n.changeLanguage(getLanguages()[0]);
|
||||||
if (site) {
|
if (site) {
|
||||||
setupEmojiDataModel(site.custom_emojis);
|
setupEmojiDataModel(site.custom_emojis ?? []);
|
||||||
}
|
}
|
||||||
setupMarkdown();
|
setupMarkdown();
|
||||||
}
|
}
|
||||||
|
@ -1408,7 +1291,7 @@ export function numToSI(value: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBanned(ps: Person): boolean {
|
export function isBanned(ps: Person): boolean {
|
||||||
let expires = ps.ban_expires;
|
const expires = ps.ban_expires;
|
||||||
// Add Z to convert from UTC date
|
// Add Z to convert from UTC date
|
||||||
// TODO this check probably isn't necessary anymore
|
// TODO this check probably isn't necessary anymore
|
||||||
if (expires) {
|
if (expires) {
|
||||||
|
@ -1422,8 +1305,12 @@ export function isBanned(ps: Person): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function myAuth(throwErr = true): string | undefined {
|
export function myAuth(): string | undefined {
|
||||||
return UserService.Instance.auth(throwErr);
|
return UserService.Instance.auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function myAuthRequired(): string {
|
||||||
|
return UserService.Instance.auth(true) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableDownvotes(siteRes: GetSiteResponse): boolean {
|
export function enableDownvotes(siteRes: GetSiteResponse): boolean {
|
||||||
|
@ -1478,8 +1365,8 @@ export function nsfwCheck(
|
||||||
pv: PostView,
|
pv: PostView,
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): boolean {
|
): boolean {
|
||||||
let nsfw = pv.post.nsfw || pv.community.nsfw;
|
const nsfw = pv.post.nsfw || pv.community.nsfw;
|
||||||
let myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
|
const myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
|
||||||
return !nsfw || (nsfw && myShowNsfw);
|
return !nsfw || (nsfw && myShowNsfw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1503,10 +1390,10 @@ export function selectableLanguages(
|
||||||
showSite?: boolean,
|
showSite?: boolean,
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): Language[] {
|
): Language[] {
|
||||||
let allLangIds = allLanguages.map(l => l.id);
|
const allLangIds = allLanguages.map(l => l.id);
|
||||||
let myLangs = myUserInfo?.discussion_languages ?? allLangIds;
|
let myLangs = myUserInfo?.discussion_languages ?? allLangIds;
|
||||||
myLangs = myLangs.length == 0 ? allLangIds : myLangs;
|
myLangs = myLangs.length == 0 ? allLangIds : myLangs;
|
||||||
let siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
|
const siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
|
||||||
|
|
||||||
if (showAll) {
|
if (showAll) {
|
||||||
return allLanguages;
|
return allLanguages;
|
||||||
|
@ -1521,12 +1408,6 @@ export function selectableLanguages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadImage(image: File): Promise<UploadImageResponse> {
|
|
||||||
const client = new LemmyHttp(getHttpBase());
|
|
||||||
|
|
||||||
return client.uploadImage({ image });
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmojiMartCategory {
|
interface EmojiMartCategory {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1587,7 +1468,7 @@ export function getQueryString<T extends Record<string, string | undefined>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthPath(pathname: string) {
|
export function isAuthPath(pathname: string) {
|
||||||
return /create_.*|inbox|settings|setup|admin|reports|registration_applications/g.test(
|
return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
|
||||||
pathname
|
pathname
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1601,3 +1482,11 @@ export function share(shareData: ShareData) {
|
||||||
navigator.share(shareData);
|
navigator.share(shareData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newVote(voteType: VoteType, myVote?: number): number {
|
||||||
|
if (voteType == VoteType.Upvote) {
|
||||||
|
return myVote == 1 ? 0 : 1;
|
||||||
|
} else {
|
||||||
|
return myVote == -1 ? 0 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -69,10 +69,10 @@ const createServerConfig = (_env, mode) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mode === "development") {
|
if (mode === "development") {
|
||||||
config.cache = {
|
// config.cache = {
|
||||||
type: "filesystem",
|
// type: "filesystem",
|
||||||
name: "server",
|
// name: "server",
|
||||||
};
|
// };
|
||||||
|
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new RunNodeWebpackPlugin({
|
new RunNodeWebpackPlugin({
|
||||||
|
@ -94,7 +94,7 @@ const createClientConfig = (_env, mode) => {
|
||||||
plugins: [
|
plugins: [
|
||||||
...base.plugins,
|
...base.plugins,
|
||||||
new ServiceWorkerPlugin({
|
new ServiceWorkerPlugin({
|
||||||
enableInDevelopment: true,
|
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
|
||||||
workbox: {
|
workbox: {
|
||||||
modifyURLPrefix: {
|
modifyURLPrefix: {
|
||||||
"/": "/static/",
|
"/": "/static/",
|
||||||
|
@ -149,10 +149,10 @@ const createClientConfig = (_env, mode) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mode === "development") {
|
if (mode === "development") {
|
||||||
config.cache = {
|
// config.cache = {
|
||||||
type: "filesystem",
|
// type: "filesystem",
|
||||||
name: "client",
|
// name: "client",
|
||||||
};
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|
Loading…
Reference in a new issue