mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-27 03:41:02 +00:00
Merge branch 'main' into inbox-refactoring-merge
This commit is contained in:
commit
e605d58888
97 changed files with 7530 additions and 3488 deletions
59
.travis.yml
vendored
59
.travis.yml
vendored
|
@ -1,35 +1,28 @@
|
||||||
language: rust
|
sudo: required
|
||||||
rust:
|
language: node_js
|
||||||
- stable
|
node_js:
|
||||||
matrix:
|
- 14
|
||||||
allow_failures:
|
services:
|
||||||
- rust: nightly
|
- docker
|
||||||
fast_finish: true
|
|
||||||
cache: cargo
|
|
||||||
before_cache:
|
|
||||||
- rm -rfv target/debug/incremental/lemmy_server-*
|
|
||||||
- rm -rfv target/debug/.fingerprint/lemmy_server-*
|
|
||||||
- rm -rfv target/debug/build/lemmy_server-*
|
|
||||||
- rm -rfv target/debug/deps/lemmy_server-*
|
|
||||||
- rm -rfv target/debug/lemmy_server.d
|
|
||||||
before_script:
|
|
||||||
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
|
||||||
- psql -c 'create database lemmy with owner lemmy;' -U postgres
|
|
||||||
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
|
|
||||||
before_install:
|
|
||||||
- cd server
|
|
||||||
script:
|
|
||||||
# Default checks, but fail if anything is detected
|
|
||||||
- cargo build
|
|
||||||
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
|
||||||
- cargo install diesel_cli --no-default-features --features postgres --force
|
|
||||||
- diesel migration run
|
|
||||||
- cargo test --workspace
|
|
||||||
env:
|
env:
|
||||||
|
matrix:
|
||||||
|
- DOCKER_COMPOSE_VERSION=1.25.5
|
||||||
global:
|
global:
|
||||||
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
- secure: nzmFoTxPn7OT+qcTULezSCT6B44j/q8RxERBQSr1FVXaCcDrBr6q9ewhGy7BHWP74r4qbif4m9r3sNELZCoFYFP3JwLnrZfX/xUwU8p61eFD2PMOJAdOywDxb94SvooOSnjBmxNvRsuqf6Zmnw378mbsSVCi9Xbx9jpoV4Jq8zKgO0M8WIl/lj2dijD95WIMrHcorbzKS3+2zW3LkPiC2bnfDAUmUDfaCj1gh9FCvzZMtrSxu7kxAeFCkR16TJUciIcGgag8rLHfxwG0h2uEJJ+3/62qCWUdgnj171oTE4ZRi0hdvt2HOY5wjHfS2y1ZxWYgo31uws3pyoTNeQZi0o7Q9Xe/4JXYZXvDfuscSZ9RiuhAstCVswtXPJJVVJQ9cdl5eX1TI0bz8eVRvRy4p40OIBjKiobkmRjl8sXjFbpYAIvFr+TgSa/K/bxm3POfI0B8bIHI85zFxUMrWt5i2IJ0dWvDNHrz+CWWKn1vVFYbBNPgDDHtE0P3LWLEioWFf+ULycjW8DefWc+b63Lf9SSaEE7FnX2mc+BaHCgubCDkJy9Au4xP8zQlJjgZwOdTedw5jvmwz3fqMZBpHypVUXzZs7cRhMWtQ7TAoGb8TOqXNgPEVW+BARNXl0wAamTgjt9v20x0wkp+/SLJwMNY+zvwmzxzd5R9TPgDOqyIRTU=
|
||||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
- secure: ALZqC4OYV315P7EZyk+c/PLJdneeU7jMC30TTzMcX3hospIu7naWekZ+HUnziFDQKZxIHWKZsq1R52DWhsERLrPF3SVa+QiXu8vTTPrETBWnu9VgyFzgdEbUKRas1X3qerEAHcNBms1EAl2FOiQM1k5EDygrClv4KWgyzntEtKJbN2UCFKxtoBSdMZA6fcGtCwffcj8uIAIP2NhZixbU+smVgVbpMpe6QEuuEoVlVrfH8iXxb8Gi+qkd0YIYAHkjtTqQ/nHuAUhcuEE0mORTNGPv7CmTwpuQiGCCdtySZc7Qq8z1x2y7RLy0+RVxM0PR8UV6iy4ipyTgZ6wTF30ksLDxOI3GlRaKF3F6kLErOiEiEUOqa+zLgUM0OLGTn+KLATQDx74in5NcKjKUAnkuxdZyuDbifvQb5tqfrGdXd22pzVZbielRJRW59ig0Nr5cxEpRtoRkoFKNk7o3XlD6JmIBjKn1UHkZ4H/oLUKIXT2qOP2fIEzgLjfpSuGwhvJRz1KRP49HYVl7Gkd45/RdZ519W0gnMkIrEaod90iXSFNTgmJTGeH0Mv0jHameN47PIT3c49MOy5Hj0XCHUPfc6qqrdGnliS5hTnrFThCfn5ZuSZxVdgGLJUQvV+D+5KDqjFdGyNGVGoEg0YdrDtGXmpojbyQDJAT7ToL3yIBF7co=
|
||||||
- RUST_TEST_THREADS=1
|
before_install:
|
||||||
|
# Install docker-compose
|
||||||
addons:
|
- sudo rm /usr/local/bin/docker-compose
|
||||||
postgresql: "9.4"
|
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname
|
||||||
|
-s`-`uname -m` > docker-compose
|
||||||
|
- chmod +x docker-compose
|
||||||
|
- sudo mv docker-compose /usr/local/bin
|
||||||
|
# Change dir
|
||||||
|
- cd docker/travis
|
||||||
|
script:
|
||||||
|
- "./run-tests.sh"
|
||||||
|
deploy:
|
||||||
|
provider: script
|
||||||
|
script: bash docker_push.sh
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
|
10
README.md
vendored
10
README.md
vendored
|
@ -104,6 +104,16 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
|
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
|
||||||
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
|
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
|
||||||
|
|
||||||
|
## Lemmy Projects
|
||||||
|
|
||||||
|
### Apps
|
||||||
|
|
||||||
|
- [Lemmy-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
- [Kotlin API ( under development )](https://github.com/eiknat/lemmy-client)
|
||||||
|
|
||||||
## Support / Donate
|
## Support / Donate
|
||||||
|
|
||||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
||||||
v0.7.26
|
v0.7.30
|
||||||
|
|
40
docker/prod/deploy.sh
vendored
40
docker/prod/deploy.sh
vendored
|
@ -24,35 +24,39 @@ cd docker/prod || exit
|
||||||
# Changing the docker-compose prod
|
# Changing the docker-compose prod
|
||||||
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
|
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
|
||||||
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../../ansible/templates/docker-compose.yml
|
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../../ansible/templates/docker-compose.yml
|
||||||
|
sed -i "s/dessalines\/lemmy:v.*/dessalines\/lemmy:$new_tag/" ../travis/docker_push.sh
|
||||||
git add ../prod/docker-compose.yml
|
git add ../prod/docker-compose.yml
|
||||||
git add ../../ansible/templates/docker-compose.yml
|
git add ../../ansible/templates/docker-compose.yml
|
||||||
|
git add ../travis/docker_push.sh
|
||||||
|
|
||||||
# The commit
|
# The commit
|
||||||
git commit -m"Version $new_tag"
|
git commit -m"Version $new_tag"
|
||||||
git tag $new_tag
|
git tag $new_tag
|
||||||
|
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
# Now doing the building on travis, but leave this in for when you need to do an arm build
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
|
|
||||||
# Rebuilding docker
|
# export COMPOSE_DOCKER_CLI_BUILD=1
|
||||||
if [ $third_semver -eq 0 ]; then
|
# export DOCKER_BUILDKIT=1
|
||||||
# TODO get linux/arm/v7 build working
|
|
||||||
# Build for Raspberry Pi / other archs too
|
# # Rebuilding docker
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 ../../ \
|
# if [ $third_semver -eq 0 ]; then
|
||||||
--file Dockerfile \
|
# # TODO get linux/arm/v7 build working
|
||||||
--tag dessalines/lemmy:$new_tag \
|
# # Build for Raspberry Pi / other archs too
|
||||||
--push
|
# docker buildx build --platform linux/amd64,linux/arm64 ../../ \
|
||||||
else
|
# --file Dockerfile \
|
||||||
docker buildx build --platform linux/amd64 ../../ \
|
# --tag dessalines/lemmy:$new_tag \
|
||||||
--file Dockerfile \
|
# --push
|
||||||
--tag dessalines/lemmy:$new_tag \
|
# else
|
||||||
--push
|
# docker buildx build --platform linux/amd64 ../../ \
|
||||||
fi
|
# --file Dockerfile \
|
||||||
|
# --tag dessalines/lemmy:$new_tag \
|
||||||
|
# --push
|
||||||
|
# fi
|
||||||
|
|
||||||
# Push
|
# Push
|
||||||
git push origin $new_tag
|
git push origin $new_tag
|
||||||
git push
|
git push
|
||||||
|
|
||||||
# Pushing to any ansible deploys
|
# Pushing to any ansible deploys
|
||||||
cd ../../../lemmy-ansible || exit
|
# cd ../../../lemmy-ansible || exit
|
||||||
ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
|
# ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
|
||||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.7.26
|
image: dessalines/lemmy:v0.7.30
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
113
docker/travis/docker-compose.yml
vendored
Normal file
113
docker/travis/docker-compose.yml
vendored
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.17-alpine
|
||||||
|
ports:
|
||||||
|
- "8540:8540"
|
||||||
|
- "8550:8550"
|
||||||
|
- "8560:8560"
|
||||||
|
volumes:
|
||||||
|
# Hack to make this work from both docker/federation/ and docker/federation-test/
|
||||||
|
- ../federation/nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
- lemmy-alpha
|
||||||
|
- pictrs
|
||||||
|
- lemmy-beta
|
||||||
|
- lemmy-gamma
|
||||||
|
- iframely
|
||||||
|
|
||||||
|
pictrs:
|
||||||
|
restart: always
|
||||||
|
image: asonix/pictrs:v0.1.13-r0
|
||||||
|
user: 991:991
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pictrs_alpha:/mnt
|
||||||
|
|
||||||
|
lemmy-alpha:
|
||||||
|
image: dessalines/lemmy:travis
|
||||||
|
environment:
|
||||||
|
- LEMMY_HOSTNAME=lemmy-alpha:8540
|
||||||
|
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
|
||||||
|
- LEMMY_JWT_SECRET=changeme
|
||||||
|
- LEMMY_FRONT_END_DIR=/app/dist
|
||||||
|
- LEMMY_FEDERATION__ENABLED=true
|
||||||
|
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||||
|
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma
|
||||||
|
- LEMMY_PORT=8540
|
||||||
|
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
|
||||||
|
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
||||||
|
- LEMMY_SETUP__SITE_NAME=lemmy-alpha
|
||||||
|
- RUST_BACKTRACE=1
|
||||||
|
- RUST_LOG=debug
|
||||||
|
depends_on:
|
||||||
|
- postgres_alpha
|
||||||
|
postgres_alpha:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres_alpha:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
lemmy-beta:
|
||||||
|
image: dessalines/lemmy:travis
|
||||||
|
environment:
|
||||||
|
- LEMMY_HOSTNAME=lemmy-beta:8550
|
||||||
|
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
|
||||||
|
- LEMMY_JWT_SECRET=changeme
|
||||||
|
- LEMMY_FRONT_END_DIR=/app/dist
|
||||||
|
- LEMMY_FEDERATION__ENABLED=true
|
||||||
|
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||||
|
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma
|
||||||
|
- LEMMY_PORT=8550
|
||||||
|
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
|
||||||
|
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
||||||
|
- LEMMY_SETUP__SITE_NAME=lemmy-beta
|
||||||
|
- RUST_BACKTRACE=1
|
||||||
|
- RUST_LOG=debug
|
||||||
|
depends_on:
|
||||||
|
- postgres_beta
|
||||||
|
postgres_beta:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres_beta:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
lemmy-gamma:
|
||||||
|
image: dessalines/lemmy:travis
|
||||||
|
environment:
|
||||||
|
- LEMMY_HOSTNAME=lemmy-gamma:8560
|
||||||
|
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
|
||||||
|
- LEMMY_JWT_SECRET=changeme
|
||||||
|
- LEMMY_FRONT_END_DIR=/app/dist
|
||||||
|
- LEMMY_FEDERATION__ENABLED=true
|
||||||
|
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||||
|
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta
|
||||||
|
- LEMMY_PORT=8560
|
||||||
|
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
|
||||||
|
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
||||||
|
- LEMMY_SETUP__SITE_NAME=lemmy-gamma
|
||||||
|
- RUST_BACKTRACE=1
|
||||||
|
- RUST_LOG=debug
|
||||||
|
depends_on:
|
||||||
|
- postgres_gamma
|
||||||
|
postgres_gamma:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres_gamma:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
volumes:
|
||||||
|
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
5
docker/travis/docker_push.sh
vendored
Normal file
5
docker/travis/docker_push.sh
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||||
|
docker tag dessalines/lemmy:travis \
|
||||||
|
dessalines/lemmy:v0.7.30
|
||||||
|
docker push dessalines/lemmy:v0.7.30
|
26
docker/travis/run-tests.sh
vendored
Executable file
26
docker/travis/run-tests.sh
vendored
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# make sure there are no old containers or old data around
|
||||||
|
sudo docker-compose down
|
||||||
|
sudo rm -rf volumes
|
||||||
|
|
||||||
|
mkdir -p volumes/pictrs_{alpha,beta,gamma}
|
||||||
|
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
|
||||||
|
|
||||||
|
sudo docker build ../../ --file ../prod/Dockerfile --tag dessalines/lemmy:travis
|
||||||
|
|
||||||
|
sudo docker-compose up -d
|
||||||
|
|
||||||
|
pushd ../../ui
|
||||||
|
echo "Waiting for Lemmy to start..."
|
||||||
|
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||||
|
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||||
|
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||||
|
yarn
|
||||||
|
yarn api-test
|
||||||
|
popd
|
||||||
|
|
||||||
|
sudo docker-compose down
|
||||||
|
|
||||||
|
sudo rm -r volumes/
|
2
docs/src/administration_configuration.md
vendored
2
docs/src/administration_configuration.md
vendored
|
@ -5,7 +5,7 @@ The configuration is based on the file
|
||||||
This file also contains documentation for all the available options. To override the defaults, you
|
This file also contains documentation for all the available options. To override the defaults, you
|
||||||
can copy the options you want to change into your local `config.hjson` file.
|
can copy the options you want to change into your local `config.hjson` file.
|
||||||
|
|
||||||
To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`.
|
To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`. Make sure you copy the `defaults.hjson` if you do this, otherwise you will be missing settings.
|
||||||
|
|
||||||
Additionally, you can override any config files with environment variables. These have the same
|
Additionally, you can override any config files with environment variables. These have the same
|
||||||
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
|
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
|
||||||
|
|
581
docs/src/contributing_websocket_http_api.md
vendored
581
docs/src/contributing_websocket_http_api.md
vendored
|
@ -17,6 +17,7 @@
|
||||||
- [Errors](#errors)
|
- [Errors](#errors)
|
||||||
- [API documentation](#api-documentation)
|
- [API documentation](#api-documentation)
|
||||||
* [Sort Types](#sort-types)
|
* [Sort Types](#sort-types)
|
||||||
|
* [Undoing actions](#undoing-actions)
|
||||||
* [Websocket vs HTTP](#websocket-vs-http)
|
* [Websocket vs HTTP](#websocket-vs-http)
|
||||||
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
|
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
|
||||||
+ [Login](#login)
|
+ [Login](#login)
|
||||||
|
@ -43,142 +44,198 @@
|
||||||
- [Request](#request-5)
|
- [Request](#request-5)
|
||||||
- [Response](#response-5)
|
- [Response](#response-5)
|
||||||
- [HTTP](#http-6)
|
- [HTTP](#http-6)
|
||||||
+ [Edit User Mention](#edit-user-mention)
|
+ [Mark User Mention as read](#mark-user-mention-as-read)
|
||||||
- [Request](#request-6)
|
- [Request](#request-6)
|
||||||
- [Response](#response-6)
|
- [Response](#response-6)
|
||||||
- [HTTP](#http-7)
|
- [HTTP](#http-7)
|
||||||
+ [Mark All As Read](#mark-all-as-read)
|
+ [Get Private Messages](#get-private-messages)
|
||||||
- [Request](#request-7)
|
- [Request](#request-7)
|
||||||
- [Response](#response-7)
|
- [Response](#response-7)
|
||||||
- [HTTP](#http-8)
|
- [HTTP](#http-8)
|
||||||
+ [Delete Account](#delete-account)
|
+ [Create Private Message](#create-private-message)
|
||||||
- [Request](#request-8)
|
- [Request](#request-8)
|
||||||
- [Response](#response-8)
|
- [Response](#response-8)
|
||||||
- [HTTP](#http-9)
|
- [HTTP](#http-9)
|
||||||
+ [Add admin](#add-admin)
|
+ [Edit Private Message](#edit-private-message)
|
||||||
- [Request](#request-9)
|
- [Request](#request-9)
|
||||||
- [Response](#response-9)
|
- [Response](#response-9)
|
||||||
- [HTTP](#http-10)
|
- [HTTP](#http-10)
|
||||||
+ [Ban user](#ban-user)
|
+ [Delete Private Message](#delete-private-message)
|
||||||
- [Request](#request-10)
|
- [Request](#request-10)
|
||||||
- [Response](#response-10)
|
- [Response](#response-10)
|
||||||
- [HTTP](#http-11)
|
- [HTTP](#http-11)
|
||||||
* [Site](#site)
|
+ [Mark Private Message as Read](#mark-private-message-as-read)
|
||||||
+ [List Categories](#list-categories)
|
|
||||||
- [Request](#request-11)
|
- [Request](#request-11)
|
||||||
- [Response](#response-11)
|
- [Response](#response-11)
|
||||||
- [HTTP](#http-12)
|
- [HTTP](#http-12)
|
||||||
+ [Search](#search)
|
+ [Mark All As Read](#mark-all-as-read)
|
||||||
- [Request](#request-12)
|
- [Request](#request-12)
|
||||||
- [Response](#response-12)
|
- [Response](#response-12)
|
||||||
- [HTTP](#http-13)
|
- [HTTP](#http-13)
|
||||||
+ [Get Modlog](#get-modlog)
|
+ [Delete Account](#delete-account)
|
||||||
- [Request](#request-13)
|
- [Request](#request-13)
|
||||||
- [Response](#response-13)
|
- [Response](#response-13)
|
||||||
- [HTTP](#http-14)
|
- [HTTP](#http-14)
|
||||||
+ [Create Site](#create-site)
|
+ [Add admin](#add-admin)
|
||||||
- [Request](#request-14)
|
- [Request](#request-14)
|
||||||
- [Response](#response-14)
|
- [Response](#response-14)
|
||||||
- [HTTP](#http-15)
|
- [HTTP](#http-15)
|
||||||
+ [Edit Site](#edit-site)
|
+ [Ban user](#ban-user)
|
||||||
- [Request](#request-15)
|
- [Request](#request-15)
|
||||||
- [Response](#response-15)
|
- [Response](#response-15)
|
||||||
- [HTTP](#http-16)
|
- [HTTP](#http-16)
|
||||||
+ [Get Site](#get-site)
|
* [Site](#site)
|
||||||
|
+ [List Categories](#list-categories)
|
||||||
- [Request](#request-16)
|
- [Request](#request-16)
|
||||||
- [Response](#response-16)
|
- [Response](#response-16)
|
||||||
- [HTTP](#http-17)
|
- [HTTP](#http-17)
|
||||||
+ [Transfer Site](#transfer-site)
|
+ [Search](#search)
|
||||||
- [Request](#request-17)
|
- [Request](#request-17)
|
||||||
- [Response](#response-17)
|
- [Response](#response-17)
|
||||||
- [HTTP](#http-18)
|
- [HTTP](#http-18)
|
||||||
+ [Get Site Config](#get-site-config)
|
+ [Get Modlog](#get-modlog)
|
||||||
- [Request](#request-18)
|
- [Request](#request-18)
|
||||||
- [Response](#response-18)
|
- [Response](#response-18)
|
||||||
- [HTTP](#http-19)
|
- [HTTP](#http-19)
|
||||||
+ [Save Site Config](#save-site-config)
|
+ [Create Site](#create-site)
|
||||||
- [Request](#request-19)
|
- [Request](#request-19)
|
||||||
- [Response](#response-19)
|
- [Response](#response-19)
|
||||||
- [HTTP](#http-20)
|
- [HTTP](#http-20)
|
||||||
* [Community](#community)
|
+ [Edit Site](#edit-site)
|
||||||
+ [Get Community](#get-community)
|
|
||||||
- [Request](#request-20)
|
- [Request](#request-20)
|
||||||
- [Response](#response-20)
|
- [Response](#response-20)
|
||||||
- [HTTP](#http-21)
|
- [HTTP](#http-21)
|
||||||
+ [Create Community](#create-community)
|
+ [Get Site](#get-site)
|
||||||
- [Request](#request-21)
|
- [Request](#request-21)
|
||||||
- [Response](#response-21)
|
- [Response](#response-21)
|
||||||
- [HTTP](#http-22)
|
- [HTTP](#http-22)
|
||||||
+ [List Communities](#list-communities)
|
+ [Transfer Site](#transfer-site)
|
||||||
- [Request](#request-22)
|
- [Request](#request-22)
|
||||||
- [Response](#response-22)
|
- [Response](#response-22)
|
||||||
- [HTTP](#http-23)
|
- [HTTP](#http-23)
|
||||||
+ [Ban from Community](#ban-from-community)
|
+ [Get Site Config](#get-site-config)
|
||||||
- [Request](#request-23)
|
- [Request](#request-23)
|
||||||
- [Response](#response-23)
|
- [Response](#response-23)
|
||||||
- [HTTP](#http-24)
|
- [HTTP](#http-24)
|
||||||
+ [Add Mod to Community](#add-mod-to-community)
|
+ [Save Site Config](#save-site-config)
|
||||||
- [Request](#request-24)
|
- [Request](#request-24)
|
||||||
- [Response](#response-24)
|
- [Response](#response-24)
|
||||||
- [HTTP](#http-25)
|
- [HTTP](#http-25)
|
||||||
+ [Edit Community](#edit-community)
|
* [Community](#community)
|
||||||
|
+ [Get Community](#get-community)
|
||||||
- [Request](#request-25)
|
- [Request](#request-25)
|
||||||
- [Response](#response-25)
|
- [Response](#response-25)
|
||||||
- [HTTP](#http-26)
|
- [HTTP](#http-26)
|
||||||
+ [Follow Community](#follow-community)
|
+ [Create Community](#create-community)
|
||||||
- [Request](#request-26)
|
- [Request](#request-26)
|
||||||
- [Response](#response-26)
|
- [Response](#response-26)
|
||||||
- [HTTP](#http-27)
|
- [HTTP](#http-27)
|
||||||
+ [Get Followed Communities](#get-followed-communities)
|
+ [List Communities](#list-communities)
|
||||||
- [Request](#request-27)
|
- [Request](#request-27)
|
||||||
- [Response](#response-27)
|
- [Response](#response-27)
|
||||||
- [HTTP](#http-28)
|
- [HTTP](#http-28)
|
||||||
+ [Transfer Community](#transfer-community)
|
+ [Ban from Community](#ban-from-community)
|
||||||
- [Request](#request-28)
|
- [Request](#request-28)
|
||||||
- [Response](#response-28)
|
- [Response](#response-28)
|
||||||
- [HTTP](#http-29)
|
- [HTTP](#http-29)
|
||||||
* [Post](#post)
|
+ [Add Mod to Community](#add-mod-to-community)
|
||||||
+ [Create Post](#create-post)
|
|
||||||
- [Request](#request-29)
|
- [Request](#request-29)
|
||||||
- [Response](#response-29)
|
- [Response](#response-29)
|
||||||
- [HTTP](#http-30)
|
- [HTTP](#http-30)
|
||||||
+ [Get Post](#get-post)
|
+ [Edit Community](#edit-community)
|
||||||
- [Request](#request-30)
|
- [Request](#request-30)
|
||||||
- [Response](#response-30)
|
- [Response](#response-30)
|
||||||
- [HTTP](#http-31)
|
- [HTTP](#http-31)
|
||||||
+ [Get Posts](#get-posts)
|
+ [Delete Community](#delete-community)
|
||||||
- [Request](#request-31)
|
- [Request](#request-31)
|
||||||
- [Response](#response-31)
|
- [Response](#response-31)
|
||||||
- [HTTP](#http-32)
|
- [HTTP](#http-32)
|
||||||
+ [Create Post Like](#create-post-like)
|
+ [Remove Community](#remove-community)
|
||||||
- [Request](#request-32)
|
- [Request](#request-32)
|
||||||
- [Response](#response-32)
|
- [Response](#response-32)
|
||||||
- [HTTP](#http-33)
|
- [HTTP](#http-33)
|
||||||
+ [Edit Post](#edit-post)
|
+ [Follow Community](#follow-community)
|
||||||
- [Request](#request-33)
|
- [Request](#request-33)
|
||||||
- [Response](#response-33)
|
- [Response](#response-33)
|
||||||
- [HTTP](#http-34)
|
- [HTTP](#http-34)
|
||||||
+ [Save Post](#save-post)
|
+ [Get Followed Communities](#get-followed-communities)
|
||||||
- [Request](#request-34)
|
- [Request](#request-34)
|
||||||
- [Response](#response-34)
|
- [Response](#response-34)
|
||||||
- [HTTP](#http-35)
|
- [HTTP](#http-35)
|
||||||
* [Comment](#comment)
|
+ [Transfer Community](#transfer-community)
|
||||||
+ [Create Comment](#create-comment)
|
|
||||||
- [Request](#request-35)
|
- [Request](#request-35)
|
||||||
- [Response](#response-35)
|
- [Response](#response-35)
|
||||||
- [HTTP](#http-36)
|
- [HTTP](#http-36)
|
||||||
+ [Edit Comment](#edit-comment)
|
* [Post](#post)
|
||||||
|
+ [Create Post](#create-post)
|
||||||
- [Request](#request-36)
|
- [Request](#request-36)
|
||||||
- [Response](#response-36)
|
- [Response](#response-36)
|
||||||
- [HTTP](#http-37)
|
- [HTTP](#http-37)
|
||||||
+ [Save Comment](#save-comment)
|
+ [Get Post](#get-post)
|
||||||
- [Request](#request-37)
|
- [Request](#request-37)
|
||||||
- [Response](#response-37)
|
- [Response](#response-37)
|
||||||
- [HTTP](#http-38)
|
- [HTTP](#http-38)
|
||||||
+ [Create Comment Like](#create-comment-like)
|
+ [Get Posts](#get-posts)
|
||||||
- [Request](#request-38)
|
- [Request](#request-38)
|
||||||
- [Response](#response-38)
|
- [Response](#response-38)
|
||||||
- [HTTP](#http-39)
|
- [HTTP](#http-39)
|
||||||
|
+ [Create Post Like](#create-post-like)
|
||||||
|
- [Request](#request-39)
|
||||||
|
- [Response](#response-39)
|
||||||
|
- [HTTP](#http-40)
|
||||||
|
+ [Edit Post](#edit-post)
|
||||||
|
- [Request](#request-40)
|
||||||
|
- [Response](#response-40)
|
||||||
|
- [HTTP](#http-41)
|
||||||
|
+ [Delete Post](#delete-post)
|
||||||
|
- [Request](#request-41)
|
||||||
|
- [Response](#response-41)
|
||||||
|
- [HTTP](#http-42)
|
||||||
|
+ [Remove Post](#remove-post)
|
||||||
|
- [Request](#request-42)
|
||||||
|
- [Response](#response-42)
|
||||||
|
- [HTTP](#http-43)
|
||||||
|
+ [Lock Post](#lock-post)
|
||||||
|
- [Request](#request-43)
|
||||||
|
- [Response](#response-43)
|
||||||
|
- [HTTP](#http-44)
|
||||||
|
+ [Sticky Post](#sticky-post)
|
||||||
|
- [Request](#request-44)
|
||||||
|
- [Response](#response-44)
|
||||||
|
- [HTTP](#http-45)
|
||||||
|
+ [Save Post](#save-post)
|
||||||
|
- [Request](#request-45)
|
||||||
|
- [Response](#response-45)
|
||||||
|
- [HTTP](#http-46)
|
||||||
|
* [Comment](#comment)
|
||||||
|
+ [Create Comment](#create-comment)
|
||||||
|
- [Request](#request-46)
|
||||||
|
- [Response](#response-46)
|
||||||
|
- [HTTP](#http-47)
|
||||||
|
+ [Edit Comment](#edit-comment)
|
||||||
|
- [Request](#request-47)
|
||||||
|
- [Response](#response-47)
|
||||||
|
- [HTTP](#http-48)
|
||||||
|
+ [Delete Comment](#delete-comment)
|
||||||
|
- [Request](#request-48)
|
||||||
|
- [Response](#response-48)
|
||||||
|
- [HTTP](#http-49)
|
||||||
|
+ [Remove Comment](#remove-comment)
|
||||||
|
- [Request](#request-49)
|
||||||
|
- [Response](#response-49)
|
||||||
|
- [HTTP](#http-50)
|
||||||
|
+ [Mark Comment as Read](#mark-comment-as-read)
|
||||||
|
- [Request](#request-50)
|
||||||
|
- [Response](#response-50)
|
||||||
|
- [HTTP](#http-51)
|
||||||
|
+ [Save Comment](#save-comment)
|
||||||
|
- [Request](#request-51)
|
||||||
|
- [Response](#response-51)
|
||||||
|
- [HTTP](#http-52)
|
||||||
|
+ [Create Comment Like](#create-comment-like)
|
||||||
|
- [Request](#request-52)
|
||||||
|
- [Response](#response-52)
|
||||||
|
- [HTTP](#http-53)
|
||||||
* [RSS / Atom feeds](#rss--atom-feeds)
|
* [RSS / Atom feeds](#rss--atom-feeds)
|
||||||
+ [All](#all)
|
+ [All](#all)
|
||||||
+ [Community](#community-1)
|
+ [Community](#community-1)
|
||||||
|
@ -281,6 +338,10 @@ These go wherever there is a `sort` field. The available sort types are:
|
||||||
- `TopYear` - the most upvoted posts/communities of the current year.
|
- `TopYear` - the most upvoted posts/communities of the current year.
|
||||||
- `TopAll` - the most upvoted posts/communities on the current instance.
|
- `TopAll` - the most upvoted posts/communities on the current instance.
|
||||||
|
|
||||||
|
### Undoing actions
|
||||||
|
|
||||||
|
Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`.
|
||||||
|
|
||||||
### Websocket vs HTTP
|
### Websocket vs HTTP
|
||||||
|
|
||||||
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
|
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
|
||||||
|
@ -464,14 +525,17 @@ Only the first user will be able to be the admin.
|
||||||
|
|
||||||
`GET /user/mentions`
|
`GET /user/mentions`
|
||||||
|
|
||||||
#### Edit User Mention
|
#### Mark User Mention as read
|
||||||
|
|
||||||
|
Only the recipient can do this.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: "EditUserMention",
|
op: "MarkUserMentionAsRead",
|
||||||
data: {
|
data: {
|
||||||
user_mention_id: i32,
|
user_mention_id: i32,
|
||||||
read: Option<bool>,
|
read: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: "EditUserMention",
|
op: "MarkUserMentionAsRead",
|
||||||
data: {
|
data: {
|
||||||
mention: UserMentionView,
|
mention: UserMentionView,
|
||||||
}
|
}
|
||||||
|
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
|
||||||
```
|
```
|
||||||
##### HTTP
|
##### HTTP
|
||||||
|
|
||||||
`PUT /user/mention`
|
`POST /user/mention/mark_as_read`
|
||||||
|
|
||||||
|
#### Get Private Messages
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "GetPrivateMessages",
|
||||||
|
data: {
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "GetPrivateMessages",
|
||||||
|
data: {
|
||||||
|
messages: Vec<PrivateMessageView>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /private_message/list`
|
||||||
|
|
||||||
|
#### Create Private Message
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "CreatePrivateMessage",
|
||||||
|
data: {
|
||||||
|
content: String,
|
||||||
|
recipient_id: i32,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "CreatePrivateMessage",
|
||||||
|
data: {
|
||||||
|
message: PrivateMessageView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /private_message`
|
||||||
|
|
||||||
|
#### Edit Private Message
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "EditPrivateMessage",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
content: String,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "EditPrivateMessage",
|
||||||
|
data: {
|
||||||
|
message: PrivateMessageView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /private_message`
|
||||||
|
|
||||||
|
#### Delete Private Message
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeletePrivateMessage",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeletePrivateMessage",
|
||||||
|
data: {
|
||||||
|
message: PrivateMessageView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /private_message/delete`
|
||||||
|
|
||||||
|
#### Mark Private Message as Read
|
||||||
|
|
||||||
|
Only the recipient can do this.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "MarkPrivateMessageAsRead",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
read: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "MarkPrivateMessageAsRead",
|
||||||
|
data: {
|
||||||
|
message: PrivateMessageView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /private_message/mark_as_read`
|
||||||
|
|
||||||
#### Mark All As Read
|
#### Mark All As Read
|
||||||
|
|
||||||
|
@ -744,6 +942,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: "GetSite"
|
op: "GetSite"
|
||||||
|
data: {
|
||||||
|
auth: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### Response
|
##### Response
|
||||||
|
@ -756,6 +958,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
online: usize, // This is currently broken
|
online: usize, // This is currently broken
|
||||||
version: String,
|
version: String,
|
||||||
|
my_user: Option<User_>, // Gives back your user and settings if logged in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -856,7 +1059,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
data: {
|
data: {
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -973,7 +1175,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
`POST /community/mod`
|
`POST /community/mod`
|
||||||
|
|
||||||
#### Edit Community
|
#### Edit Community
|
||||||
Mods and admins can remove and lock a community, creators can delete it.
|
Only mods can edit a community.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -984,10 +1186,6 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
removed: Option<bool>,
|
|
||||||
deleted: Option<bool>,
|
|
||||||
reason: Option<String>,
|
|
||||||
expires: Option<i64>,
|
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1005,6 +1203,62 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
|
|
||||||
`PUT /community`
|
`PUT /community`
|
||||||
|
|
||||||
|
#### Delete Community
|
||||||
|
Only a creator can delete a community
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeleteCommunity",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeleteCommunity",
|
||||||
|
data: {
|
||||||
|
community: CommunityView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/delete`
|
||||||
|
|
||||||
|
#### Remove Community
|
||||||
|
Only admins can remove a community.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemoveCommunity",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
|
reason: Option<String>,
|
||||||
|
expires: Option<i64>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemoveCommunity",
|
||||||
|
data: {
|
||||||
|
community: CommunityView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/remove`
|
||||||
|
|
||||||
#### Follow Community
|
#### Follow Community
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -1090,8 +1344,9 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
name: String,
|
name: String,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
|
nsfw: bool,
|
||||||
community_id: i32,
|
community_id: i32,
|
||||||
auth: String
|
auth: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1128,7 +1383,6 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1197,25 +1451,17 @@ Post listing types are `All, Subscribed, Community`
|
||||||
`POST /post/like`
|
`POST /post/like`
|
||||||
|
|
||||||
#### Edit Post
|
#### Edit Post
|
||||||
|
|
||||||
Mods and admins can remove and lock a post, creators can delete it.
|
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: "EditPost",
|
op: "EditPost",
|
||||||
data: {
|
data: {
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
creator_id: i32,
|
|
||||||
community_id: i32,
|
|
||||||
name: String,
|
name: String,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
removed: Option<bool>,
|
nsfw: bool,
|
||||||
deleted: Option<bool>,
|
auth: String,
|
||||||
locked: Option<bool>,
|
|
||||||
reason: Option<String>,
|
|
||||||
auth: String
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1233,6 +1479,120 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
|
|
||||||
`PUT /post`
|
`PUT /post`
|
||||||
|
|
||||||
|
#### Delete Post
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeletePost",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeletePost",
|
||||||
|
data: {
|
||||||
|
post: PostView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/delete`
|
||||||
|
|
||||||
|
#### Remove Post
|
||||||
|
|
||||||
|
Only admins and mods can remove a post.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemovePost",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
|
reason: Option<String>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemovePost",
|
||||||
|
data: {
|
||||||
|
post: PostView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/remove`
|
||||||
|
|
||||||
|
#### Lock Post
|
||||||
|
|
||||||
|
Only admins and mods can lock a post.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "LockPost",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
locked: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "LockPost",
|
||||||
|
data: {
|
||||||
|
post: PostView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/lock`
|
||||||
|
|
||||||
|
#### Sticky Post
|
||||||
|
|
||||||
|
Only admins and mods can sticky a post.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "StickyPost",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
stickied: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "StickyPost",
|
||||||
|
data: {
|
||||||
|
post: PostView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/sticky`
|
||||||
|
|
||||||
#### Save Post
|
#### Save Post
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -1267,8 +1627,8 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
data: {
|
data: {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>,
|
parent_id: Option<i32>,
|
||||||
edit_id: Option<i32>,
|
|
||||||
post_id: i32,
|
post_id: i32,
|
||||||
|
form_id: Option<String>, // An optional form id, so you know which message came back
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1289,7 +1649,7 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
|
|
||||||
#### Edit Comment
|
#### Edit Comment
|
||||||
|
|
||||||
Mods and admins can remove a comment, creators can delete it.
|
Only the creator can edit the comment.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -1297,15 +1657,9 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
op: "EditComment",
|
op: "EditComment",
|
||||||
data: {
|
data: {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>,
|
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
creator_id: i32,
|
form_id: Option<String>,
|
||||||
post_id: i32,
|
auth: String,
|
||||||
removed: Option<bool>,
|
|
||||||
deleted: Option<bool>,
|
|
||||||
reason: Option<String>,
|
|
||||||
read: Option<bool>,
|
|
||||||
auth: String
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1322,6 +1676,92 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
|
|
||||||
`PUT /comment`
|
`PUT /comment`
|
||||||
|
|
||||||
|
#### Delete Comment
|
||||||
|
|
||||||
|
Only the creator can delete the comment.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeleteComment",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "DeleteComment",
|
||||||
|
data: {
|
||||||
|
comment: CommentView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment/delete`
|
||||||
|
|
||||||
|
|
||||||
|
#### Remove Comment
|
||||||
|
|
||||||
|
Only a mod or admin can remove the comment.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemoveComment",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
|
reason: Option<String>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "RemoveComment",
|
||||||
|
data: {
|
||||||
|
comment: CommentView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment/remove`
|
||||||
|
|
||||||
|
#### Mark Comment as Read
|
||||||
|
|
||||||
|
Only the recipient can do this.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "MarkCommentAsRead",
|
||||||
|
data: {
|
||||||
|
edit_id: i32,
|
||||||
|
read: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "MarkCommentAsRead",
|
||||||
|
data: {
|
||||||
|
comment: CommentView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment/mark_as_read`
|
||||||
|
|
||||||
#### Save Comment
|
#### Save Comment
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -1357,7 +1797,6 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
op: "CreateCommentLike",
|
op: "CreateCommentLike",
|
||||||
data: {
|
data: {
|
||||||
comment_id: i32,
|
comment_id: i32,
|
||||||
post_id: i32,
|
|
||||||
score: i16,
|
score: i16,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
|
|
6
server/Cargo.lock
generated
vendored
6
server/Cargo.lock
generated
vendored
|
@ -1397,9 +1397,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-signature-normalization"
|
name = "http-signature-normalization"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "648233553603e7bb55bc1ea08a514661e212c09c10f6434507894273d8b5e773"
|
checksum = "ee917294413cec0db93a8af6ecfa63730c1d2bb604bd1da69ba75b342fb23f21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -1559,7 +1559,9 @@ dependencies = [
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"regex",
|
||||||
"serde 1.0.114",
|
"serde 1.0.114",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
4
server/lemmy_db/Cargo.toml
vendored
4
server/lemmy_db/Cargo.toml
vendored
|
@ -13,4 +13,6 @@ strum_macros = "0.18.0"
|
||||||
log = "0.4.0"
|
log = "0.4.0"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
bcrypt = "0.8.0"
|
bcrypt = "0.8.0"
|
||||||
url = { version = "2.1.1", features = ["serde"] }
|
url = { version = "2.1.1", features = ["serde"] }
|
||||||
|
lazy_static = "1.3.0"
|
||||||
|
regex = "1.3.5"
|
||||||
|
|
|
@ -97,14 +97,6 @@ impl Comment {
|
||||||
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
|
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
|
||||||
use crate::schema::comment::dsl::*;
|
|
||||||
|
|
||||||
diesel::update(comment.find(comment_id))
|
|
||||||
.set(read.eq(true))
|
|
||||||
.get_result::<Self>(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||||
use crate::schema::comment::dsl::*;
|
use crate::schema::comment::dsl::*;
|
||||||
|
|
||||||
|
@ -116,6 +108,46 @@ impl Comment {
|
||||||
))
|
))
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_deleted(
|
||||||
|
conn: &PgConnection,
|
||||||
|
comment_id: i32,
|
||||||
|
new_deleted: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::comment::dsl::*;
|
||||||
|
diesel::update(comment.find(comment_id))
|
||||||
|
.set(deleted.eq(new_deleted))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_removed(
|
||||||
|
conn: &PgConnection,
|
||||||
|
comment_id: i32,
|
||||||
|
new_removed: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::comment::dsl::*;
|
||||||
|
diesel::update(comment.find(comment_id))
|
||||||
|
.set(removed.eq(new_removed))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
|
||||||
|
use crate::schema::comment::dsl::*;
|
||||||
|
diesel::update(comment.find(comment_id))
|
||||||
|
.set(read.eq(new_read))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_content(
|
||||||
|
conn: &PgConnection,
|
||||||
|
comment_id: i32,
|
||||||
|
new_content: &str,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::comment::dsl::*;
|
||||||
|
diesel::update(comment.find(comment_id))
|
||||||
|
.set((content.eq(new_content), updated.eq(naive_now())))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
|
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
naive_now,
|
||||||
schema::{community, community_follower, community_moderator, community_user_ban},
|
schema::{community, community_follower, community_moderator, community_user_ban},
|
||||||
Bannable,
|
Bannable,
|
||||||
Crud,
|
Crud,
|
||||||
|
@ -29,7 +30,6 @@ pub struct Community {
|
||||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add better delete, remove, lock actions here.
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
||||||
#[table_name = "community"]
|
#[table_name = "community"]
|
||||||
pub struct CommunityForm {
|
pub struct CommunityForm {
|
||||||
|
@ -99,6 +99,57 @@ impl Community {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
community.filter(local.eq(true)).load::<Community>(conn)
|
community.filter(local.eq(true)).load::<Community>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_deleted(
|
||||||
|
conn: &PgConnection,
|
||||||
|
community_id: i32,
|
||||||
|
new_deleted: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::community::dsl::*;
|
||||||
|
diesel::update(community.find(community_id))
|
||||||
|
.set(deleted.eq(new_deleted))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_removed(
|
||||||
|
conn: &PgConnection,
|
||||||
|
community_id: i32,
|
||||||
|
new_removed: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::community::dsl::*;
|
||||||
|
diesel::update(community.find(community_id))
|
||||||
|
.set(removed.eq(new_removed))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_creator(
|
||||||
|
conn: &PgConnection,
|
||||||
|
community_id: i32,
|
||||||
|
new_creator_id: i32,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::community::dsl::*;
|
||||||
|
diesel::update(community.find(community_id))
|
||||||
|
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn community_mods_and_admins(conn: &PgConnection, community_id: i32) -> Result<Vec<i32>, Error> {
|
||||||
|
use crate::{community_view::CommunityModeratorView, user_view::UserView};
|
||||||
|
let mut mods_and_admins: Vec<i32> = Vec::new();
|
||||||
|
mods_and_admins.append(
|
||||||
|
&mut CommunityModeratorView::for_community(conn, community_id)
|
||||||
|
.map(|v| v.into_iter().map(|m| m.user_id).collect())?,
|
||||||
|
);
|
||||||
|
mods_and_admins
|
||||||
|
.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
|
||||||
|
Ok(mods_and_admins)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
|
||||||
|
Self::community_mods_and_admins(conn, community_id)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains(&user_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||||
|
|
|
@ -295,18 +295,18 @@ pub struct CommunityModeratorView {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunityModeratorView {
|
impl CommunityModeratorView {
|
||||||
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
|
pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
use super::community_view::community_moderator_view::dsl::*;
|
use super::community_view::community_moderator_view::dsl::*;
|
||||||
community_moderator_view
|
community_moderator_view
|
||||||
.filter(community_id.eq(from_community_id))
|
.filter(community_id.eq(for_community_id))
|
||||||
.order_by(published)
|
.order_by(published)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
|
pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
use super::community_view::community_moderator_view::dsl::*;
|
use super::community_view::community_moderator_view::dsl::*;
|
||||||
community_moderator_view
|
community_moderator_view
|
||||||
.filter(user_id.eq(from_user_id))
|
.filter(user_id.eq(for_user_id))
|
||||||
.order_by(published)
|
.order_by(published)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
pub extern crate diesel;
|
pub extern crate diesel;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub extern crate strum_macros;
|
pub extern crate strum_macros;
|
||||||
|
#[macro_use]
|
||||||
|
pub extern crate lazy_static;
|
||||||
pub extern crate bcrypt;
|
pub extern crate bcrypt;
|
||||||
pub extern crate chrono;
|
pub extern crate chrono;
|
||||||
pub extern crate log;
|
pub extern crate log;
|
||||||
|
pub extern crate regex;
|
||||||
pub extern crate serde;
|
pub extern crate serde;
|
||||||
pub extern crate serde_json;
|
pub extern crate serde_json;
|
||||||
pub extern crate sha2;
|
pub extern crate sha2;
|
||||||
|
@ -12,6 +15,7 @@ pub extern crate strum;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{env, env::VarError};
|
use std::{env, env::VarError};
|
||||||
|
|
||||||
|
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
|
||||||
chrono::prelude::Utc::now().naive_utc()
|
chrono::prelude::Utc::now().naive_utc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_email_regex(test: &str) -> bool {
|
||||||
|
EMAIL_REGEX.is_match(test)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref EMAIL_REGEX: Regex =
|
||||||
|
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::fuzzy_search;
|
use super::fuzzy_search;
|
||||||
use crate::get_database_url_from_env;
|
use crate::{get_database_url_from_env, is_email_regex};
|
||||||
use diesel::{Connection, PgConnection};
|
use diesel::{Connection, PgConnection};
|
||||||
|
|
||||||
pub fn establish_unpooled_connection() -> PgConnection {
|
pub fn establish_unpooled_connection() -> PgConnection {
|
||||||
|
@ -194,4 +207,10 @@ mod tests {
|
||||||
let test = "This is a fuzzy search";
|
let test = "This is a fuzzy search";
|
||||||
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
|
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email() {
|
||||||
|
assert!(is_email_regex("gush@gmail.com"));
|
||||||
|
assert!(!is_email_regex("nada_neutho"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,50 @@ impl Post {
|
||||||
))
|
))
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_deleted(
|
||||||
|
conn: &PgConnection,
|
||||||
|
post_id: i32,
|
||||||
|
new_deleted: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::post::dsl::*;
|
||||||
|
diesel::update(post.find(post_id))
|
||||||
|
.set(deleted.eq(new_deleted))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_removed(
|
||||||
|
conn: &PgConnection,
|
||||||
|
post_id: i32,
|
||||||
|
new_removed: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::post::dsl::*;
|
||||||
|
diesel::update(post.find(post_id))
|
||||||
|
.set(removed.eq(new_removed))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
|
||||||
|
use crate::schema::post::dsl::*;
|
||||||
|
diesel::update(post.find(post_id))
|
||||||
|
.set(locked.eq(new_locked))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_stickied(
|
||||||
|
conn: &PgConnection,
|
||||||
|
post_id: i32,
|
||||||
|
new_stickied: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::post::dsl::*;
|
||||||
|
diesel::update(post.find(post_id))
|
||||||
|
.set(stickied.eq(new_stickied))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
|
||||||
|
user_id == post_creator_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<PostForm> for Post {
|
impl Crud<PostForm> for Post {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{schema::private_message, Crud};
|
use crate::{naive_now, schema::private_message, Crud};
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -80,6 +80,50 @@ impl PrivateMessage {
|
||||||
.filter(ap_id.eq(object_id))
|
.filter(ap_id.eq(object_id))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_content(
|
||||||
|
conn: &PgConnection,
|
||||||
|
private_message_id: i32,
|
||||||
|
new_content: &str,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::update(private_message.find(private_message_id))
|
||||||
|
.set((content.eq(new_content), updated.eq(naive_now())))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_deleted(
|
||||||
|
conn: &PgConnection,
|
||||||
|
private_message_id: i32,
|
||||||
|
new_deleted: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::update(private_message.find(private_message_id))
|
||||||
|
.set(deleted.eq(new_deleted))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_read(
|
||||||
|
conn: &PgConnection,
|
||||||
|
private_message_id: i32,
|
||||||
|
new_read: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::update(private_message.find(private_message_id))
|
||||||
|
.set(read.eq(new_read))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::update(
|
||||||
|
private_message
|
||||||
|
.filter(recipient_id.eq(for_recipient_id))
|
||||||
|
.filter(read.eq(false)),
|
||||||
|
)
|
||||||
|
.set(read.eq(true))
|
||||||
|
.get_results::<Self>(conn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -180,6 +224,10 @@ mod tests {
|
||||||
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
||||||
let updated_private_message =
|
let updated_private_message =
|
||||||
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
||||||
|
let deleted_private_message =
|
||||||
|
PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
|
||||||
|
let marked_read_private_message =
|
||||||
|
PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap();
|
||||||
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
||||||
User_::delete(&conn, inserted_creator.id).unwrap();
|
User_::delete(&conn, inserted_creator.id).unwrap();
|
||||||
User_::delete(&conn, inserted_recipient.id).unwrap();
|
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||||
|
@ -187,6 +235,8 @@ mod tests {
|
||||||
assert_eq!(expected_private_message, read_private_message);
|
assert_eq!(expected_private_message, read_private_message);
|
||||||
assert_eq!(expected_private_message, updated_private_message);
|
assert_eq!(expected_private_message, updated_private_message);
|
||||||
assert_eq!(expected_private_message, inserted_private_message);
|
assert_eq!(expected_private_message, inserted_private_message);
|
||||||
|
assert!(deleted_private_message.deleted);
|
||||||
|
assert!(marked_read_private_message.read);
|
||||||
assert_eq!(1, num_deleted);
|
assert_eq!(1, num_deleted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
is_email_regex,
|
||||||
naive_now,
|
naive_now,
|
||||||
schema::{user_, user_::dsl::*},
|
schema::{user_, user_::dsl::*},
|
||||||
Crud,
|
Crud,
|
||||||
};
|
};
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
|
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[table_name = "user_"]
|
#[table_name = "user_"]
|
||||||
pub struct User_ {
|
pub struct User_ {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
@ -125,9 +127,18 @@ impl User_ {
|
||||||
use crate::schema::user_::dsl::*;
|
use crate::schema::user_::dsl::*;
|
||||||
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
|
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl User_ {
|
pub fn find_by_email_or_username(
|
||||||
|
conn: &PgConnection,
|
||||||
|
username_or_email: &str,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
if is_email_regex(username_or_email) {
|
||||||
|
Self::find_by_email(conn, username_or_email)
|
||||||
|
} else {
|
||||||
|
Self::find_by_username(conn, username_or_email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
|
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
|
||||||
user_.filter(name.eq(username)).first::<User_>(conn)
|
user_.filter(name.eq(username)).first::<User_>(conn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,30 @@ impl Crud<UserMentionForm> for UserMention {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UserMention {
|
||||||
|
pub fn update_read(
|
||||||
|
conn: &PgConnection,
|
||||||
|
user_mention_id: i32,
|
||||||
|
new_read: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::update(user_mention.find(user_mention_id))
|
||||||
|
.set(read.eq(new_read))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::update(
|
||||||
|
user_mention
|
||||||
|
.filter(recipient_id.eq(for_recipient_id))
|
||||||
|
.filter(read.eq(false)),
|
||||||
|
)
|
||||||
|
.set(read.eq(true))
|
||||||
|
.get_results::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
@ -56,14 +56,14 @@ pub struct UserView {
|
||||||
pub actor_id: String,
|
pub actor_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>, // TODO this shouldn't be in this view
|
||||||
pub matrix_user_id: Option<String>,
|
pub matrix_user_id: Option<String>,
|
||||||
pub bio: Option<String>,
|
pub bio: Option<String>,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub show_avatars: bool,
|
pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
|
||||||
pub send_notifications_to_email: bool,
|
pub send_notifications_to_email: bool, // TODO also never used
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub post_score: i64,
|
pub post_score: i64,
|
||||||
|
|
2
server/lemmy_utils/Cargo.toml
vendored
2
server/lemmy_utils/Cargo.toml
vendored
|
@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]}
|
||||||
comrak = "0.7"
|
comrak = "0.7"
|
||||||
lazy_static = "1.3.0"
|
lazy_static = "1.3.0"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
url = { version = "2.1.1", features = ["serde"] }
|
url = { version = "2.1.1", features = ["serde"] }
|
||||||
|
|
|
@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
|
||||||
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
|
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_email_regex(test: &str) -> bool {
|
|
||||||
EMAIL_REGEX.is_match(test)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_slurs(test: &str) -> String {
|
pub fn remove_slurs(test: &str) -> String {
|
||||||
SLUR_REGEX.replace_all(test, "*removed*").to_string()
|
SLUR_REGEX.replace_all(test, "*removed*").to_string()
|
||||||
}
|
}
|
||||||
|
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
is_email_regex,
|
|
||||||
is_valid_community_name,
|
is_valid_community_name,
|
||||||
is_valid_post_title,
|
is_valid_post_title,
|
||||||
is_valid_username,
|
is_valid_username,
|
||||||
|
@ -185,12 +180,6 @@ mod tests {
|
||||||
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
|
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_email() {
|
|
||||||
assert!(is_email_regex("gush@gmail.com"));
|
|
||||||
assert!(!is_email_regex("nada_neutho"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_register_username() {
|
fn test_valid_register_username() {
|
||||||
assert!(is_valid_username("Hello_98"));
|
assert!(is_valid_username("Hello_98"));
|
||||||
|
|
|
@ -81,9 +81,9 @@ impl Settings {
|
||||||
fn init() -> Result<Self, ConfigError> {
|
fn init() -> Result<Self, ConfigError> {
|
||||||
let mut s = Config::new();
|
let mut s = Config::new();
|
||||||
|
|
||||||
s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
|
s.merge(File::with_name(&Self::get_config_defaults_location()))?;
|
||||||
|
|
||||||
s.merge(File::with_name(&Self::get_config_location()).required(false))?;
|
s.merge(File::with_name(CONFIG_FILE).required(false))?;
|
||||||
|
|
||||||
// Add in settings from the environment (with a prefix of LEMMY)
|
// Add in settings from the environment (with a prefix of LEMMY)
|
||||||
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
|
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
|
||||||
|
@ -115,16 +115,16 @@ impl Settings {
|
||||||
format!("{}/api/v1", self.hostname)
|
format!("{}/api/v1", self.hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_location() -> String {
|
pub fn get_config_defaults_location() -> String {
|
||||||
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
|
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE_DEFAULTS.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_config_file() -> Result<String, Error> {
|
pub fn read_config_file() -> Result<String, Error> {
|
||||||
fs::read_to_string(Self::get_config_location())
|
fs::read_to_string(CONFIG_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config_file(data: &str) -> Result<String, Error> {
|
pub fn save_config_file(data: &str) -> Result<String, Error> {
|
||||||
fs::write(Self::get_config_location(), data)?;
|
fs::write(CONFIG_FILE, data)?;
|
||||||
|
|
||||||
// Reload the new settings
|
// Reload the new settings
|
||||||
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
|
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use diesel::{result::Error, PgConnection};
|
use diesel::{result::Error, PgConnection};
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
||||||
use lemmy_db::{user::User_, Crud};
|
use lemmy_db::{user::User_, Crud};
|
||||||
use lemmy_utils::{is_email_regex, settings::Settings};
|
use lemmy_utils::settings::Settings;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
type Jwt = String;
|
type Jwt = String;
|
||||||
|
@ -9,15 +9,7 @@ type Jwt = String;
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
|
||||||
pub iss: String,
|
pub iss: String,
|
||||||
pub show_nsfw: bool,
|
|
||||||
pub theme: String,
|
|
||||||
pub default_sort_type: i16,
|
|
||||||
pub default_listing_type: i16,
|
|
||||||
pub lang: String,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub show_avatars: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -36,15 +28,7 @@ impl Claims {
|
||||||
pub fn jwt(user: User_, hostname: String) -> Jwt {
|
pub fn jwt(user: User_, hostname: String) -> Jwt {
|
||||||
let my_claims = Claims {
|
let my_claims = Claims {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.name.to_owned(),
|
|
||||||
iss: hostname,
|
iss: hostname,
|
||||||
show_nsfw: user.show_nsfw,
|
|
||||||
theme: user.theme.to_owned(),
|
|
||||||
default_sort_type: user.default_sort_type,
|
|
||||||
default_listing_type: user.default_listing_type,
|
|
||||||
lang: user.lang.to_owned(),
|
|
||||||
avatar: user.avatar.to_owned(),
|
|
||||||
show_avatars: user.show_avatars.to_owned(),
|
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
@ -54,18 +38,6 @@ impl Claims {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move these into user?
|
|
||||||
pub fn find_by_email_or_username(
|
|
||||||
conn: &PgConnection,
|
|
||||||
username_or_email: &str,
|
|
||||||
) -> Result<User_, Error> {
|
|
||||||
if is_email_regex(username_or_email) {
|
|
||||||
User_::find_by_email(conn, username_or_email)
|
|
||||||
} else {
|
|
||||||
User_::find_by_username(conn, username_or_email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
|
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
|
||||||
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
|
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
|
||||||
User_::read(&conn, claims.id)
|
User_::read(&conn, claims.id)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, APIError, Oper, Perform},
|
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
|
||||||
apub::{ApubLikeableType, ApubObjectType},
|
apub::{ApubLikeableType, ApubObjectType},
|
||||||
blocking,
|
blocking,
|
||||||
websocket::{
|
websocket::{
|
||||||
|
@ -15,12 +15,10 @@ use lemmy_db::{
|
||||||
comment_view::*,
|
comment_view::*,
|
||||||
community_view::*,
|
community_view::*,
|
||||||
moderator::*,
|
moderator::*,
|
||||||
naive_now,
|
|
||||||
post::*,
|
post::*,
|
||||||
site_view::*,
|
site_view::*,
|
||||||
user::*,
|
user::*,
|
||||||
user_mention::*,
|
user_mention::*,
|
||||||
user_view::*,
|
|
||||||
Crud,
|
Crud,
|
||||||
Likeable,
|
Likeable,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
@ -44,22 +42,38 @@ use std::str::FromStr;
|
||||||
pub struct CreateComment {
|
pub struct CreateComment {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>,
|
parent_id: Option<i32>,
|
||||||
edit_id: Option<i32>, // TODO this isn't used
|
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
|
form_id: Option<String>,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EditComment {
|
pub struct EditComment {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
|
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
creator_id: i32,
|
form_id: Option<String>,
|
||||||
pub post_id: i32,
|
auth: String,
|
||||||
removed: Option<bool>,
|
}
|
||||||
deleted: Option<bool>,
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DeleteComment {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RemoveComment {
|
||||||
|
edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
read: Option<bool>,
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MarkCommentAsRead {
|
||||||
|
edit_id: i32,
|
||||||
|
read: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,12 +88,12 @@ pub struct SaveComment {
|
||||||
pub struct CommentResponse {
|
pub struct CommentResponse {
|
||||||
pub comment: CommentView,
|
pub comment: CommentView,
|
||||||
pub recipient_ids: Vec<i32>,
|
pub recipient_ids: Vec<i32>,
|
||||||
|
pub form_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateCommentLike {
|
pub struct CreateCommentLike {
|
||||||
comment_id: i32,
|
comment_id: i32,
|
||||||
pub post_id: i32,
|
|
||||||
score: i16,
|
score: i16,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if post is locked, no new comments
|
||||||
|
if post.locked {
|
||||||
|
return Err(APIError::err("locked").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the comment
|
||||||
let comment_form2 = comment_form.clone();
|
let comment_form2 = comment_form.clone();
|
||||||
let inserted_comment =
|
let inserted_comment =
|
||||||
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
|
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
|
||||||
|
@ -157,6 +177,7 @@ impl Perform for Oper<CreateComment> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Necessary to update the ap_id
|
||||||
let inserted_comment_id = inserted_comment.id;
|
let inserted_comment_id = inserted_comment.id;
|
||||||
let updated_comment: Comment = match blocking(pool, move |conn| {
|
let updated_comment: Comment = match blocking(pool, move |conn| {
|
||||||
let apub_id =
|
let apub_id =
|
||||||
|
@ -176,7 +197,7 @@ impl Perform for Oper<CreateComment> {
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
let mentions = scrape_text_for_mentions(&comment_form.content);
|
||||||
let recipient_ids =
|
let recipient_ids =
|
||||||
send_local_notifs(mentions, updated_comment.clone(), &user, post, pool).await?;
|
send_local_notifs(mentions, updated_comment.clone(), &user, post, pool, true).await?;
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -201,6 +222,7 @@ impl Perform for Oper<CreateComment> {
|
||||||
let mut res = CommentResponse {
|
let mut res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: data.form_id.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
if let Some(ws) = websocket_info {
|
||||||
|
@ -237,122 +259,34 @@ impl Perform for Oper<EditComment> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let orig_comment =
|
let orig_comment =
|
||||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||||
|
|
||||||
let mut editors: Vec<i32> = vec![orig_comment.creator_id];
|
// Check for a site ban
|
||||||
let mut moderators: Vec<i32> = vec![];
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
let community_id = orig_comment.community_id;
|
return Err(APIError::err("site_ban").into());
|
||||||
moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(&conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
|
|
||||||
editors.extend(&moderators);
|
|
||||||
// You are allowed to mark the comment as read even if you're banned.
|
|
||||||
if data.read.is_none() {
|
|
||||||
// Verify its the creator or a mod, or an admin
|
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
|
||||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a community ban
|
|
||||||
let community_id = orig_comment.community_id;
|
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a site ban
|
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check that user can mark as read
|
|
||||||
let parent_id = orig_comment.parent_id;
|
|
||||||
match parent_id {
|
|
||||||
Some(pid) => {
|
|
||||||
let parent_comment =
|
|
||||||
blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
|
|
||||||
if user_id != parent_comment.creator_id {
|
|
||||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let parent_post_id = orig_comment.post_id;
|
|
||||||
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
|
|
||||||
if user_id != parent_post.creator_id {
|
|
||||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_comment.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the creator can edit
|
||||||
|
if user_id != orig_comment.creator_id {
|
||||||
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the update
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
|
|
||||||
|
|
||||||
let comment_form = {
|
|
||||||
if data.read.is_none() {
|
|
||||||
// the ban etc checks should been made and have passed
|
|
||||||
// the comment can be properly edited
|
|
||||||
let post_removed = if moderators.contains(&user_id) {
|
|
||||||
data.removed
|
|
||||||
} else {
|
|
||||||
Some(read_comment.removed)
|
|
||||||
};
|
|
||||||
|
|
||||||
CommentForm {
|
|
||||||
content: content_slurs_removed,
|
|
||||||
parent_id: read_comment.parent_id,
|
|
||||||
post_id: read_comment.post_id,
|
|
||||||
creator_id: read_comment.creator_id,
|
|
||||||
removed: post_removed.to_owned(),
|
|
||||||
deleted: data.deleted.to_owned(),
|
|
||||||
read: Some(read_comment.read),
|
|
||||||
published: None,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
ap_id: read_comment.ap_id,
|
|
||||||
local: read_comment.local,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// the only field that can be updated it the read field
|
|
||||||
CommentForm {
|
|
||||||
content: read_comment.content,
|
|
||||||
parent_id: read_comment.parent_id,
|
|
||||||
post_id: read_comment.post_id,
|
|
||||||
creator_id: read_comment.creator_id,
|
|
||||||
removed: Some(read_comment.removed).to_owned(),
|
|
||||||
deleted: Some(read_comment.deleted).to_owned(),
|
|
||||||
read: data.read.to_owned(),
|
|
||||||
published: None,
|
|
||||||
updated: orig_comment.updated,
|
|
||||||
ap_id: read_comment.ap_id,
|
|
||||||
local: read_comment.local,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
|
||||||
let comment_form2 = comment_form.clone();
|
|
||||||
let updated_comment = match blocking(pool, move |conn| {
|
let updated_comment = match blocking(pool, move |conn| {
|
||||||
Comment::update(conn, edit_id, &comment_form2)
|
Comment::update_content(conn, edit_id, &content_slurs_removed)
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
@ -360,54 +294,19 @@ impl Perform for Oper<EditComment> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.read.is_none() {
|
// Send the apub update
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
updated_comment
|
||||||
if deleted {
|
.send_update(&user, &self.client, pool)
|
||||||
updated_comment
|
.await?;
|
||||||
.send_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_comment
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
|
||||||
if moderators.contains(&user_id) {
|
|
||||||
if removed {
|
|
||||||
updated_comment
|
|
||||||
.send_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_comment
|
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_comment
|
|
||||||
.send_update(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mod tables
|
// Do the mentions / recipients
|
||||||
if moderators.contains(&user_id) {
|
let post_id = orig_comment.post_id;
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
|
||||||
let form = ModRemoveCommentForm {
|
|
||||||
mod_user_id: user_id,
|
|
||||||
comment_id: data.edit_id,
|
|
||||||
removed: Some(removed),
|
|
||||||
reason: data.reason.to_owned(),
|
|
||||||
};
|
|
||||||
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let post_id = data.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||||
|
|
||||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
let updated_comment_content = updated_comment.content.to_owned();
|
||||||
let recipient_ids = send_local_notifs(mentions, updated_comment, &user, post, pool).await?;
|
let mentions = scrape_text_for_mentions(&updated_comment_content);
|
||||||
|
let recipient_ids =
|
||||||
|
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let comment_view = blocking(pool, move |conn| {
|
let comment_view = blocking(pool, move |conn| {
|
||||||
|
@ -418,6 +317,7 @@ impl Perform for Oper<EditComment> {
|
||||||
let mut res = CommentResponse {
|
let mut res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: data.form_id.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
if let Some(ws) = websocket_info {
|
||||||
|
@ -436,6 +336,291 @@ impl Perform for Oper<EditComment> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<DeleteComment> {
|
||||||
|
type Response = CommentResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<CommentResponse, LemmyError> {
|
||||||
|
let data: &DeleteComment = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_comment =
|
||||||
|
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_comment.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the creator can delete
|
||||||
|
if user_id != orig_comment.creator_id {
|
||||||
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the delete
|
||||||
|
let deleted = data.deleted;
|
||||||
|
let updated_comment = match blocking(pool, move |conn| {
|
||||||
|
Comment::update_deleted(conn, edit_id, deleted)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the apub message
|
||||||
|
if deleted {
|
||||||
|
updated_comment
|
||||||
|
.send_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
updated_comment
|
||||||
|
.send_undo_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch it
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let comment_view = blocking(pool, move |conn| {
|
||||||
|
CommentView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Build the recipients
|
||||||
|
let post_id = comment_view.post_id;
|
||||||
|
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||||
|
let mentions = vec![];
|
||||||
|
let recipient_ids =
|
||||||
|
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
|
||||||
|
|
||||||
|
let mut res = CommentResponse {
|
||||||
|
comment: comment_view,
|
||||||
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendComment {
|
||||||
|
op: UserOperation::DeleteComment,
|
||||||
|
comment: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// strip out the recipient_ids, so that
|
||||||
|
// users don't get double notifs
|
||||||
|
res.recipient_ids = Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<RemoveComment> {
|
||||||
|
type Response = CommentResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<CommentResponse, LemmyError> {
|
||||||
|
let data: &RemoveComment = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_comment =
|
||||||
|
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_comment.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only a mod or admin can remove
|
||||||
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
|
|
||||||
|
// Do the remove
|
||||||
|
let removed = data.removed;
|
||||||
|
let updated_comment = match blocking(pool, move |conn| {
|
||||||
|
Comment::update_removed(conn, edit_id, removed)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mod tables
|
||||||
|
let form = ModRemoveCommentForm {
|
||||||
|
mod_user_id: user_id,
|
||||||
|
comment_id: data.edit_id,
|
||||||
|
removed: Some(removed),
|
||||||
|
reason: data.reason.to_owned(),
|
||||||
|
};
|
||||||
|
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
|
||||||
|
|
||||||
|
// Send the apub message
|
||||||
|
if removed {
|
||||||
|
updated_comment
|
||||||
|
.send_remove(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
updated_comment
|
||||||
|
.send_undo_remove(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch it
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let comment_view = blocking(pool, move |conn| {
|
||||||
|
CommentView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Build the recipients
|
||||||
|
let post_id = comment_view.post_id;
|
||||||
|
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||||
|
let mentions = vec![];
|
||||||
|
let recipient_ids =
|
||||||
|
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
|
||||||
|
|
||||||
|
let mut res = CommentResponse {
|
||||||
|
comment: comment_view,
|
||||||
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendComment {
|
||||||
|
op: UserOperation::RemoveComment,
|
||||||
|
comment: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// strip out the recipient_ids, so that
|
||||||
|
// users don't get double notifs
|
||||||
|
res.recipient_ids = Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<MarkCommentAsRead> {
|
||||||
|
type Response = CommentResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
_websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<CommentResponse, LemmyError> {
|
||||||
|
let data: &MarkCommentAsRead = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_comment =
|
||||||
|
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_comment.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the recipient can mark as read
|
||||||
|
// Needs to fetch the parent comment / post to get the recipient
|
||||||
|
let parent_id = orig_comment.parent_id;
|
||||||
|
match parent_id {
|
||||||
|
Some(pid) => {
|
||||||
|
let parent_comment =
|
||||||
|
blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
|
||||||
|
if user_id != parent_comment.creator_id {
|
||||||
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let parent_post_id = orig_comment.post_id;
|
||||||
|
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
|
||||||
|
if user_id != parent_post.creator_id {
|
||||||
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the mark as read
|
||||||
|
let read = data.read;
|
||||||
|
match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? {
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refetch it
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let comment_view = blocking(pool, move |conn| {
|
||||||
|
CommentView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = CommentResponse {
|
||||||
|
comment: comment_view,
|
||||||
|
recipient_ids: Vec::new(),
|
||||||
|
form_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for Oper<SaveComment> {
|
impl Perform for Oper<SaveComment> {
|
||||||
type Response = CommentResponse;
|
type Response = CommentResponse;
|
||||||
|
@ -480,6 +665,7 @@ impl Perform for Oper<SaveComment> {
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids: Vec::new(),
|
recipient_ids: Vec::new(),
|
||||||
|
form_id: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -512,8 +698,12 @@ impl Perform for Oper<CreateCommentLike> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let comment_id = data.comment_id;
|
||||||
|
let orig_comment =
|
||||||
|
blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??;
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post_id = data.post_id;
|
let post_id = orig_comment.post_id;
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||||
let community_id = post.community_id;
|
let community_id = post.community_id;
|
||||||
let is_banned =
|
let is_banned =
|
||||||
|
@ -550,7 +740,7 @@ impl Perform for Oper<CreateCommentLike> {
|
||||||
|
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
comment_id: data.comment_id,
|
comment_id: data.comment_id,
|
||||||
post_id: data.post_id,
|
post_id,
|
||||||
user_id,
|
user_id,
|
||||||
score: data.score,
|
score: data.score,
|
||||||
};
|
};
|
||||||
|
@ -587,6 +777,7 @@ impl Perform for Oper<CreateCommentLike> {
|
||||||
let mut res = CommentResponse {
|
let mut res = CommentResponse {
|
||||||
comment: liked_comment,
|
comment: liked_comment,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
if let Some(ws) = websocket_info {
|
||||||
|
@ -675,10 +866,11 @@ pub async fn send_local_notifs(
|
||||||
user: &User_,
|
user: &User_,
|
||||||
post: Post,
|
post: Post,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
|
do_send_email: bool,
|
||||||
) -> Result<Vec<i32>, LemmyError> {
|
) -> Result<Vec<i32>, LemmyError> {
|
||||||
let user2 = user.clone();
|
let user2 = user.clone();
|
||||||
let ids = blocking(pool, move |conn| {
|
let ids = blocking(pool, move |conn| {
|
||||||
do_send_local_notifs(conn, &mentions, &comment, &user2, &post)
|
do_send_local_notifs(conn, &mentions, &comment, &user2, &post, do_send_email)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -691,6 +883,7 @@ fn do_send_local_notifs(
|
||||||
comment: &Comment,
|
comment: &Comment,
|
||||||
user: &User_,
|
user: &User_,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
|
do_send_email: bool,
|
||||||
) -> Vec<i32> {
|
) -> Vec<i32> {
|
||||||
let mut recipient_ids = Vec::new();
|
let mut recipient_ids = Vec::new();
|
||||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||||
|
@ -721,7 +914,7 @@ fn do_send_local_notifs(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send an email to those users that have notifications on
|
// Send an email to those users that have notifications on
|
||||||
if mention_user.send_notifications_to_email {
|
if do_send_email && mention_user.send_notifications_to_email {
|
||||||
if let Some(mention_email) = mention_user.email {
|
if let Some(mention_email) = mention_user.email {
|
||||||
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
|
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
|
||||||
let html = &format!(
|
let html = &format!(
|
||||||
|
@ -745,7 +938,7 @@ fn do_send_local_notifs(
|
||||||
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
|
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
|
||||||
recipient_ids.push(parent_user.id);
|
recipient_ids.push(parent_user.id);
|
||||||
|
|
||||||
if parent_user.send_notifications_to_email {
|
if do_send_email && parent_user.send_notifications_to_email {
|
||||||
if let Some(comment_reply_email) = parent_user.email {
|
if let Some(comment_reply_email) = parent_user.email {
|
||||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
||||||
let html = &format!(
|
let html = &format!(
|
||||||
|
@ -768,7 +961,7 @@ fn do_send_local_notifs(
|
||||||
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
|
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
|
||||||
recipient_ids.push(parent_user.id);
|
recipient_ids.push(parent_user.id);
|
||||||
|
|
||||||
if parent_user.send_notifications_to_email {
|
if do_send_email && parent_user.send_notifications_to_email {
|
||||||
if let Some(post_reply_email) = parent_user.email {
|
if let Some(post_reply_email) = parent_user.email {
|
||||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
||||||
let html = &format!(
|
let html = &format!(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, APIError, Oper, Perform},
|
api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
|
||||||
apub::ActorType,
|
apub::ActorType,
|
||||||
blocking,
|
blocking,
|
||||||
websocket::{
|
websocket::{
|
||||||
|
@ -34,7 +34,6 @@ pub struct GetCommunity {
|
||||||
pub struct GetCommunityResponse {
|
pub struct GetCommunityResponse {
|
||||||
pub community: CommunityView,
|
pub community: CommunityView,
|
||||||
pub moderators: Vec<CommunityModeratorView>,
|
pub moderators: Vec<CommunityModeratorView>,
|
||||||
pub admins: Vec<UserView>,
|
|
||||||
pub online: usize,
|
pub online: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,9 +100,21 @@ pub struct EditCommunity {
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
removed: Option<bool>,
|
|
||||||
deleted: Option<bool>,
|
|
||||||
nsfw: bool,
|
nsfw: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DeleteCommunity {
|
||||||
|
pub edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RemoveCommunity {
|
||||||
|
pub edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
expires: Option<i64>,
|
expires: Option<i64>,
|
||||||
auth: String,
|
auth: String,
|
||||||
|
@ -184,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
|
||||||
let site_creator_id = site.creator_id;
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
|
||||||
let creator_user = admins.remove(creator_index);
|
|
||||||
admins.insert(0, creator_user);
|
|
||||||
|
|
||||||
let online = if let Some(ws) = websocket_info {
|
let online = if let Some(ws) = websocket_info {
|
||||||
if let Some(id) = ws.id {
|
if let Some(id) = ws.id {
|
||||||
ws.chatserver.do_send(JoinCommunityRoom {
|
ws.chatserver.do_send(JoinCommunityRoom {
|
||||||
|
@ -212,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
|
||||||
let res = GetCommunityResponse {
|
let res = GetCommunityResponse {
|
||||||
community: community_view,
|
community: community_view,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
|
||||||
online,
|
online,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -366,24 +369,15 @@ impl Perform for Oper<EditCommunity> {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify its a mod
|
// Verify its a mod (only mods can edit it)
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let mut editors: Vec<i32> = Vec::new();
|
let mods: Vec<i32> = blocking(pool, move |conn| {
|
||||||
editors.append(
|
CommunityModeratorView::for_community(conn, edit_id)
|
||||||
&mut blocking(pool, move |conn| {
|
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
||||||
CommunityModeratorView::for_community(conn, edit_id)
|
})
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
.await??;
|
||||||
})
|
if !mods.contains(&user_id) {
|
||||||
.await??,
|
return Err(APIError::err("not_a_moderator").into());
|
||||||
);
|
|
||||||
editors.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
if !editors.contains(&user_id) {
|
|
||||||
return Err(APIError::err("no_community_edit_allowed").into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
|
@ -395,8 +389,8 @@ impl Perform for Oper<EditCommunity> {
|
||||||
description: data.description.to_owned(),
|
description: data.description.to_owned(),
|
||||||
category_id: data.category_id.to_owned(),
|
category_id: data.category_id.to_owned(),
|
||||||
creator_id: read_community.creator_id,
|
creator_id: read_community.creator_id,
|
||||||
removed: data.removed.to_owned(),
|
removed: Some(read_community.removed),
|
||||||
deleted: data.deleted.to_owned(),
|
deleted: Some(read_community.deleted),
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
actor_id: read_community.actor_id,
|
actor_id: read_community.actor_id,
|
||||||
|
@ -408,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let updated_community = match blocking(pool, move |conn| {
|
match blocking(pool, move |conn| {
|
||||||
Community::update(conn, edit_id, &community_form)
|
Community::update(conn, edit_id, &community_form)
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
|
@ -417,42 +411,77 @@ impl Perform for Oper<EditCommunity> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// TODO there needs to be some kind of an apub update
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
// process for communities and users
|
||||||
let expires = match data.expires {
|
|
||||||
Some(time) => Some(naive_from_unix(time)),
|
let edit_id = data.edit_id;
|
||||||
None => None,
|
let community_view = blocking(pool, move |conn| {
|
||||||
};
|
CommunityView::read(conn, edit_id, Some(user_id))
|
||||||
let form = ModRemoveCommunityForm {
|
})
|
||||||
mod_user_id: user_id,
|
.await??;
|
||||||
community_id: data.edit_id,
|
|
||||||
removed: Some(removed),
|
let res = CommunityResponse {
|
||||||
reason: data.reason.to_owned(),
|
community: community_view,
|
||||||
expires,
|
};
|
||||||
};
|
|
||||||
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
|
send_community_websocket(&res, websocket_info, UserOperation::EditCommunity);
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<DeleteCommunity> {
|
||||||
|
type Response = CommunityResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<CommunityResponse, LemmyError> {
|
||||||
|
let data: &DeleteCommunity = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
// Verify its the creator (only a creator can delete the community)
|
||||||
if deleted {
|
let edit_id = data.edit_id;
|
||||||
updated_community
|
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
||||||
.send_delete(&user, &self.client, pool)
|
if read_community.creator_id != user_id {
|
||||||
.await?;
|
return Err(APIError::err("no_community_edit_allowed").into());
|
||||||
} else {
|
}
|
||||||
updated_community
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
// Do the delete
|
||||||
.await?;
|
let edit_id = data.edit_id;
|
||||||
}
|
let deleted = data.deleted;
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
let updated_community = match blocking(pool, move |conn| {
|
||||||
if removed {
|
Community::update_deleted(conn, edit_id, deleted)
|
||||||
updated_community
|
})
|
||||||
.send_remove(&user, &self.client, pool)
|
.await?
|
||||||
.await?;
|
{
|
||||||
} else {
|
Ok(community) => community,
|
||||||
updated_community
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
};
|
||||||
.await?;
|
|
||||||
}
|
// Send apub messages
|
||||||
|
if deleted {
|
||||||
|
updated_community
|
||||||
|
.send_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
updated_community
|
||||||
|
.send_undo_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
|
@ -465,20 +494,88 @@ impl Perform for Oper<EditCommunity> {
|
||||||
community: community_view,
|
community: community_view,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
|
||||||
// Strip out the user id and subscribed when sending to others
|
|
||||||
let mut res_sent = res.clone();
|
|
||||||
res_sent.community.user_id = None;
|
|
||||||
res_sent.community.subscribed = None;
|
|
||||||
|
|
||||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
Ok(res)
|
||||||
op: UserOperation::EditCommunity,
|
}
|
||||||
response: res_sent,
|
}
|
||||||
community_id: data.edit_id,
|
|
||||||
my_id: ws.id,
|
#[async_trait::async_trait(?Send)]
|
||||||
});
|
impl Perform for Oper<RemoveCommunity> {
|
||||||
|
type Response = CommunityResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<CommunityResponse, LemmyError> {
|
||||||
|
let data: &RemoveCommunity = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify its an admin (only an admin can remove a community)
|
||||||
|
is_admin(pool, user_id).await?;
|
||||||
|
|
||||||
|
// Do the remove
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let removed = data.removed;
|
||||||
|
let updated_community = match blocking(pool, move |conn| {
|
||||||
|
Community::update_removed(conn, edit_id, removed)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(community) => community,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mod tables
|
||||||
|
let expires = match data.expires {
|
||||||
|
Some(time) => Some(naive_from_unix(time)),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let form = ModRemoveCommunityForm {
|
||||||
|
mod_user_id: user_id,
|
||||||
|
community_id: data.edit_id,
|
||||||
|
removed: Some(removed),
|
||||||
|
reason: data.reason.to_owned(),
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
|
||||||
|
|
||||||
|
// Apub messages
|
||||||
|
if removed {
|
||||||
|
updated_community
|
||||||
|
.send_remove(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
updated_community
|
||||||
|
.send_undo_remove(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let community_view = blocking(pool, move |conn| {
|
||||||
|
CommunityView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = CommunityResponse {
|
||||||
|
community: community_view,
|
||||||
|
};
|
||||||
|
|
||||||
|
send_community_websocket(&res, websocket_info, UserOperation::RemoveCommunity);
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -494,21 +591,26 @@ impl Perform for Oper<ListCommunities> {
|
||||||
) -> Result<ListCommunitiesResponse, LemmyError> {
|
) -> Result<ListCommunitiesResponse, LemmyError> {
|
||||||
let data: &ListCommunities = &self.data;
|
let data: &ListCommunities = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -654,27 +756,10 @@ impl Perform for Oper<BanFromCommunity> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let mut community_moderators: Vec<i32> = vec![];
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
|
|
||||||
community_moderators.append(
|
// Verify that only mods or admins can ban
|
||||||
&mut blocking(pool, move |conn| {
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
CommunityModeratorView::for_community(&conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
community_moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !community_moderators.contains(&user_id) {
|
|
||||||
return Err(APIError::err("couldnt_update_community").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let community_user_ban_form = CommunityUserBanForm {
|
let community_user_ban_form = CommunityUserBanForm {
|
||||||
community_id: data.community_id,
|
community_id: data.community_id,
|
||||||
|
@ -694,6 +779,7 @@ impl Perform for Oper<BanFromCommunity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
// TODO eventually do correct expires
|
||||||
let expires = match data.expires {
|
let expires = match data.expires {
|
||||||
Some(time) => Some(naive_from_unix(time)),
|
Some(time) => Some(naive_from_unix(time)),
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -753,27 +839,10 @@ impl Perform for Oper<AddModToCommunity> {
|
||||||
user_id: data.user_id,
|
user_id: data.user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut community_moderators: Vec<i32> = vec![];
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
|
|
||||||
community_moderators.append(
|
// Verify that only mods or admins can add mod
|
||||||
&mut blocking(pool, move |conn| {
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
CommunityModeratorView::for_community(&conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
community_moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !community_moderators.contains(&user_id) {
|
|
||||||
return Err(APIError::err("couldnt_update_community").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.added {
|
if data.added {
|
||||||
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
||||||
|
@ -852,26 +921,9 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
return Err(APIError::err("not_an_admin").into());
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
|
||||||
name: read_community.name,
|
|
||||||
title: read_community.title,
|
|
||||||
description: read_community.description,
|
|
||||||
category_id: read_community.category_id,
|
|
||||||
creator_id: data.user_id, // This makes the new user the community creator
|
|
||||||
removed: None,
|
|
||||||
deleted: None,
|
|
||||||
nsfw: read_community.nsfw,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
actor_id: read_community.actor_id,
|
|
||||||
local: read_community.local,
|
|
||||||
private_key: read_community.private_key,
|
|
||||||
public_key: read_community.public_key,
|
|
||||||
last_refreshed_at: None,
|
|
||||||
published: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
|
let new_creator = data.user_id;
|
||||||
|
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
|
||||||
if blocking(pool, update).await?.is_err() {
|
if blocking(pool, update).await?.is_err() {
|
||||||
return Err(APIError::err("couldnt_update_community").into());
|
return Err(APIError::err("couldnt_update_community").into());
|
||||||
};
|
};
|
||||||
|
@ -941,8 +993,27 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
Ok(GetCommunityResponse {
|
Ok(GetCommunityResponse {
|
||||||
community: community_view,
|
community: community_view,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
|
||||||
online: 0,
|
online: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_community_websocket(
|
||||||
|
res: &CommunityResponse,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
op: UserOperation,
|
||||||
|
) {
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
// Strip out the user id and subscribed when sending to others
|
||||||
|
let mut res_sent = res.clone();
|
||||||
|
res_sent.community.user_id = None;
|
||||||
|
res_sent.community.subscribed = None;
|
||||||
|
|
||||||
|
ws.chatserver.do_send(SendCommunityRoomMessage {
|
||||||
|
op,
|
||||||
|
response: res_sent,
|
||||||
|
community_id: res.community.id,
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
|
use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError};
|
||||||
use actix_web::client::Client;
|
use actix_web::client::Client;
|
||||||
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
|
use lemmy_db::{
|
||||||
|
community::*,
|
||||||
|
community_view::*,
|
||||||
|
moderator::*,
|
||||||
|
site::*,
|
||||||
|
user::*,
|
||||||
|
user_view::*,
|
||||||
|
Crud,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod claims;
|
pub mod claims;
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
|
@ -44,3 +52,25 @@ pub trait Perform {
|
||||||
websocket_info: Option<WebsocketInfo>,
|
websocket_info: Option<WebsocketInfo>,
|
||||||
) -> Result<Self::Response, LemmyError>;
|
) -> Result<Self::Response, LemmyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_mod_or_admin(
|
||||||
|
pool: &DbPool,
|
||||||
|
user_id: i32,
|
||||||
|
community_id: i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let is_mod_or_admin = blocking(pool, move |conn| {
|
||||||
|
Community::is_mod_or_admin(conn, user_id, community_id)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if !is_mod_or_admin {
|
||||||
|
return Err(APIError::err("not_an_admin").into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if !user.admin {
|
||||||
|
return Err(APIError::err("not_an_admin").into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, APIError, Oper, Perform},
|
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
|
||||||
apub::{ApubLikeableType, ApubObjectType},
|
apub::{ApubLikeableType, ApubObjectType},
|
||||||
blocking,
|
blocking,
|
||||||
fetch_iframely_and_pictrs_data,
|
fetch_iframely_and_pictrs_data,
|
||||||
|
@ -18,10 +18,8 @@ use lemmy_db::{
|
||||||
naive_now,
|
naive_now,
|
||||||
post::*,
|
post::*,
|
||||||
post_view::*,
|
post_view::*,
|
||||||
site::*,
|
|
||||||
site_view::*,
|
site_view::*,
|
||||||
user::*,
|
user::*,
|
||||||
user_view::*,
|
|
||||||
Crud,
|
Crud,
|
||||||
Likeable,
|
Likeable,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
@ -66,7 +64,6 @@ pub struct GetPostResponse {
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
|
||||||
pub online: usize,
|
pub online: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,20 +93,42 @@ pub struct CreatePostLike {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EditPost {
|
pub struct EditPost {
|
||||||
pub edit_id: i32,
|
pub edit_id: i32,
|
||||||
creator_id: i32,
|
|
||||||
community_id: i32,
|
|
||||||
name: String,
|
name: String,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
removed: Option<bool>,
|
|
||||||
deleted: Option<bool>,
|
|
||||||
nsfw: bool,
|
nsfw: bool,
|
||||||
locked: Option<bool>,
|
auth: String,
|
||||||
stickied: Option<bool>,
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DeletePost {
|
||||||
|
pub edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RemovePost {
|
||||||
|
pub edit_id: i32,
|
||||||
|
removed: bool,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LockPost {
|
||||||
|
pub edit_id: i32,
|
||||||
|
locked: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct StickyPost {
|
||||||
|
pub edit_id: i32,
|
||||||
|
stickied: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SavePost {
|
pub struct SavePost {
|
||||||
post_id: i32,
|
post_id: i32,
|
||||||
|
@ -311,14 +330,6 @@ impl Perform for Oper<GetPost> {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let site_creator_id =
|
|
||||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
|
||||||
let creator_user = admins.remove(creator_index);
|
|
||||||
admins.insert(0, creator_user);
|
|
||||||
|
|
||||||
let online = if let Some(ws) = websocket_info {
|
let online = if let Some(ws) = websocket_info {
|
||||||
if let Some(id) = ws.id {
|
if let Some(id) = ws.id {
|
||||||
ws.chatserver.do_send(JoinPostRoom {
|
ws.chatserver.do_send(JoinPostRoom {
|
||||||
|
@ -343,7 +354,6 @@ impl Perform for Oper<GetPost> {
|
||||||
comments,
|
comments,
|
||||||
community,
|
community,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
|
||||||
online,
|
online,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -360,21 +370,26 @@ impl Perform for Oper<GetPosts> {
|
||||||
) -> Result<GetPostsResponse, LemmyError> {
|
) -> Result<GetPostsResponse, LemmyError> {
|
||||||
let data: &GetPosts = &self.data;
|
let data: &GetPosts = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -549,35 +564,10 @@ impl Perform for Oper<EditPost> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||||
|
|
||||||
// Verify its the creator or a mod or admin
|
|
||||||
let community_id = read_post.community_id;
|
|
||||||
let mut editors: Vec<i32> = vec![read_post.creator_id];
|
|
||||||
let mut moderators: Vec<i32> = vec![];
|
|
||||||
|
|
||||||
moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
moderators.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
|
|
||||||
editors.extend(&moderators);
|
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
|
||||||
return Err(APIError::err("no_post_edit_allowed").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let community_id = read_post.community_id;
|
let community_id = orig_post.community_id;
|
||||||
let is_banned =
|
let is_banned =
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
if blocking(pool, is_banned).await? {
|
if blocking(pool, is_banned).await? {
|
||||||
|
@ -590,55 +580,34 @@ impl Perform for Oper<EditPost> {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that only the creator can edit
|
||||||
|
if !Post::is_post_creator(user_id, orig_post.creator_id) {
|
||||||
|
return Err(APIError::err("no_post_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch Iframely and Pictrs cached image
|
// Fetch Iframely and Pictrs cached image
|
||||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
||||||
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
||||||
|
|
||||||
let post_form = {
|
let post_form = PostForm {
|
||||||
// only modify some properties if they are a moderator
|
name: data.name.trim().to_owned(),
|
||||||
if moderators.contains(&user_id) {
|
url: data.url.to_owned(),
|
||||||
PostForm {
|
body: data.body.to_owned(),
|
||||||
name: data.name.trim().to_owned(),
|
nsfw: data.nsfw,
|
||||||
url: data.url.to_owned(),
|
creator_id: orig_post.creator_id.to_owned(),
|
||||||
body: data.body.to_owned(),
|
community_id: orig_post.community_id,
|
||||||
creator_id: read_post.creator_id.to_owned(),
|
removed: Some(orig_post.removed),
|
||||||
community_id: read_post.community_id,
|
deleted: Some(orig_post.deleted),
|
||||||
removed: data.removed.to_owned(),
|
locked: Some(orig_post.locked),
|
||||||
deleted: data.deleted.to_owned(),
|
stickied: Some(orig_post.stickied),
|
||||||
nsfw: data.nsfw,
|
updated: Some(naive_now()),
|
||||||
locked: data.locked.to_owned(),
|
embed_title: iframely_title,
|
||||||
stickied: data.stickied.to_owned(),
|
embed_description: iframely_description,
|
||||||
updated: Some(naive_now()),
|
embed_html: iframely_html,
|
||||||
embed_title: iframely_title,
|
thumbnail_url: pictrs_thumbnail,
|
||||||
embed_description: iframely_description,
|
ap_id: orig_post.ap_id,
|
||||||
embed_html: iframely_html,
|
local: orig_post.local,
|
||||||
thumbnail_url: pictrs_thumbnail,
|
published: None,
|
||||||
ap_id: read_post.ap_id,
|
|
||||||
local: read_post.local,
|
|
||||||
published: None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PostForm {
|
|
||||||
name: read_post.name.trim().to_owned(),
|
|
||||||
url: data.url.to_owned(),
|
|
||||||
body: data.body.to_owned(),
|
|
||||||
creator_id: read_post.creator_id.to_owned(),
|
|
||||||
community_id: read_post.community_id,
|
|
||||||
removed: Some(read_post.removed),
|
|
||||||
deleted: data.deleted.to_owned(),
|
|
||||||
nsfw: data.nsfw,
|
|
||||||
locked: Some(read_post.locked),
|
|
||||||
stickied: Some(read_post.stickied),
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
embed_title: iframely_title,
|
|
||||||
embed_description: iframely_description,
|
|
||||||
embed_html: iframely_html,
|
|
||||||
thumbnail_url: pictrs_thumbnail,
|
|
||||||
ap_id: read_post.ap_id,
|
|
||||||
local: read_post.local,
|
|
||||||
published: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
|
@ -656,58 +625,8 @@ impl Perform for Oper<EditPost> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if moderators.contains(&user_id) {
|
// Send apub update
|
||||||
// Mod tables
|
updated_post.send_update(&user, &self.client, pool).await?;
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
|
||||||
let form = ModRemovePostForm {
|
|
||||||
mod_user_id: user_id,
|
|
||||||
post_id: data.edit_id,
|
|
||||||
removed: Some(removed),
|
|
||||||
reason: data.reason.to_owned(),
|
|
||||||
};
|
|
||||||
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(locked) = data.locked.to_owned() {
|
|
||||||
let form = ModLockPostForm {
|
|
||||||
mod_user_id: user_id,
|
|
||||||
post_id: data.edit_id,
|
|
||||||
locked: Some(locked),
|
|
||||||
};
|
|
||||||
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(stickied) = data.stickied.to_owned() {
|
|
||||||
let form = ModStickyPostForm {
|
|
||||||
mod_user_id: user_id,
|
|
||||||
post_id: data.edit_id,
|
|
||||||
stickied: Some(stickied),
|
|
||||||
};
|
|
||||||
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
|
||||||
if deleted {
|
|
||||||
updated_post.send_delete(&user, &self.client, pool).await?;
|
|
||||||
} else {
|
|
||||||
updated_post
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
|
||||||
if moderators.contains(&user_id) {
|
|
||||||
if removed {
|
|
||||||
updated_post.send_remove(&user, &self.client, pool).await?;
|
|
||||||
} else {
|
|
||||||
updated_post
|
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_post.send_update(&user, &self.client, pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let post_view = blocking(pool, move |conn| {
|
let post_view = blocking(pool, move |conn| {
|
||||||
|
@ -729,6 +648,324 @@ impl Perform for Oper<EditPost> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<DeletePost> {
|
||||||
|
type Response = PostResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PostResponse, LemmyError> {
|
||||||
|
let data: &DeletePost = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_post.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the creator can delete
|
||||||
|
if !Post::is_post_creator(user_id, orig_post.creator_id) {
|
||||||
|
return Err(APIError::err("no_post_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let deleted = data.deleted;
|
||||||
|
let updated_post = blocking(pool, move |conn| {
|
||||||
|
Post::update_deleted(conn, edit_id, deleted)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// apub updates
|
||||||
|
if deleted {
|
||||||
|
updated_post.send_delete(&user, &self.client, pool).await?;
|
||||||
|
} else {
|
||||||
|
updated_post
|
||||||
|
.send_undo_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let post_view = blocking(pool, move |conn| {
|
||||||
|
PostView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = PostResponse { post: post_view };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendPost {
|
||||||
|
op: UserOperation::DeletePost,
|
||||||
|
post: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<RemovePost> {
|
||||||
|
type Response = PostResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PostResponse, LemmyError> {
|
||||||
|
let data: &RemovePost = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_post.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the mods can remove
|
||||||
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
|
|
||||||
|
// Update the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let removed = data.removed;
|
||||||
|
let updated_post = blocking(pool, move |conn| {
|
||||||
|
Post::update_removed(conn, edit_id, removed)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Mod tables
|
||||||
|
let form = ModRemovePostForm {
|
||||||
|
mod_user_id: user_id,
|
||||||
|
post_id: data.edit_id,
|
||||||
|
removed: Some(removed),
|
||||||
|
reason: data.reason.to_owned(),
|
||||||
|
};
|
||||||
|
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
|
||||||
|
|
||||||
|
// apub updates
|
||||||
|
if removed {
|
||||||
|
updated_post.send_remove(&user, &self.client, pool).await?;
|
||||||
|
} else {
|
||||||
|
updated_post
|
||||||
|
.send_undo_remove(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let post_view = blocking(pool, move |conn| {
|
||||||
|
PostView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = PostResponse { post: post_view };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendPost {
|
||||||
|
op: UserOperation::RemovePost,
|
||||||
|
post: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<LockPost> {
|
||||||
|
type Response = PostResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PostResponse, LemmyError> {
|
||||||
|
let data: &LockPost = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_post.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the mods can lock
|
||||||
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
|
|
||||||
|
// Update the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let locked = data.locked;
|
||||||
|
let updated_post =
|
||||||
|
blocking(pool, move |conn| Post::update_locked(conn, edit_id, locked)).await??;
|
||||||
|
|
||||||
|
// Mod tables
|
||||||
|
let form = ModLockPostForm {
|
||||||
|
mod_user_id: user_id,
|
||||||
|
post_id: data.edit_id,
|
||||||
|
locked: Some(locked),
|
||||||
|
};
|
||||||
|
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
|
||||||
|
|
||||||
|
// apub updates
|
||||||
|
updated_post.send_update(&user, &self.client, pool).await?;
|
||||||
|
|
||||||
|
// Refetch the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let post_view = blocking(pool, move |conn| {
|
||||||
|
PostView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = PostResponse { post: post_view };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendPost {
|
||||||
|
op: UserOperation::LockPost,
|
||||||
|
post: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<StickyPost> {
|
||||||
|
type Response = PostResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PostResponse, LemmyError> {
|
||||||
|
let data: &StickyPost = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let community_id = orig_post.community_id;
|
||||||
|
let is_banned =
|
||||||
|
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||||
|
if blocking(pool, is_banned).await? {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that only the mods can sticky
|
||||||
|
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||||
|
|
||||||
|
// Update the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let stickied = data.stickied;
|
||||||
|
let updated_post = blocking(pool, move |conn| {
|
||||||
|
Post::update_stickied(conn, edit_id, stickied)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Mod tables
|
||||||
|
let form = ModStickyPostForm {
|
||||||
|
mod_user_id: user_id,
|
||||||
|
post_id: data.edit_id,
|
||||||
|
stickied: Some(stickied),
|
||||||
|
};
|
||||||
|
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
|
||||||
|
|
||||||
|
// Apub updates
|
||||||
|
// TODO stickied should pry work like locked for ease of use
|
||||||
|
updated_post.send_update(&user, &self.client, pool).await?;
|
||||||
|
|
||||||
|
// Refetch the post
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let post_view = blocking(pool, move |conn| {
|
||||||
|
PostView::read(conn, edit_id, Some(user_id))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = PostResponse { post: post_view };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendPost {
|
||||||
|
op: UserOperation::StickyPost,
|
||||||
|
post: res.clone(),
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for Oper<SavePost> {
|
impl Perform for Oper<SavePost> {
|
||||||
type Response = PostResponse;
|
type Response = PostResponse;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::user::Register;
|
use super::user::Register;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, APIError, Oper, Perform},
|
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||||
apub::fetcher::search_by_apub_id,
|
apub::fetcher::search_by_apub_id,
|
||||||
blocking,
|
blocking,
|
||||||
version,
|
version,
|
||||||
|
@ -18,6 +18,7 @@ use lemmy_db::{
|
||||||
post_view::*,
|
post_view::*,
|
||||||
site::*,
|
site::*,
|
||||||
site_view::*,
|
site_view::*,
|
||||||
|
user::*,
|
||||||
user_view::*,
|
user_view::*,
|
||||||
Crud,
|
Crud,
|
||||||
SearchType,
|
SearchType,
|
||||||
|
@ -98,7 +99,9 @@ pub struct EditSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetSite {}
|
pub struct GetSite {
|
||||||
|
auth: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SiteResponse {
|
pub struct SiteResponse {
|
||||||
|
@ -112,6 +115,7 @@ pub struct GetSiteResponse {
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
pub online: usize,
|
pub online: usize,
|
||||||
version: String,
|
version: String,
|
||||||
|
my_user: Option<User_>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -257,10 +261,7 @@ impl Perform for Oper<CreateSite> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
is_admin(pool, user_id).await?;
|
||||||
if !user.admin {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -311,10 +312,7 @@ impl Perform for Oper<EditSite> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
is_admin(pool, user_id).await?;
|
||||||
if !user.admin {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||||
|
|
||||||
|
@ -358,7 +356,7 @@ impl Perform for Oper<GetSite> {
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
websocket_info: Option<WebsocketInfo>,
|
websocket_info: Option<WebsocketInfo>,
|
||||||
) -> Result<GetSiteResponse, LemmyError> {
|
) -> Result<GetSiteResponse, LemmyError> {
|
||||||
let _data: &GetSite = &self.data;
|
let data: &GetSite = &self.data;
|
||||||
|
|
||||||
// TODO refactor this a little
|
// TODO refactor this a little
|
||||||
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
||||||
|
@ -421,12 +419,29 @@ impl Perform for Oper<GetSite> {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Giving back your user, if you're logged in
|
||||||
|
let my_user: Option<User_> = match &data.auth {
|
||||||
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
user.password_encrypted = "".to_string();
|
||||||
|
user.private_key = None;
|
||||||
|
user.public_key = None;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
|
Err(_e) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
site: site_view,
|
site: site_view,
|
||||||
admins,
|
admins,
|
||||||
banned,
|
banned,
|
||||||
online,
|
online,
|
||||||
version: version::VERSION.to_string(),
|
version: version::VERSION.to_string(),
|
||||||
|
my_user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -620,6 +635,11 @@ impl Perform for Oper<TransferSite> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
// TODO add a User_::read_safe() for this.
|
||||||
|
user.password_encrypted = "".to_string();
|
||||||
|
user.private_key = None;
|
||||||
|
user.public_key = None;
|
||||||
|
|
||||||
let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||||
|
|
||||||
|
@ -670,6 +690,7 @@ impl Perform for Oper<TransferSite> {
|
||||||
banned,
|
banned,
|
||||||
online: 0,
|
online: 0,
|
||||||
version: version::VERSION.to_string(),
|
version: version::VERSION.to_string(),
|
||||||
|
my_user: Some(user),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -693,12 +714,7 @@ impl Perform for Oper<GetSiteConfig> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Only let admins read this
|
// Only let admins read this
|
||||||
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
is_admin(pool, user_id).await?;
|
||||||
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
|
|
||||||
|
|
||||||
if !admin_ids.contains(&user_id) {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_hjson = Settings::read_config_file()?;
|
let config_hjson = Settings::read_config_file()?;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, APIError, Oper, Perform},
|
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||||
apub::ApubObjectType,
|
apub::ApubObjectType,
|
||||||
blocking,
|
blocking,
|
||||||
websocket::{
|
websocket::{
|
||||||
|
@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse {
|
||||||
moderates: Vec<CommunityModeratorView>,
|
moderates: Vec<CommunityModeratorView>,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
admins: Vec<UserView>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -174,9 +173,9 @@ pub struct GetUserMentions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EditUserMention {
|
pub struct MarkUserMentionAsRead {
|
||||||
user_mention_id: i32,
|
user_mention_id: i32,
|
||||||
read: Option<bool>,
|
read: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,9 +215,21 @@ pub struct CreatePrivateMessage {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EditPrivateMessage {
|
pub struct EditPrivateMessage {
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
content: Option<String>,
|
content: String,
|
||||||
deleted: Option<bool>,
|
auth: String,
|
||||||
read: Option<bool>,
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DeletePrivateMessage {
|
||||||
|
edit_id: i32,
|
||||||
|
deleted: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MarkPrivateMessageAsRead {
|
||||||
|
edit_id: i32,
|
||||||
|
read: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +275,7 @@ impl Perform for Oper<Login> {
|
||||||
// Fetch that username / email
|
// Fetch that username / email
|
||||||
let username_or_email = data.username_or_email.clone();
|
let username_or_email = data.username_or_email.clone();
|
||||||
let user = match blocking(pool, move |conn| {
|
let user = match blocking(pool, move |conn| {
|
||||||
Claims::find_by_email_or_username(conn, &username_or_email)
|
User_::find_by_email_or_username(conn, &username_or_email)
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
@ -550,21 +561,26 @@ impl Perform for Oper<GetUserDetails> {
|
||||||
) -> Result<GetUserDetailsResponse, LemmyError> {
|
) -> Result<GetUserDetailsResponse, LemmyError> {
|
||||||
let data: &GetUserDetails = &self.data;
|
let data: &GetUserDetails = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -631,14 +647,6 @@ impl Perform for Oper<GetUserDetails> {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let site_creator_id =
|
|
||||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
|
||||||
let creator_user = admins.remove(creator_index);
|
|
||||||
admins.insert(0, creator_user);
|
|
||||||
|
|
||||||
// If its not the same user, remove the email, and settings
|
// If its not the same user, remove the email, and settings
|
||||||
// TODO an if let chain would be better here, but can't figure it out
|
// TODO an if let chain would be better here, but can't figure it out
|
||||||
// TODO separate out settings into its own thing
|
// TODO separate out settings into its own thing
|
||||||
|
@ -653,7 +661,6 @@ impl Perform for Oper<GetUserDetails> {
|
||||||
moderates,
|
moderates,
|
||||||
comments,
|
comments,
|
||||||
posts,
|
posts,
|
||||||
admins,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,10 +684,7 @@ impl Perform for Oper<AddAdmin> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
|
is_admin(pool, user_id).await?;
|
||||||
if !blocking(pool, is_admin).await?? {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let added = data.added;
|
let added = data.added;
|
||||||
let added_user_id = data.user_id;
|
let added_user_id = data.user_id;
|
||||||
|
@ -739,10 +743,7 @@ impl Perform for Oper<BanUser> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
|
is_admin(pool, user_id).await?;
|
||||||
if !blocking(pool, is_admin).await?? {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let ban = data.ban;
|
let ban = data.ban;
|
||||||
let banned_user_id = data.user_id;
|
let banned_user_id = data.user_id;
|
||||||
|
@ -862,7 +863,7 @@ impl Perform for Oper<GetUserMentions> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for Oper<EditUserMention> {
|
impl Perform for Oper<MarkUserMentionAsRead> {
|
||||||
type Response = UserMentionResponse;
|
type Response = UserMentionResponse;
|
||||||
|
|
||||||
async fn perform(
|
async fn perform(
|
||||||
|
@ -870,7 +871,7 @@ impl Perform for Oper<EditUserMention> {
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
_websocket_info: Option<WebsocketInfo>,
|
||||||
) -> Result<UserMentionResponse, LemmyError> {
|
) -> Result<UserMentionResponse, LemmyError> {
|
||||||
let data: &EditUserMention = &self.data;
|
let data: &MarkUserMentionAsRead = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
|
@ -887,15 +888,9 @@ impl Perform for Oper<EditUserMention> {
|
||||||
return Err(APIError::err("couldnt_update_comment").into());
|
return Err(APIError::err("couldnt_update_comment").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_mention_form = UserMentionForm {
|
|
||||||
recipient_id: read_user_mention.recipient_id,
|
|
||||||
comment_id: read_user_mention.comment_id,
|
|
||||||
read: data.read.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_mention_id = read_user_mention.id;
|
let user_mention_id = read_user_mention.id;
|
||||||
let update_mention =
|
let read = data.read;
|
||||||
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
|
let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
|
||||||
if blocking(pool, update_mention).await?.is_err() {
|
if blocking(pool, update_mention).await?.is_err() {
|
||||||
return Err(APIError::err("couldnt_update_comment").into());
|
return Err(APIError::err("couldnt_update_comment").into());
|
||||||
};
|
};
|
||||||
|
@ -940,70 +935,26 @@ impl Perform for Oper<MarkAllAsRead> {
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
// TODO: this should probably be a bulk operation
|
// TODO: this should probably be a bulk operation
|
||||||
|
// Not easy to do as a bulk operation,
|
||||||
|
// because recipient_id isn't in the comment table
|
||||||
for reply in &replies {
|
for reply in &replies {
|
||||||
let reply_id = reply.id;
|
let reply_id = reply.id;
|
||||||
let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id);
|
let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
|
||||||
if blocking(pool, mark_as_read).await?.is_err() {
|
if blocking(pool, mark_as_read).await?.is_err() {
|
||||||
return Err(APIError::err("couldnt_update_comment").into());
|
return Err(APIError::err("couldnt_update_comment").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mentions
|
// Mark all user mentions as read
|
||||||
let mentions = blocking(pool, move |conn| {
|
let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
|
||||||
UserMentionQueryBuilder::create(conn, user_id)
|
if blocking(pool, update_user_mentions).await?.is_err() {
|
||||||
.unread_only(true)
|
return Err(APIError::err("couldnt_update_comment").into());
|
||||||
.page(1)
|
|
||||||
.limit(999)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO: this should probably be a bulk operation
|
|
||||||
for mention in &mentions {
|
|
||||||
let mention_form = UserMentionForm {
|
|
||||||
recipient_id: mention.to_owned().recipient_id,
|
|
||||||
comment_id: mention.to_owned().id,
|
|
||||||
read: Some(true),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_mention_id = mention.user_mention_id;
|
|
||||||
let update_mention =
|
|
||||||
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form);
|
|
||||||
if blocking(pool, update_mention).await?.is_err() {
|
|
||||||
return Err(APIError::err("couldnt_update_comment").into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// messages
|
// Mark all private_messages as read
|
||||||
let messages = blocking(pool, move |conn| {
|
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
|
||||||
PrivateMessageQueryBuilder::create(conn, user_id)
|
if blocking(pool, update_pm).await?.is_err() {
|
||||||
.page(1)
|
return Err(APIError::err("couldnt_update_private_message").into());
|
||||||
.limit(999)
|
|
||||||
.unread_only(true)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO: this should probably be a bulk operation
|
|
||||||
for message in &messages {
|
|
||||||
let private_message_form = PrivateMessageForm {
|
|
||||||
content: message.to_owned().content,
|
|
||||||
creator_id: message.to_owned().creator_id,
|
|
||||||
recipient_id: message.to_owned().recipient_id,
|
|
||||||
deleted: None,
|
|
||||||
read: Some(true),
|
|
||||||
updated: None,
|
|
||||||
ap_id: message.to_owned().ap_id,
|
|
||||||
local: message.local,
|
|
||||||
published: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let message_id = message.id;
|
|
||||||
let update_pm =
|
|
||||||
move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
|
|
||||||
if blocking(pool, update_pm).await?.is_err() {
|
|
||||||
return Err(APIError::err("couldnt_update_private_message").into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GetRepliesResponse { replies: vec![] })
|
Ok(GetRepliesResponse { replies: vec![] })
|
||||||
|
@ -1242,11 +1193,11 @@ impl Perform for Oper<CreatePrivateMessage> {
|
||||||
let subject = &format!(
|
let subject = &format!(
|
||||||
"{} - Private Message from {}",
|
"{} - Private Message from {}",
|
||||||
Settings::get().hostname,
|
Settings::get().hostname,
|
||||||
claims.username
|
user.name,
|
||||||
);
|
);
|
||||||
let html = &format!(
|
let html = &format!(
|
||||||
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
claims.username, &content_slurs_removed, hostname
|
user.name, &content_slurs_removed, hostname
|
||||||
);
|
);
|
||||||
match send_email(subject, &email, &recipient_user.name, html) {
|
match send_email(subject, &email, &recipient_user.name, html) {
|
||||||
Ok(_o) => _o,
|
Ok(_o) => _o,
|
||||||
|
@ -1293,59 +1244,25 @@ impl Perform for Oper<EditPrivateMessage> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
|
||||||
let orig_private_message =
|
|
||||||
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
if user.banned {
|
if user.banned {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to make sure they are the creator (or the recipient marking as read
|
// Checking permissions
|
||||||
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|
let edit_id = data.edit_id;
|
||||||
|| orig_private_message.creator_id.eq(&user_id))
|
let orig_private_message =
|
||||||
{
|
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||||
|
if user_id != orig_private_message.creator_id {
|
||||||
return Err(APIError::err("no_private_message_edit_allowed").into());
|
return Err(APIError::err("no_private_message_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_slurs_removed = match &data.content {
|
// Doing the update
|
||||||
Some(content) => remove_slurs(content),
|
let content_slurs_removed = remove_slurs(&data.content);
|
||||||
None => orig_private_message.content.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let private_message_form = {
|
|
||||||
if data.read.is_some() {
|
|
||||||
PrivateMessageForm {
|
|
||||||
content: orig_private_message.content.to_owned(),
|
|
||||||
creator_id: orig_private_message.creator_id,
|
|
||||||
recipient_id: orig_private_message.recipient_id,
|
|
||||||
read: data.read.to_owned(),
|
|
||||||
updated: orig_private_message.updated,
|
|
||||||
deleted: Some(orig_private_message.deleted),
|
|
||||||
ap_id: orig_private_message.ap_id,
|
|
||||||
local: orig_private_message.local,
|
|
||||||
published: None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PrivateMessageForm {
|
|
||||||
content: content_slurs_removed,
|
|
||||||
creator_id: orig_private_message.creator_id,
|
|
||||||
recipient_id: orig_private_message.recipient_id,
|
|
||||||
deleted: data.deleted.to_owned(),
|
|
||||||
read: Some(orig_private_message.read),
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
ap_id: orig_private_message.ap_id,
|
|
||||||
local: orig_private_message.local,
|
|
||||||
published: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let updated_private_message = match blocking(pool, move |conn| {
|
let updated_private_message = match blocking(pool, move |conn| {
|
||||||
PrivateMessage::update(conn, edit_id, &private_message_form)
|
PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
@ -1353,30 +1270,14 @@ impl Perform for Oper<EditPrivateMessage> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.read.is_none() {
|
// Send the apub update
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
updated_private_message
|
||||||
if deleted {
|
.send_update(&user, &self.client, pool)
|
||||||
updated_private_message
|
.await?;
|
||||||
.send_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_private_message
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_private_message
|
|
||||||
.send_update(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_private_message
|
|
||||||
.send_update(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||||
|
let recipient_id = message.recipient_id;
|
||||||
|
|
||||||
let res = PrivateMessageResponse { message };
|
let res = PrivateMessageResponse { message };
|
||||||
|
|
||||||
|
@ -1384,7 +1285,146 @@ impl Perform for Oper<EditPrivateMessage> {
|
||||||
ws.chatserver.do_send(SendUserRoomMessage {
|
ws.chatserver.do_send(SendUserRoomMessage {
|
||||||
op: UserOperation::EditPrivateMessage,
|
op: UserOperation::EditPrivateMessage,
|
||||||
response: res.clone(),
|
response: res.clone(),
|
||||||
recipient_id: orig_private_message.recipient_id,
|
recipient_id,
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<DeletePrivateMessage> {
|
||||||
|
type Response = PrivateMessageResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PrivateMessageResponse, LemmyError> {
|
||||||
|
let data: &DeletePrivateMessage = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking permissions
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_private_message =
|
||||||
|
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||||
|
if user_id != orig_private_message.creator_id {
|
||||||
|
return Err(APIError::err("no_private_message_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doing the update
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let deleted = data.deleted;
|
||||||
|
let updated_private_message = match blocking(pool, move |conn| {
|
||||||
|
PrivateMessage::update_deleted(conn, edit_id, deleted)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(private_message) => private_message,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the apub update
|
||||||
|
if data.deleted {
|
||||||
|
updated_private_message
|
||||||
|
.send_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
updated_private_message
|
||||||
|
.send_undo_delete(&user, &self.client, pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||||
|
let recipient_id = message.recipient_id;
|
||||||
|
|
||||||
|
let res = PrivateMessageResponse { message };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendUserRoomMessage {
|
||||||
|
op: UserOperation::DeletePrivateMessage,
|
||||||
|
response: res.clone(),
|
||||||
|
recipient_id,
|
||||||
|
my_id: ws.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<MarkPrivateMessageAsRead> {
|
||||||
|
type Response = PrivateMessageResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<PrivateMessageResponse, LemmyError> {
|
||||||
|
let data: &MarkPrivateMessageAsRead = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
if user.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking permissions
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let orig_private_message =
|
||||||
|
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||||
|
if user_id != orig_private_message.recipient_id {
|
||||||
|
return Err(APIError::err("couldnt_update_private_message").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doing the update
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let read = data.read;
|
||||||
|
match blocking(pool, move |conn| {
|
||||||
|
PrivateMessage::update_read(conn, edit_id, read)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(private_message) => private_message,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// No need to send an apub update
|
||||||
|
|
||||||
|
let edit_id = data.edit_id;
|
||||||
|
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||||
|
let recipient_id = message.recipient_id;
|
||||||
|
|
||||||
|
let res = PrivateMessageResponse { message };
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(SendUserRoomMessage {
|
||||||
|
op: UserOperation::MarkPrivateMessageAsRead,
|
||||||
|
response: res.clone(),
|
||||||
|
recipient_id,
|
||||||
my_id: ws.id,
|
my_id: ws.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
pub struct PageExtension {
|
pub struct PageExtension {
|
||||||
pub comments_enabled: bool,
|
pub comments_enabled: bool,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
|
pub stickied: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for PageExtension
|
impl<U> UnparsedExtension<U> for PageExtension
|
||||||
|
@ -19,12 +20,14 @@ where
|
||||||
Ok(PageExtension {
|
Ok(PageExtension {
|
||||||
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
|
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
|
||||||
sensitive: unparsed_mut.remove("sensitive")?,
|
sensitive: unparsed_mut.remove("sensitive")?,
|
||||||
|
stickied: unparsed_mut.remove("stickied")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||||
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
|
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
|
||||||
unparsed_mut.insert("sensitive", self.sensitive)?;
|
unparsed_mut.insert("sensitive", self.sensitive)?;
|
||||||
|
unparsed_mut.insert("stickied", self.stickied)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
apub::{
|
apub::inbox::{
|
||||||
inbox::activities::{
|
activities::{
|
||||||
create::receive_create,
|
create::receive_create,
|
||||||
delete::receive_delete,
|
delete::receive_delete,
|
||||||
dislike::receive_dislike,
|
dislike::receive_dislike,
|
||||||
|
@ -9,15 +9,14 @@ use crate::{
|
||||||
undo::receive_undo,
|
undo::receive_undo,
|
||||||
update::receive_update,
|
update::receive_update,
|
||||||
},
|
},
|
||||||
inbox::shared_inbox::receive_unhandled_activity,
|
shared_inbox::receive_unhandled_activity,
|
||||||
},
|
},
|
||||||
routes::ChatServerParam,
|
routes::ChatServerParam,
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::*, prelude::ExtendsExt};
|
use activitystreams_new::{activity::*, base::AnyBase, prelude::ExtendsExt};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
|
|
||||||
pub async fn receive_announce(
|
pub async fn receive_announce(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -30,27 +29,13 @@ pub async fn receive_announce(
|
||||||
let object = announce.object();
|
let object = announce.object();
|
||||||
let object2 = object.clone().one().unwrap();
|
let object2 = object.clone().one().unwrap();
|
||||||
match kind {
|
match kind {
|
||||||
Some("Create") => {
|
Some("Create") => receive_create(object2, client, pool, chat_server).await,
|
||||||
receive_create(object2, client, pool, chat_server).await
|
Some("Update") => receive_update(object2, client, pool, chat_server).await,
|
||||||
}
|
Some("Like") => receive_like(object2, client, pool, chat_server).await,
|
||||||
Some("Update") => {
|
Some("Dislike") => receive_dislike(object2, client, pool, chat_server).await,
|
||||||
receive_update(object2, client, pool, chat_server).await
|
Some("Delete") => receive_delete(object2, client, pool, chat_server).await,
|
||||||
}
|
Some("Remove") => receive_remove(object2, client, pool, chat_server).await,
|
||||||
Some("Like") => {
|
Some("Undo") => receive_undo(object2, client, pool, chat_server).await,
|
||||||
receive_like(object2, client, pool, chat_server).await
|
|
||||||
}
|
|
||||||
Some("Dislike") => {
|
|
||||||
receive_dislike(object2, client, pool, chat_server).await
|
|
||||||
}
|
|
||||||
Some("Delete") => {
|
|
||||||
receive_delete(object2, client, pool, chat_server).await
|
|
||||||
}
|
|
||||||
Some("Remove") => {
|
|
||||||
receive_remove(object2, client, pool, chat_server).await
|
|
||||||
}
|
|
||||||
Some("Undo") => {
|
|
||||||
receive_undo(object2, client, pool, chat_server).await
|
|
||||||
}
|
|
||||||
_ => receive_unhandled_activity(announce),
|
_ => receive_unhandled_activity(announce),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,12 @@ use crate::{
|
||||||
comment::{send_local_notifs, CommentResponse},
|
comment::{send_local_notifs, CommentResponse},
|
||||||
post::PostResponse,
|
post::PostResponse,
|
||||||
},
|
},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
PageExt,
|
PageExt,
|
||||||
|
@ -18,7 +22,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::Create, object::Note, prelude::*};
|
use activitystreams_new::{activity::Create, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{Comment, CommentForm},
|
comment::{Comment, CommentForm},
|
||||||
|
@ -28,8 +32,6 @@ use lemmy_db::{
|
||||||
Crud,
|
Crud,
|
||||||
};
|
};
|
||||||
use lemmy_utils::scrape_text_for_mentions;
|
use lemmy_utils::scrape_text_for_mentions;
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::{announce_if_community_is_local};
|
|
||||||
|
|
||||||
pub async fn receive_create(
|
pub async fn receive_create(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -100,7 +102,7 @@ async fn receive_create_comment(
|
||||||
// anyway.
|
// anyway.
|
||||||
let mentions = scrape_text_for_mentions(&inserted_comment.content);
|
let mentions = scrape_text_for_mentions(&inserted_comment.content);
|
||||||
let recipient_ids =
|
let recipient_ids =
|
||||||
send_local_notifs(mentions, inserted_comment.clone(), &user, post, pool).await?;
|
send_local_notifs(mentions, inserted_comment.clone(), &user, post, pool, true).await?;
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let comment_view = blocking(pool, move |conn| {
|
let comment_view = blocking(pool, move |conn| {
|
||||||
|
@ -111,6 +113,7 @@ async fn receive_create_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
||||||
apub::inbox::
|
|
||||||
shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
GroupExt,
|
GroupExt,
|
||||||
|
@ -18,7 +21,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::Delete, object::Note, prelude::*};
|
use activitystreams_new::{activity::Delete, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{Comment, CommentForm},
|
comment::{Comment, CommentForm},
|
||||||
|
@ -30,8 +33,6 @@ use lemmy_db::{
|
||||||
post_view::PostView,
|
post_view::PostView,
|
||||||
Crud,
|
Crud,
|
||||||
};
|
};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_delete(
|
pub async fn receive_delete(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -146,6 +147,7 @@ async fn receive_delete_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{comment::CommentResponse, post::PostResponse},
|
api::{comment::CommentResponse, post::PostResponse},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
PageExt,
|
PageExt,
|
||||||
|
@ -16,7 +20,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::Dislike, object::Note, prelude::*};
|
use activitystreams_new::{activity::Dislike, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{CommentForm, CommentLike, CommentLikeForm},
|
comment::{CommentForm, CommentLike, CommentLikeForm},
|
||||||
|
@ -25,8 +29,6 @@ use lemmy_db::{
|
||||||
post_view::PostView,
|
post_view::PostView,
|
||||||
Likeable,
|
Likeable,
|
||||||
};
|
};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_dislike(
|
pub async fn receive_dislike(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -119,6 +121,7 @@ async fn receive_dislike_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{comment::CommentResponse, post::PostResponse},
|
api::{comment::CommentResponse, post::PostResponse},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
PageExt,
|
PageExt,
|
||||||
|
@ -16,7 +20,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::Like, object::Note, prelude::*};
|
use activitystreams_new::{activity::Like, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{CommentForm, CommentLike, CommentLikeForm},
|
comment::{CommentForm, CommentLike, CommentLikeForm},
|
||||||
|
@ -25,8 +29,6 @@ use lemmy_db::{
|
||||||
post_view::PostView,
|
post_view::PostView,
|
||||||
Likeable,
|
Likeable,
|
||||||
};
|
};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_like(
|
pub async fn receive_like(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -119,6 +121,7 @@ async fn receive_like_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
GroupExt,
|
GroupExt,
|
||||||
|
@ -17,7 +21,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::Remove, object::Note, prelude::*};
|
use activitystreams_new::{activity::Remove, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{Comment, CommentForm},
|
comment::{Comment, CommentForm},
|
||||||
|
@ -29,8 +33,6 @@ use lemmy_db::{
|
||||||
post_view::PostView,
|
post_view::PostView,
|
||||||
Crud,
|
Crud,
|
||||||
};
|
};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_remove(
|
pub async fn receive_remove(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -145,6 +147,7 @@ async fn receive_remove_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
GroupExt,
|
GroupExt,
|
||||||
|
@ -17,7 +21,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{activity::*, object::Note, prelude::*};
|
use activitystreams_new::{activity::*, base::AnyBase, object::Note, prelude::*};
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
|
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
|
||||||
|
@ -30,8 +34,6 @@ use lemmy_db::{
|
||||||
Crud,
|
Crud,
|
||||||
Likeable,
|
Likeable,
|
||||||
};
|
};
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_undo(
|
pub async fn receive_undo(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -101,17 +103,14 @@ async fn receive_undo_like(
|
||||||
|
|
||||||
async fn receive_undo_dislike(
|
async fn receive_undo_dislike(
|
||||||
undo: Undo,
|
undo: Undo,
|
||||||
client: &Client,
|
_client: &Client,
|
||||||
pool: &DbPool,
|
_pool: &DbPool,
|
||||||
chat_server: ChatServerParam,
|
_chat_server: ChatServerParam,
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
|
let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
|
||||||
|
|
||||||
let type_ = dislike.object().as_single_kind_str().unwrap();
|
let type_ = dislike.object().as_single_kind_str().unwrap();
|
||||||
match type_ {
|
Err(format_err!("Undo Delete type {} not supported", type_).into())
|
||||||
// TODO: handle dislike
|
|
||||||
d => Err(format_err!("Undo Delete type {} not supported", d).into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn receive_undo_delete_comment(
|
async fn receive_undo_delete_comment(
|
||||||
|
@ -159,6 +158,7 @@ async fn receive_undo_delete_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
@ -216,6 +216,7 @@ async fn receive_undo_remove_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
@ -499,6 +500,7 @@ async fn receive_undo_like_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -3,9 +3,13 @@ use crate::{
|
||||||
comment::{send_local_notifs, CommentResponse},
|
comment::{send_local_notifs, CommentResponse},
|
||||||
post::PostResponse,
|
post::PostResponse,
|
||||||
},
|
},
|
||||||
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
|
|
||||||
apub::{
|
apub::{
|
||||||
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
fetcher::{get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post},
|
||||||
|
inbox::shared_inbox::{
|
||||||
|
announce_if_community_is_local,
|
||||||
|
get_user_from_activity,
|
||||||
|
receive_unhandled_activity,
|
||||||
|
},
|
||||||
ActorType,
|
ActorType,
|
||||||
FromApub,
|
FromApub,
|
||||||
PageExt,
|
PageExt,
|
||||||
|
@ -19,11 +23,7 @@ use crate::{
|
||||||
DbPool,
|
DbPool,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use activitystreams_new::{
|
use activitystreams_new::{activity::Update, base::AnyBase, object::Note, prelude::*};
|
||||||
activity::{Update},
|
|
||||||
object::Note,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use actix_web::{client::Client, HttpResponse};
|
use actix_web::{client::Client, HttpResponse};
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::{Comment, CommentForm},
|
comment::{Comment, CommentForm},
|
||||||
|
@ -33,8 +33,6 @@ use lemmy_db::{
|
||||||
Crud,
|
Crud,
|
||||||
};
|
};
|
||||||
use lemmy_utils::scrape_text_for_mentions;
|
use lemmy_utils::scrape_text_for_mentions;
|
||||||
use activitystreams_new::base::AnyBase;
|
|
||||||
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
|
|
||||||
|
|
||||||
pub async fn receive_update(
|
pub async fn receive_update(
|
||||||
activity: AnyBase,
|
activity: AnyBase,
|
||||||
|
@ -106,7 +104,8 @@ async fn receive_update_comment(
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||||
|
|
||||||
let mentions = scrape_text_for_mentions(&updated_comment.content);
|
let mentions = scrape_text_for_mentions(&updated_comment.content);
|
||||||
let recipient_ids = send_local_notifs(mentions, updated_comment, &user, post, pool).await?;
|
let recipient_ids =
|
||||||
|
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let comment_view =
|
let comment_view =
|
||||||
|
@ -115,6 +114,7 @@ async fn receive_update_comment(
|
||||||
let res = CommentResponse {
|
let res = CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
chat_server.do_send(SendComment {
|
chat_server.do_send(SendComment {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod activities;
|
pub mod activities;
|
||||||
pub mod community_inbox;
|
pub mod community_inbox;
|
||||||
pub mod shared_inbox;
|
pub mod shared_inbox;
|
||||||
pub mod user_inbox;
|
pub mod user_inbox;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
apub::{
|
apub::{
|
||||||
|
community::do_announce,
|
||||||
extensions::signatures::verify,
|
extensions::signatures::verify,
|
||||||
fetcher::{
|
fetcher::{
|
||||||
get_or_fetch_and_upsert_remote_actor,
|
get_or_fetch_and_upsert_remote_actor,
|
||||||
|
get_or_fetch_and_upsert_remote_community,
|
||||||
get_or_fetch_and_upsert_remote_user,
|
get_or_fetch_and_upsert_remote_user,
|
||||||
},
|
},
|
||||||
inbox::activities::{
|
inbox::activities::{
|
||||||
|
@ -23,18 +25,15 @@ use crate::{
|
||||||
};
|
};
|
||||||
use activitystreams_new::{
|
use activitystreams_new::{
|
||||||
activity::{ActorAndObject, ActorAndObjectRef},
|
activity::{ActorAndObject, ActorAndObjectRef},
|
||||||
base::{AsBase},
|
base::{AsBase, Extends},
|
||||||
|
object::AsObject,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
||||||
use lemmy_db::{user::User_};
|
use lemmy_db::user::User_;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::fmt::Debug;
|
|
||||||
use crate::apub::fetcher::get_or_fetch_and_upsert_remote_community;
|
|
||||||
use activitystreams_new::object::AsObject;
|
|
||||||
use crate::apub::community::do_announce;
|
|
||||||
use activitystreams_new::base::Extends;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
@ -78,9 +77,7 @@ pub async fn shared_inbox(
|
||||||
let kind = activity.kind().unwrap();
|
let kind = activity.kind().unwrap();
|
||||||
dbg!(kind);
|
dbg!(kind);
|
||||||
match kind {
|
match kind {
|
||||||
ValidTypes::Announce => {
|
ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
|
||||||
receive_announce(any_base, &client, &pool, chat_server).await
|
|
||||||
}
|
|
||||||
ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
|
ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
|
||||||
ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
|
ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
|
||||||
ValidTypes::Like => receive_like(any_base, &client, &pool, chat_server).await,
|
ValidTypes::Like => receive_like(any_base, &client, &pool, chat_server).await,
|
||||||
|
@ -91,7 +88,9 @@ pub async fn shared_inbox(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, LemmyError>
|
pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
|
||||||
|
activity: A,
|
||||||
|
) -> Result<HttpResponse, LemmyError>
|
||||||
where
|
where
|
||||||
A: Debug,
|
A: Debug,
|
||||||
{
|
{
|
||||||
|
@ -104,8 +103,8 @@ pub(in crate::apub::inbox) async fn get_user_from_activity<T, A>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
) -> Result<User_, LemmyError>
|
) -> Result<User_, LemmyError>
|
||||||
where
|
where
|
||||||
T: AsBase<A> + ActorAndObjectRef,
|
T: AsBase<A> + ActorAndObjectRef,
|
||||||
{
|
{
|
||||||
let actor = activity.actor()?;
|
let actor = activity.actor()?;
|
||||||
let user_uri = actor.as_single_xsd_any_uri().unwrap();
|
let user_uri = actor.as_single_xsd_any_uri().unwrap();
|
||||||
|
@ -118,11 +117,11 @@ pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
) -> Result<(), LemmyError>
|
) -> Result<(), LemmyError>
|
||||||
where
|
where
|
||||||
T: AsObject<Kind>,
|
T: AsObject<Kind>,
|
||||||
T: Extends<Kind>,
|
T: Extends<Kind>,
|
||||||
Kind: Serialize,
|
Kind: Serialize,
|
||||||
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
|
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let cc = activity.cc().unwrap();
|
let cc = activity.cc().unwrap();
|
||||||
let cc = cc.as_many().unwrap();
|
let cc = cc.as_many().unwrap();
|
||||||
|
|
|
@ -133,6 +133,7 @@ impl ToApub for Post {
|
||||||
let ext = PageExtension {
|
let ext = PageExtension {
|
||||||
comments_enabled: !self.locked,
|
comments_enabled: !self.locked,
|
||||||
sensitive: self.nsfw,
|
sensitive: self.nsfw,
|
||||||
|
stickied: self.stickied,
|
||||||
};
|
};
|
||||||
Ok(Ext1::new(page, ext))
|
Ok(Ext1::new(page, ext))
|
||||||
}
|
}
|
||||||
|
@ -244,7 +245,7 @@ impl FromApub for PostForm {
|
||||||
.map(|u| u.to_owned().naive_local()),
|
.map(|u| u.to_owned().naive_local()),
|
||||||
deleted: None,
|
deleted: None,
|
||||||
nsfw: ext.sensitive,
|
nsfw: ext.sensitive,
|
||||||
stickied: None, // -> put it in "featured" collection of the community
|
stickied: Some(ext.stickied),
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_description,
|
embed_description,
|
||||||
embed_html,
|
embed_html,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::claims::Claims,
|
|
||||||
apub::{
|
apub::{
|
||||||
activities::send_activity,
|
activities::send_activity,
|
||||||
create_apub_response,
|
create_apub_response,
|
||||||
|
@ -257,7 +256,7 @@ pub async fn get_apub_user_http(
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
) -> Result<HttpResponse<Body>, LemmyError> {
|
||||||
let user_name = info.into_inner().user_name;
|
let user_name = info.into_inner().user_name;
|
||||||
let user = blocking(&db, move |conn| {
|
let user = blocking(&db, move |conn| {
|
||||||
Claims::find_by_email_or_username(conn, &user_name)
|
User_::find_by_email_or_username(conn, &user_name)
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
let u = user.to_apub(&db).await?;
|
let u = user.to_apub(&db).await?;
|
||||||
|
|
|
@ -53,7 +53,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.route("", web::put().to(route_post::<EditCommunity>))
|
.route("", web::put().to(route_post::<EditCommunity>))
|
||||||
.route("/list", web::get().to(route_get::<ListCommunities>))
|
.route("/list", web::get().to(route_get::<ListCommunities>))
|
||||||
.route("/follow", web::post().to(route_post::<FollowCommunity>))
|
.route("/follow", web::post().to(route_post::<FollowCommunity>))
|
||||||
|
.route("/delete", web::post().to(route_post::<DeleteCommunity>))
|
||||||
// Mod Actions
|
// Mod Actions
|
||||||
|
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
|
||||||
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
|
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
|
||||||
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
||||||
.route("/mod", web::post().to(route_post::<AddModToCommunity>)),
|
.route("/mod", web::post().to(route_post::<AddModToCommunity>)),
|
||||||
|
@ -71,6 +73,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("", web::get().to(route_get::<GetPost>))
|
.route("", web::get().to(route_get::<GetPost>))
|
||||||
.route("", web::put().to(route_post::<EditPost>))
|
.route("", web::put().to(route_post::<EditPost>))
|
||||||
|
.route("/delete", web::post().to(route_post::<DeletePost>))
|
||||||
|
.route("/remove", web::post().to(route_post::<RemovePost>))
|
||||||
|
.route("/lock", web::post().to(route_post::<LockPost>))
|
||||||
|
.route("/sticky", web::post().to(route_post::<StickyPost>))
|
||||||
.route("/list", web::get().to(route_get::<GetPosts>))
|
.route("/list", web::get().to(route_get::<GetPosts>))
|
||||||
.route("/like", web::post().to(route_post::<CreatePostLike>))
|
.route("/like", web::post().to(route_post::<CreatePostLike>))
|
||||||
.route("/save", web::put().to(route_post::<SavePost>)),
|
.route("/save", web::put().to(route_post::<SavePost>)),
|
||||||
|
@ -81,6 +87,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("", web::post().to(route_post::<CreateComment>))
|
.route("", web::post().to(route_post::<CreateComment>))
|
||||||
.route("", web::put().to(route_post::<EditComment>))
|
.route("", web::put().to(route_post::<EditComment>))
|
||||||
|
.route("/delete", web::post().to(route_post::<DeleteComment>))
|
||||||
|
.route("/remove", web::post().to(route_post::<RemoveComment>))
|
||||||
|
.route(
|
||||||
|
"/mark_as_read",
|
||||||
|
web::post().to(route_post::<MarkCommentAsRead>),
|
||||||
|
)
|
||||||
.route("/like", web::post().to(route_post::<CreateCommentLike>))
|
.route("/like", web::post().to(route_post::<CreateCommentLike>))
|
||||||
.route("/save", web::put().to(route_post::<SaveComment>)),
|
.route("/save", web::put().to(route_post::<SaveComment>)),
|
||||||
)
|
)
|
||||||
|
@ -90,7 +102,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
|
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
|
||||||
.route("", web::post().to(route_post::<CreatePrivateMessage>))
|
.route("", web::post().to(route_post::<CreatePrivateMessage>))
|
||||||
.route("", web::put().to(route_post::<EditPrivateMessage>)),
|
.route("", web::put().to(route_post::<EditPrivateMessage>))
|
||||||
|
.route(
|
||||||
|
"/delete",
|
||||||
|
web::post().to(route_post::<DeletePrivateMessage>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/mark_as_read",
|
||||||
|
web::post().to(route_post::<MarkPrivateMessageAsRead>),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// User
|
// User
|
||||||
.service(
|
.service(
|
||||||
|
@ -107,7 +127,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("", web::get().to(route_get::<GetUserDetails>))
|
.route("", web::get().to(route_get::<GetUserDetails>))
|
||||||
.route("/mention", web::get().to(route_get::<GetUserMentions>))
|
.route("/mention", web::get().to(route_get::<GetUserMentions>))
|
||||||
.route("/mention", web::put().to(route_post::<EditUserMention>))
|
.route(
|
||||||
|
"/mention/mark_as_read",
|
||||||
|
web::post().to(route_post::<MarkUserMentionAsRead>),
|
||||||
|
)
|
||||||
.route("/replies", web::get().to(route_get::<GetReplies>))
|
.route("/replies", web::get().to(route_get::<GetReplies>))
|
||||||
.route(
|
.route(
|
||||||
"/followed_communities",
|
"/followed_communities",
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use crate::apub::{
|
use crate::apub::{
|
||||||
comment::get_apub_comment,
|
comment::get_apub_comment,
|
||||||
community::*,
|
community::*,
|
||||||
inbox::community_inbox::community_inbox,
|
inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox},
|
||||||
post::get_apub_post,
|
post::get_apub_post,
|
||||||
inbox::shared_inbox::shared_inbox,
|
|
||||||
user::*,
|
user::*,
|
||||||
inbox::user_inbox::user_inbox,
|
|
||||||
APUB_JSON_CONTENT_TYPE,
|
APUB_JSON_CONTENT_TYPE,
|
||||||
};
|
};
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
pub const VERSION: &str = "v0.7.26";
|
pub const VERSION: &str = "v0.7.30";
|
||||||
|
|
|
@ -28,19 +28,28 @@ pub enum UserOperation {
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
CreateComment,
|
CreateComment,
|
||||||
EditComment,
|
EditComment,
|
||||||
|
DeleteComment,
|
||||||
|
RemoveComment,
|
||||||
|
MarkCommentAsRead,
|
||||||
SaveComment,
|
SaveComment,
|
||||||
CreateCommentLike,
|
CreateCommentLike,
|
||||||
GetPosts,
|
GetPosts,
|
||||||
CreatePostLike,
|
CreatePostLike,
|
||||||
EditPost,
|
EditPost,
|
||||||
|
DeletePost,
|
||||||
|
RemovePost,
|
||||||
|
LockPost,
|
||||||
|
StickyPost,
|
||||||
SavePost,
|
SavePost,
|
||||||
EditCommunity,
|
EditCommunity,
|
||||||
|
DeleteCommunity,
|
||||||
|
RemoveCommunity,
|
||||||
FollowCommunity,
|
FollowCommunity,
|
||||||
GetFollowedCommunities,
|
GetFollowedCommunities,
|
||||||
GetUserDetails,
|
GetUserDetails,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
GetUserMentions,
|
GetUserMentions,
|
||||||
EditUserMention,
|
MarkUserMentionAsRead,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
@ -59,6 +68,8 @@ pub enum UserOperation {
|
||||||
PasswordChange,
|
PasswordChange,
|
||||||
CreatePrivateMessage,
|
CreatePrivateMessage,
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
|
DeletePrivateMessage,
|
||||||
|
MarkPrivateMessageAsRead,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
GetComments,
|
GetComments,
|
||||||
|
|
|
@ -212,6 +212,9 @@ impl ChatServer {
|
||||||
|
|
||||||
// Also leave all communities
|
// Also leave all communities
|
||||||
// This avoids double messages
|
// This avoids double messages
|
||||||
|
// TODO found a bug, whereby community messages like
|
||||||
|
// delete and remove aren't sent, because
|
||||||
|
// you left the community room
|
||||||
for sessions in self.community_rooms.values_mut() {
|
for sessions in self.community_rooms.values_mut() {
|
||||||
sessions.remove(&id);
|
sessions.remove(&id);
|
||||||
}
|
}
|
||||||
|
@ -443,18 +446,28 @@ impl ChatServer {
|
||||||
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
||||||
UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
|
UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
|
||||||
UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
|
UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
|
||||||
UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
|
UserOperation::MarkUserMentionAsRead => {
|
||||||
|
do_user_operation::<MarkUserMentionAsRead>(args).await
|
||||||
|
}
|
||||||
UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
|
UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
|
||||||
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
|
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
|
||||||
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
|
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
|
||||||
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
|
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
|
||||||
|
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
|
||||||
|
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
|
||||||
|
|
||||||
|
// Private Message ops
|
||||||
UserOperation::CreatePrivateMessage => {
|
UserOperation::CreatePrivateMessage => {
|
||||||
do_user_operation::<CreatePrivateMessage>(args).await
|
do_user_operation::<CreatePrivateMessage>(args).await
|
||||||
}
|
}
|
||||||
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
|
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
|
||||||
|
UserOperation::DeletePrivateMessage => {
|
||||||
|
do_user_operation::<DeletePrivateMessage>(args).await
|
||||||
|
}
|
||||||
|
UserOperation::MarkPrivateMessageAsRead => {
|
||||||
|
do_user_operation::<MarkPrivateMessageAsRead>(args).await
|
||||||
|
}
|
||||||
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
|
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
|
||||||
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
|
|
||||||
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
|
|
||||||
|
|
||||||
// Site ops
|
// Site ops
|
||||||
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
|
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
|
||||||
|
@ -473,6 +486,8 @@ impl ChatServer {
|
||||||
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
|
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
|
||||||
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
|
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
|
||||||
UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
|
UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
|
||||||
|
UserOperation::DeleteCommunity => do_user_operation::<DeleteCommunity>(args).await,
|
||||||
|
UserOperation::RemoveCommunity => do_user_operation::<RemoveCommunity>(args).await,
|
||||||
UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
|
UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
|
||||||
UserOperation::GetFollowedCommunities => {
|
UserOperation::GetFollowedCommunities => {
|
||||||
do_user_operation::<GetFollowedCommunities>(args).await
|
do_user_operation::<GetFollowedCommunities>(args).await
|
||||||
|
@ -485,12 +500,19 @@ impl ChatServer {
|
||||||
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
|
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
|
||||||
UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
|
UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
|
||||||
UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
|
UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
|
||||||
|
UserOperation::DeletePost => do_user_operation::<DeletePost>(args).await,
|
||||||
|
UserOperation::RemovePost => do_user_operation::<RemovePost>(args).await,
|
||||||
|
UserOperation::LockPost => do_user_operation::<LockPost>(args).await,
|
||||||
|
UserOperation::StickyPost => do_user_operation::<StickyPost>(args).await,
|
||||||
UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
|
UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
|
||||||
UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
|
UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
|
||||||
|
|
||||||
// Comment ops
|
// Comment ops
|
||||||
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
|
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
|
||||||
UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
|
UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
|
||||||
|
UserOperation::DeleteComment => do_user_operation::<DeleteComment>(args).await,
|
||||||
|
UserOperation::RemoveComment => do_user_operation::<RemoveComment>(args).await,
|
||||||
|
UserOperation::MarkCommentAsRead => do_user_operation::<MarkCommentAsRead>(args).await,
|
||||||
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
|
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
|
||||||
UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
|
UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
|
||||||
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
|
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
|
||||||
|
|
11
ui/assets/css/main.css
vendored
11
ui/assets/css/main.css
vendored
|
@ -2,6 +2,11 @@
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-expand-lg .navbar-nav .nav-link {
|
||||||
|
padding-right: .75rem !important;
|
||||||
|
padding-left: .75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -134,12 +139,14 @@ blockquote {
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
min-height: 60px;
|
||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.thumbnail {
|
.thumbnail svg {
|
||||||
height: 40px;
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-s-hows {
|
.no-s-hows {
|
||||||
|
|
23
ui/assets/css/themes/_variables.litely.scss
vendored
23
ui/assets/css/themes/_variables.litely.scss
vendored
|
@ -1,17 +1,17 @@
|
||||||
|
|
||||||
$white: #ffffff;
|
$white: #ffffff;
|
||||||
$orange: #faa077;
|
$orange: #f1641e;
|
||||||
$cyan: #02bdc2;
|
$cyan: #02bdc2;
|
||||||
$green: #d4e9d7;
|
$green: #00C853;
|
||||||
$secondary: $green;
|
$secondary: $green;
|
||||||
$body-color: $gray-700;
|
$body-color: $gray-700;
|
||||||
$link-color: theme-color("danger");;
|
$link-color: theme-color("primary");;
|
||||||
$primary: $orange;
|
$primary: $orange;
|
||||||
$red: #d8486a;
|
$red: #d8486a;
|
||||||
$border-radius: 1.5rem;
|
$border-radius: 0.5rem;
|
||||||
$border-radius-lg: 1.5rem;
|
$border-radius-lg: 0.5rem;
|
||||||
$border-radius-sm: 1rem;
|
$border-radius-sm: 1rem;
|
||||||
$font-family-sans-serif: Guardian-EgypTT,serif,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
$font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI","Helvetica",Arial,sans-serif;
|
||||||
$headings-color: $gray-700;
|
$headings-color: $gray-700;
|
||||||
$input-btn-focus-color: rgba($component-active-bg, .75);
|
$input-btn-focus-color: rgba($component-active-bg, .75);
|
||||||
$form-feedback-valid-color: theme-color("info");
|
$form-feedback-valid-color: theme-color("info");
|
||||||
|
@ -21,10 +21,13 @@ $navbar-dark-toggler-border-color: rgba($black, .1);
|
||||||
$navbar-light-active-color: $gray-900;
|
$navbar-light-active-color: $gray-900;
|
||||||
$card-color: $gray-700;
|
$card-color: $gray-700;
|
||||||
$card-cap-color: $gray-700;
|
$card-cap-color: $gray-700;
|
||||||
$info: darken($green, 25%);;
|
$info: $blue;
|
||||||
$body-bg: #f2f0f0;
|
$body-bg: #fff;
|
||||||
$success: darken($green, 25%);;
|
$success: $indigo;
|
||||||
$danger: darken($primary, 25%);
|
$danger: darken($primary, 25%);
|
||||||
$navbar-light-hover-color: $gray-900;
|
$navbar-light-hover-color: $gray-900;
|
||||||
$card-bg: $gray-100;
|
$card-bg: $gray-100;
|
||||||
$border-color: $gray-700;
|
$border-color: $gray-700;
|
||||||
|
$mark-bg: rgb(255, 252, 239);
|
||||||
|
$font-weight-bold: 600;
|
||||||
|
$rounded-pill: 0.25rem;
|
||||||
|
|
2
ui/assets/css/themes/litely.min.css
vendored
2
ui/assets/css/themes/litely.min.css
vendored
File diff suppressed because one or more lines are too long
21
ui/package.json
vendored
21
ui/package.json
vendored
|
@ -16,11 +16,13 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/autosize": "^3.0.6",
|
"@types/autosize": "^3.0.6",
|
||||||
|
"@types/jest": "^26.0.7",
|
||||||
"@types/js-cookie": "^2.2.6",
|
"@types/js-cookie": "^2.2.6",
|
||||||
"@types/jwt-decode": "^2.2.1",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/markdown-it": "^0.0.9",
|
"@types/markdown-it": "^10.0.1",
|
||||||
"@types/markdown-it-container": "^2.0.2",
|
"@types/markdown-it-container": "^2.0.2",
|
||||||
"@types/node": "^13.11.1",
|
"@types/node": "^14.0.26",
|
||||||
|
"@types/node-fetch": "^2.5.6",
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"bootswatch": "^4.3.1",
|
"bootswatch": "^4.3.1",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
|
@ -30,12 +32,13 @@
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"i18next": "^19.4.1",
|
"i18next": "^19.4.1",
|
||||||
"inferno": "^7.4.2",
|
"inferno": "^7.4.2",
|
||||||
|
"inferno-helmet": "^5.2.1",
|
||||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||||
"inferno-router": "^7.4.2",
|
"inferno-router": "^7.4.2",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"markdown-it": "^10.0.0",
|
"markdown-it": "^11.0.0",
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"markdown-it-emoji": "^1.4.0",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
@ -51,16 +54,14 @@
|
||||||
"ws": "^7.2.3"
|
"ws": "^7.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^25.2.1",
|
"eslint": "^7.5.0",
|
||||||
"@types/node-fetch": "^2.5.6",
|
|
||||||
"eslint": "^6.5.1",
|
|
||||||
"eslint-plugin-inferno": "^7.14.3",
|
"eslint-plugin-inferno": "^7.14.3",
|
||||||
"eslint-plugin-jane": "^7.2.1",
|
"eslint-plugin-jane": "^8.0.4",
|
||||||
"fuse-box": "^3.1.3",
|
"fuse-box": "^3.1.3",
|
||||||
"jest": "^25.4.0",
|
"jest": "^26.0.7",
|
||||||
"lint-staged": "^10.1.3",
|
"lint-staged": "^10.1.3",
|
||||||
"sortpack": "^2.1.4",
|
"sortpack": "^2.1.4",
|
||||||
"ts-jest": "^25.4.0",
|
"ts-jest": "^26.1.3",
|
||||||
"ts-node": "^8.8.2",
|
"ts-node": "^8.8.2",
|
||||||
"ts-transform-classcat": "^1.0.0",
|
"ts-transform-classcat": "^1.0.0",
|
||||||
"ts-transform-inferno": "^4.0.3",
|
"ts-transform-inferno": "^4.0.3",
|
||||||
|
|
276
ui/src/api_tests/api.spec.ts
vendored
276
ui/src/api_tests/api.spec.ts
vendored
|
@ -4,22 +4,29 @@ import {
|
||||||
LoginForm,
|
LoginForm,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
PostForm,
|
PostForm,
|
||||||
|
DeletePostForm,
|
||||||
|
RemovePostForm,
|
||||||
|
StickyPostForm,
|
||||||
|
LockPostForm,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
FollowCommunityForm,
|
FollowCommunityForm,
|
||||||
CommunityResponse,
|
CommunityResponse,
|
||||||
GetFollowedCommunitiesResponse,
|
GetFollowedCommunitiesResponse,
|
||||||
GetPostForm,
|
|
||||||
GetPostResponse,
|
GetPostResponse,
|
||||||
CommentForm,
|
CommentForm,
|
||||||
|
DeleteCommentForm,
|
||||||
|
RemoveCommentForm,
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
CommunityForm,
|
CommunityForm,
|
||||||
GetCommunityForm,
|
DeleteCommunityForm,
|
||||||
|
RemoveCommunityForm,
|
||||||
GetCommunityResponse,
|
GetCommunityResponse,
|
||||||
CommentLikeForm,
|
CommentLikeForm,
|
||||||
CreatePostLikeForm,
|
CreatePostLikeForm,
|
||||||
PrivateMessageForm,
|
PrivateMessageForm,
|
||||||
EditPrivateMessageForm,
|
EditPrivateMessageForm,
|
||||||
|
DeletePrivateMessageForm,
|
||||||
PrivateMessageResponse,
|
PrivateMessageResponse,
|
||||||
PrivateMessagesResponse,
|
PrivateMessagesResponse,
|
||||||
GetUserMentionsResponse,
|
GetUserMentionsResponse,
|
||||||
|
@ -97,7 +104,6 @@ describe('main', () => {
|
||||||
name,
|
name,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
community_id: 2,
|
community_id: 2,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -266,7 +272,6 @@ describe('main', () => {
|
||||||
name,
|
name,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
community_id: 3,
|
community_id: 3,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -323,7 +328,6 @@ describe('main', () => {
|
||||||
edit_id: 2,
|
edit_id: 2,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
community_id: 3,
|
community_id: 3,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -342,6 +346,27 @@ describe('main', () => {
|
||||||
expect(updateResponse.post.community_local).toBe(false);
|
expect(updateResponse.post.community_local).toBe(false);
|
||||||
expect(updateResponse.post.creator_local).toBe(true);
|
expect(updateResponse.post.creator_local).toBe(true);
|
||||||
|
|
||||||
|
let stickyPostForm: StickyPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
stickied: true,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let stickyRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/sticky`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(stickyPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(stickyRes.post.name).toBe(name);
|
||||||
|
expect(stickyRes.post.stickied).toBe(true);
|
||||||
|
|
||||||
|
// Fetch from B
|
||||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -350,6 +375,76 @@ describe('main', () => {
|
||||||
expect(getPostRes.post.name).toBe(name);
|
expect(getPostRes.post.name).toBe(name);
|
||||||
expect(getPostRes.post.community_local).toBe(true);
|
expect(getPostRes.post.community_local).toBe(true);
|
||||||
expect(getPostRes.post.creator_local).toBe(false);
|
expect(getPostRes.post.creator_local).toBe(false);
|
||||||
|
expect(getPostRes.post.stickied).toBe(true);
|
||||||
|
|
||||||
|
let lockPostForm: LockPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
locked: true,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let lockedRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/lock`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(lockPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(lockedRes.post.name).toBe(name);
|
||||||
|
expect(lockedRes.post.locked).toBe(true);
|
||||||
|
|
||||||
|
// Fetch from B to make sure its locked
|
||||||
|
getPostRes = await fetch(getPostUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(d => d.json());
|
||||||
|
expect(getPostRes.post.locked).toBe(true);
|
||||||
|
|
||||||
|
// Create a test comment on a locked post, it should be undefined
|
||||||
|
// since it shouldn't get created.
|
||||||
|
let content = 'A rejected comment on a locked post';
|
||||||
|
let commentForm: CommentForm = {
|
||||||
|
content,
|
||||||
|
post_id: 2,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let createResponse: CommentResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/comment`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(commentForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(createResponse['error']).toBe('locked');
|
||||||
|
|
||||||
|
// Unlock the post for later actions
|
||||||
|
let unlockPostForm: LockPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
locked: false,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let unlockedRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/lock`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(unlockPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(unlockedRes.post.name).toBe(name);
|
||||||
|
expect(unlockedRes.post.locked).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -382,7 +477,6 @@ describe('main', () => {
|
||||||
let unlikeCommentForm: CommentLikeForm = {
|
let unlikeCommentForm: CommentLikeForm = {
|
||||||
comment_id: createResponse.comment.id,
|
comment_id: createResponse.comment.id,
|
||||||
score: 0,
|
score: 0,
|
||||||
post_id: 2,
|
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -585,7 +679,6 @@ describe('main', () => {
|
||||||
name: postName,
|
name: postName,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
community_id: createCommunityRes.community.id,
|
community_id: createCommunityRes.community.id,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -620,19 +713,16 @@ describe('main', () => {
|
||||||
expect(createCommentRes.comment.content).toBe(commentContent);
|
expect(createCommentRes.comment.content).toBe(commentContent);
|
||||||
|
|
||||||
// lemmy_beta deletes the comment
|
// lemmy_beta deletes the comment
|
||||||
let deleteCommentForm: CommentForm = {
|
let deleteCommentForm: DeleteCommentForm = {
|
||||||
content: commentContent,
|
|
||||||
edit_id: createCommentRes.comment.id,
|
edit_id: createCommentRes.comment.id,
|
||||||
post_id: createPostRes.post.id,
|
|
||||||
deleted: true,
|
deleted: true,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
creator_id: createCommentRes.comment.creator_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let deleteCommentRes: CommentResponse = await fetch(
|
let deleteCommentRes: CommentResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/comment`,
|
`${lemmyBetaApiUrl}/comment/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -649,19 +739,16 @@ describe('main', () => {
|
||||||
expect(getPostRes.comments[0].deleted).toBe(true);
|
expect(getPostRes.comments[0].deleted).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta undeletes the comment
|
// lemmy_beta undeletes the comment
|
||||||
let undeleteCommentForm: CommentForm = {
|
let undeleteCommentForm: DeleteCommentForm = {
|
||||||
content: commentContent,
|
|
||||||
edit_id: createCommentRes.comment.id,
|
edit_id: createCommentRes.comment.id,
|
||||||
post_id: createPostRes.post.id,
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
creator_id: createCommentRes.comment.creator_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let undeleteCommentRes: CommentResponse = await fetch(
|
let undeleteCommentRes: CommentResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/comment`,
|
`${lemmyBetaApiUrl}/comment/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -677,23 +764,22 @@ describe('main', () => {
|
||||||
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
|
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
|
||||||
|
|
||||||
// lemmy_beta deletes the post
|
// lemmy_beta deletes the post
|
||||||
let deletePostForm: PostForm = {
|
let deletePostForm: DeletePostForm = {
|
||||||
name: postName,
|
|
||||||
edit_id: createPostRes.post.id,
|
edit_id: createPostRes.post.id,
|
||||||
auth: lemmyBetaAuth,
|
|
||||||
community_id: createPostRes.post.community_id,
|
|
||||||
creator_id: createPostRes.post.creator_id,
|
|
||||||
nsfw: false,
|
|
||||||
deleted: true,
|
deleted: true,
|
||||||
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
|
let deletePostRes: PostResponse = await fetch(
|
||||||
method: 'PUT',
|
`${lemmyBetaApiUrl}/post/delete`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: 'POST',
|
||||||
},
|
headers: {
|
||||||
body: wrapper(deletePostForm),
|
'Content-Type': 'application/json',
|
||||||
}).then(d => d.json());
|
},
|
||||||
|
body: wrapper(deletePostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
expect(deletePostRes.post.deleted).toBe(true);
|
expect(deletePostRes.post.deleted).toBe(true);
|
||||||
|
|
||||||
// Make sure lemmy_alpha sees the post is deleted
|
// Make sure lemmy_alpha sees the post is deleted
|
||||||
|
@ -703,20 +789,16 @@ describe('main', () => {
|
||||||
expect(getPostResAgain.post.deleted).toBe(true);
|
expect(getPostResAgain.post.deleted).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta undeletes the post
|
// lemmy_beta undeletes the post
|
||||||
let undeletePostForm: PostForm = {
|
let undeletePostForm: DeletePostForm = {
|
||||||
name: postName,
|
|
||||||
edit_id: createPostRes.post.id,
|
edit_id: createPostRes.post.id,
|
||||||
auth: lemmyBetaAuth,
|
|
||||||
community_id: createPostRes.post.community_id,
|
|
||||||
creator_id: createPostRes.post.creator_id,
|
|
||||||
nsfw: false,
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let undeletePostRes: PostResponse = await fetch(
|
let undeletePostRes: PostResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/post`,
|
`${lemmyBetaApiUrl}/post/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -732,20 +814,16 @@ describe('main', () => {
|
||||||
expect(getPostResAgainTwo.post.deleted).toBe(false);
|
expect(getPostResAgainTwo.post.deleted).toBe(false);
|
||||||
|
|
||||||
// lemmy_beta deletes the community
|
// lemmy_beta deletes the community
|
||||||
let deleteCommunityForm: CommunityForm = {
|
let deleteCommunityForm: DeleteCommunityForm = {
|
||||||
name: communityName,
|
|
||||||
title: communityName,
|
|
||||||
category_id: 1,
|
|
||||||
edit_id: createCommunityRes.community.id,
|
edit_id: createCommunityRes.community.id,
|
||||||
nsfw: false,
|
|
||||||
deleted: true,
|
deleted: true,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let deleteResponse: CommunityResponse = await fetch(
|
let deleteResponse: CommunityResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/community`,
|
`${lemmyBetaApiUrl}/community/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -765,20 +843,16 @@ describe('main', () => {
|
||||||
expect(getCommunityRes.community.deleted).toBe(true);
|
expect(getCommunityRes.community.deleted).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta undeletes the community
|
// lemmy_beta undeletes the community
|
||||||
let undeleteCommunityForm: CommunityForm = {
|
let undeleteCommunityForm: DeleteCommunityForm = {
|
||||||
name: communityName,
|
|
||||||
title: communityName,
|
|
||||||
category_id: 1,
|
|
||||||
edit_id: createCommunityRes.community.id,
|
edit_id: createCommunityRes.community.id,
|
||||||
nsfw: false,
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let undeleteCommunityRes: CommunityResponse = await fetch(
|
let undeleteCommunityRes: CommunityResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/community`,
|
`${lemmyBetaApiUrl}/community/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -861,7 +935,6 @@ describe('main', () => {
|
||||||
name: postName,
|
name: postName,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
community_id: createCommunityRes.community.id,
|
community_id: createCommunityRes.community.id,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -896,19 +969,16 @@ describe('main', () => {
|
||||||
expect(createCommentRes.comment.content).toBe(commentContent);
|
expect(createCommentRes.comment.content).toBe(commentContent);
|
||||||
|
|
||||||
// lemmy_beta removes the comment
|
// lemmy_beta removes the comment
|
||||||
let removeCommentForm: CommentForm = {
|
let removeCommentForm: RemoveCommentForm = {
|
||||||
content: commentContent,
|
|
||||||
edit_id: createCommentRes.comment.id,
|
edit_id: createCommentRes.comment.id,
|
||||||
post_id: createPostRes.post.id,
|
|
||||||
removed: true,
|
removed: true,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
creator_id: createCommentRes.comment.creator_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let removeCommentRes: CommentResponse = await fetch(
|
let removeCommentRes: CommentResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/comment`,
|
`${lemmyBetaApiUrl}/comment/remove`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -925,19 +995,16 @@ describe('main', () => {
|
||||||
expect(getPostRes.comments[0].removed).toBe(true);
|
expect(getPostRes.comments[0].removed).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta undeletes the comment
|
// lemmy_beta undeletes the comment
|
||||||
let unremoveCommentForm: CommentForm = {
|
let unremoveCommentForm: RemoveCommentForm = {
|
||||||
content: commentContent,
|
|
||||||
edit_id: createCommentRes.comment.id,
|
edit_id: createCommentRes.comment.id,
|
||||||
post_id: createPostRes.post.id,
|
|
||||||
removed: false,
|
removed: false,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
creator_id: createCommentRes.comment.creator_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let unremoveCommentRes: CommentResponse = await fetch(
|
let unremoveCommentRes: CommentResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/comment`,
|
`${lemmyBetaApiUrl}/comment/remove`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -953,23 +1020,22 @@ describe('main', () => {
|
||||||
expect(getPostUnremoveRes.comments[0].removed).toBe(false);
|
expect(getPostUnremoveRes.comments[0].removed).toBe(false);
|
||||||
|
|
||||||
// lemmy_beta deletes the post
|
// lemmy_beta deletes the post
|
||||||
let removePostForm: PostForm = {
|
let removePostForm: RemovePostForm = {
|
||||||
name: postName,
|
|
||||||
edit_id: createPostRes.post.id,
|
edit_id: createPostRes.post.id,
|
||||||
auth: lemmyBetaAuth,
|
|
||||||
community_id: createPostRes.post.community_id,
|
|
||||||
creator_id: createPostRes.post.creator_id,
|
|
||||||
nsfw: false,
|
|
||||||
removed: true,
|
removed: true,
|
||||||
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
|
let removePostRes: PostResponse = await fetch(
|
||||||
method: 'PUT',
|
`${lemmyBetaApiUrl}/post/remove`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: 'POST',
|
||||||
},
|
headers: {
|
||||||
body: wrapper(removePostForm),
|
'Content-Type': 'application/json',
|
||||||
}).then(d => d.json());
|
},
|
||||||
|
body: wrapper(removePostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
expect(removePostRes.post.removed).toBe(true);
|
expect(removePostRes.post.removed).toBe(true);
|
||||||
|
|
||||||
// Make sure lemmy_alpha sees the post is deleted
|
// Make sure lemmy_alpha sees the post is deleted
|
||||||
|
@ -979,20 +1045,16 @@ describe('main', () => {
|
||||||
expect(getPostResAgain.post.removed).toBe(true);
|
expect(getPostResAgain.post.removed).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta unremoves the post
|
// lemmy_beta unremoves the post
|
||||||
let unremovePostForm: PostForm = {
|
let unremovePostForm: RemovePostForm = {
|
||||||
name: postName,
|
|
||||||
edit_id: createPostRes.post.id,
|
edit_id: createPostRes.post.id,
|
||||||
auth: lemmyBetaAuth,
|
|
||||||
community_id: createPostRes.post.community_id,
|
|
||||||
creator_id: createPostRes.post.creator_id,
|
|
||||||
nsfw: false,
|
|
||||||
removed: false,
|
removed: false,
|
||||||
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let unremovePostRes: PostResponse = await fetch(
|
let unremovePostRes: PostResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/post`,
|
`${lemmyBetaApiUrl}/post/remove`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1007,21 +1069,17 @@ describe('main', () => {
|
||||||
}).then(d => d.json());
|
}).then(d => d.json());
|
||||||
expect(getPostResAgainTwo.post.removed).toBe(false);
|
expect(getPostResAgainTwo.post.removed).toBe(false);
|
||||||
|
|
||||||
// lemmy_beta deletes the community
|
// lemmy_beta removes the community
|
||||||
let removeCommunityForm: CommunityForm = {
|
let removeCommunityForm: RemoveCommunityForm = {
|
||||||
name: communityName,
|
|
||||||
title: communityName,
|
|
||||||
category_id: 1,
|
|
||||||
edit_id: createCommunityRes.community.id,
|
edit_id: createCommunityRes.community.id,
|
||||||
nsfw: false,
|
|
||||||
removed: true,
|
removed: true,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let removeCommunityRes: CommunityResponse = await fetch(
|
let removeCommunityRes: CommunityResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/community`,
|
`${lemmyBetaApiUrl}/community/remove`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1029,7 +1087,7 @@ describe('main', () => {
|
||||||
}
|
}
|
||||||
).then(d => d.json());
|
).then(d => d.json());
|
||||||
|
|
||||||
// Make sure the delete went through
|
// Make sure the remove went through
|
||||||
expect(removeCommunityRes.community.removed).toBe(true);
|
expect(removeCommunityRes.community.removed).toBe(true);
|
||||||
|
|
||||||
// Re-get it from alpha, make sure its removed there too
|
// Re-get it from alpha, make sure its removed there too
|
||||||
|
@ -1041,20 +1099,16 @@ describe('main', () => {
|
||||||
expect(getCommunityRes.community.removed).toBe(true);
|
expect(getCommunityRes.community.removed).toBe(true);
|
||||||
|
|
||||||
// lemmy_beta unremoves the community
|
// lemmy_beta unremoves the community
|
||||||
let unremoveCommunityForm: CommunityForm = {
|
let unremoveCommunityForm: RemoveCommunityForm = {
|
||||||
name: communityName,
|
|
||||||
title: communityName,
|
|
||||||
category_id: 1,
|
|
||||||
edit_id: createCommunityRes.community.id,
|
edit_id: createCommunityRes.community.id,
|
||||||
nsfw: false,
|
|
||||||
removed: false,
|
removed: false,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let unremoveCommunityRes: CommunityResponse = await fetch(
|
let unremoveCommunityRes: CommunityResponse = await fetch(
|
||||||
`${lemmyBetaApiUrl}/community`,
|
`${lemmyBetaApiUrl}/community/remove`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1149,16 +1203,16 @@ describe('main', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// lemmy alpha deletes the private message
|
// lemmy alpha deletes the private message
|
||||||
let deletePrivateMessageForm: EditPrivateMessageForm = {
|
let deletePrivateMessageForm: DeletePrivateMessageForm = {
|
||||||
deleted: true,
|
deleted: true,
|
||||||
edit_id: createRes.message.id,
|
edit_id: createRes.message.id,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let deleteRes: PrivateMessageResponse = await fetch(
|
let deleteRes: PrivateMessageResponse = await fetch(
|
||||||
`${lemmyAlphaApiUrl}/private_message`,
|
`${lemmyAlphaApiUrl}/private_message/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1182,16 +1236,16 @@ describe('main', () => {
|
||||||
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
|
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
|
||||||
|
|
||||||
// lemmy alpha undeletes the private message
|
// lemmy alpha undeletes the private message
|
||||||
let undeletePrivateMessageForm: EditPrivateMessageForm = {
|
let undeletePrivateMessageForm: DeletePrivateMessageForm = {
|
||||||
deleted: false,
|
deleted: false,
|
||||||
edit_id: createRes.message.id,
|
edit_id: createRes.message.id,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
let undeleteRes: PrivateMessageResponse = await fetch(
|
let undeleteRes: PrivateMessageResponse = await fetch(
|
||||||
`${lemmyAlphaApiUrl}/private_message`,
|
`${lemmyAlphaApiUrl}/private_message/delete`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
@ -1252,7 +1306,6 @@ describe('main', () => {
|
||||||
name: postName,
|
name: postName,
|
||||||
auth: lemmyAlphaAuth,
|
auth: lemmyAlphaAuth,
|
||||||
community_id: 2,
|
community_id: 2,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1363,7 +1416,6 @@ describe('main', () => {
|
||||||
name: betaPostName,
|
name: betaPostName,
|
||||||
auth: lemmyBetaAuth,
|
auth: lemmyBetaAuth,
|
||||||
community_id: 2,
|
community_id: 2,
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
17
ui/src/components/admin-settings.tsx
vendored
17
ui/src/components/admin-settings.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -80,9 +81,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `${i18n.t('admin_settings')} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -92,7 +102,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
) : (
|
) : (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<SiteForm site={this.state.siteRes.site} />
|
{this.state.siteRes.site.id && (
|
||||||
|
<SiteForm site={this.state.siteRes.site} />
|
||||||
|
)}
|
||||||
{this.admins()}
|
{this.admins()}
|
||||||
{this.bannedUsers()}
|
{this.bannedUsers()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -220,9 +232,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
}
|
}
|
||||||
this.state.siteRes = data;
|
this.state.siteRes = data;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('admin_settings')} - ${
|
|
||||||
this.state.siteRes.site.name
|
|
||||||
}`;
|
|
||||||
} else if (res.op == UserOperation.EditSite) {
|
} else if (res.op == UserOperation.EditSite) {
|
||||||
let data = res.data as SiteResponse;
|
let data = res.data as SiteResponse;
|
||||||
this.state.siteRes.site = data.site;
|
this.state.siteRes.site = data.site;
|
||||||
|
|
45
ui/src/components/comment-form.tsx
vendored
45
ui/src/components/comment-form.tsx
vendored
|
@ -115,34 +115,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFinished(op: UserOperation, data: CommentResponse) {
|
handleCommentSubmit(msg: { val: string; formId: string }) {
|
||||||
let isReply =
|
this.state.commentForm.content = msg.val;
|
||||||
this.props.node !== undefined && data.comment.parent_id !== null;
|
this.state.commentForm.form_id = msg.formId;
|
||||||
let xor =
|
|
||||||
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(data.comment.creator_id == UserService.Instance.user.id &&
|
|
||||||
((op == UserOperation.CreateComment &&
|
|
||||||
// If its a reply, make sure parent child match
|
|
||||||
isReply &&
|
|
||||||
data.comment.parent_id == this.props.node.comment.id) ||
|
|
||||||
// Otherwise, check the XOR of the two
|
|
||||||
(!isReply && xor))) ||
|
|
||||||
// If its a comment edit, only check that its from your user, and that its a
|
|
||||||
// text edit only
|
|
||||||
|
|
||||||
(data.comment.creator_id == UserService.Instance.user.id &&
|
|
||||||
op == UserOperation.EditComment &&
|
|
||||||
data.comment.content)
|
|
||||||
) {
|
|
||||||
this.state.finished = true;
|
|
||||||
this.setState(this.state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCommentSubmit(val: string) {
|
|
||||||
this.state.commentForm.content = val;
|
|
||||||
if (this.props.edit) {
|
if (this.props.edit) {
|
||||||
WebSocketService.Instance.editComment(this.state.commentForm);
|
WebSocketService.Instance.editComment(this.state.commentForm);
|
||||||
} else {
|
} else {
|
||||||
|
@ -160,12 +135,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
|
|
||||||
// Only do the showing and hiding if logged in
|
// Only do the showing and hiding if logged in
|
||||||
if (UserService.Instance.user) {
|
if (UserService.Instance.user) {
|
||||||
if (res.op == UserOperation.CreateComment) {
|
if (
|
||||||
|
res.op == UserOperation.CreateComment ||
|
||||||
|
res.op == UserOperation.EditComment
|
||||||
|
) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
this.handleFinished(res.op, data);
|
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
// This only finishes this form, if the randomly generated form_id matches the one received
|
||||||
let data = res.data as CommentResponse;
|
if (this.state.commentForm.form_id == data.form_id) {
|
||||||
this.handleFinished(res.op, data);
|
this.setState({ finished: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
ui/src/components/comment-node.tsx
vendored
47
ui/src/components/comment-node.tsx
vendored
|
@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
|
||||||
import {
|
import {
|
||||||
CommentNode as CommentNodeI,
|
CommentNode as CommentNodeI,
|
||||||
CommentLikeForm,
|
CommentLikeForm,
|
||||||
CommentForm as CommentFormI,
|
DeleteCommentForm,
|
||||||
EditUserMentionForm,
|
RemoveCommentForm,
|
||||||
|
MarkCommentAsReadForm,
|
||||||
|
MarkUserMentionAsReadForm,
|
||||||
SaveCommentForm,
|
SaveCommentForm,
|
||||||
BanFromCommunityForm,
|
BanFromCommunityForm,
|
||||||
BanUserForm,
|
BanUserForm,
|
||||||
|
@ -62,6 +64,7 @@ interface CommentNodeState {
|
||||||
|
|
||||||
interface CommentNodeProps {
|
interface CommentNodeProps {
|
||||||
node: CommentNodeI;
|
node: CommentNodeI;
|
||||||
|
noBorder?: boolean;
|
||||||
noIndent?: boolean;
|
noIndent?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
|
@ -134,9 +137,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`comment-${node.comment.id}`}
|
id={`comment-${node.comment.id}`}
|
||||||
className={`details comment-node border-top border-light py-2 ${
|
className={`details comment-node py-2 ${
|
||||||
this.isCommentNew ? 'mark' : ''
|
!this.props.noBorder ? 'border-top border-light' : ''
|
||||||
}`}
|
} ${this.isCommentNew ? 'mark' : ''}`}
|
||||||
style={
|
style={
|
||||||
!this.props.noIndent &&
|
!this.props.noIndent &&
|
||||||
this.props.node.comment.parent_id &&
|
this.props.node.comment.parent_id &&
|
||||||
|
@ -202,7 +205,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm text-muted"
|
class="btn text-muted"
|
||||||
onClick={linkEvent(this, this.handleCommentCollapse)}
|
onClick={linkEvent(this, this.handleCommentCollapse)}
|
||||||
>
|
>
|
||||||
{this.state.collapsed ? (
|
{this.state.collapsed ? (
|
||||||
|
@ -218,7 +221,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
{/* This is an expanding spacer for mobile */}
|
{/* This is an expanding spacer for mobile */}
|
||||||
<div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
|
<div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
|
className={`btn p-0 unselectable pointer ${this.scoreColor}`}
|
||||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||||
data-tippy-content={this.pointsTippy}
|
data-tippy-content={this.pointsTippy}
|
||||||
>
|
>
|
||||||
|
@ -848,16 +851,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick(i: CommentNode) {
|
handleDeleteClick(i: CommentNode) {
|
||||||
let deleteForm: CommentFormI = {
|
let deleteForm: DeleteCommentForm = {
|
||||||
content: i.props.node.comment.content,
|
|
||||||
edit_id: i.props.node.comment.id,
|
edit_id: i.props.node.comment.id,
|
||||||
creator_id: i.props.node.comment.creator_id,
|
|
||||||
post_id: i.props.node.comment.post_id,
|
|
||||||
parent_id: i.props.node.comment.parent_id,
|
|
||||||
deleted: !i.props.node.comment.deleted,
|
deleted: !i.props.node.comment.deleted,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(deleteForm);
|
WebSocketService.Instance.deleteComment(deleteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveCommentClick(i: CommentNode) {
|
handleSaveCommentClick(i: CommentNode) {
|
||||||
|
@ -901,7 +900,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
let form: CommentLikeForm = {
|
let form: CommentLikeForm = {
|
||||||
comment_id: i.comment.id,
|
comment_id: i.comment.id,
|
||||||
post_id: i.comment.post_id,
|
|
||||||
score: this.state.my_vote,
|
score: this.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -929,7 +927,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
let form: CommentLikeForm = {
|
let form: CommentLikeForm = {
|
||||||
comment_id: i.comment.id,
|
comment_id: i.comment.id,
|
||||||
post_id: i.comment.post_id,
|
|
||||||
score: this.state.my_vote,
|
score: this.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -950,17 +947,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
handleModRemoveSubmit(i: CommentNode) {
|
handleModRemoveSubmit(i: CommentNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let form: CommentFormI = {
|
let form: RemoveCommentForm = {
|
||||||
content: i.props.node.comment.content,
|
|
||||||
edit_id: i.props.node.comment.id,
|
edit_id: i.props.node.comment.id,
|
||||||
creator_id: i.props.node.comment.creator_id,
|
|
||||||
post_id: i.props.node.comment.post_id,
|
|
||||||
parent_id: i.props.node.comment.parent_id,
|
|
||||||
removed: !i.props.node.comment.removed,
|
removed: !i.props.node.comment.removed,
|
||||||
reason: i.state.removeReason,
|
reason: i.state.removeReason,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.removeComment(form);
|
||||||
|
|
||||||
i.state.showRemoveDialog = false;
|
i.state.showRemoveDialog = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
@ -969,22 +962,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
handleMarkRead(i: CommentNode) {
|
handleMarkRead(i: CommentNode) {
|
||||||
// if it has a user_mention_id field, then its a mention
|
// if it has a user_mention_id field, then its a mention
|
||||||
if (i.props.node.comment.user_mention_id) {
|
if (i.props.node.comment.user_mention_id) {
|
||||||
let form: EditUserMentionForm = {
|
let form: MarkUserMentionAsReadForm = {
|
||||||
user_mention_id: i.props.node.comment.user_mention_id,
|
user_mention_id: i.props.node.comment.user_mention_id,
|
||||||
read: !i.props.node.comment.read,
|
read: !i.props.node.comment.read,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editUserMention(form);
|
WebSocketService.Instance.markUserMentionAsRead(form);
|
||||||
} else {
|
} else {
|
||||||
let form: CommentFormI = {
|
let form: MarkCommentAsReadForm = {
|
||||||
content: i.props.node.comment.content,
|
|
||||||
edit_id: i.props.node.comment.id,
|
edit_id: i.props.node.comment.id,
|
||||||
creator_id: i.props.node.comment.creator_id,
|
|
||||||
post_id: i.props.node.comment.post_id,
|
|
||||||
parent_id: i.props.node.comment.parent_id,
|
|
||||||
read: !i.props.node.comment.read,
|
read: !i.props.node.comment.read,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.markCommentAsRead(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
i.state.readLoading = true;
|
i.state.readLoading = true;
|
||||||
|
|
2
ui/src/components/comment-nodes.tsx
vendored
2
ui/src/components/comment-nodes.tsx
vendored
|
@ -16,6 +16,7 @@ interface CommentNodesProps {
|
||||||
moderators?: Array<CommunityUser>;
|
moderators?: Array<CommunityUser>;
|
||||||
admins?: Array<UserView>;
|
admins?: Array<UserView>;
|
||||||
postCreatorId?: number;
|
postCreatorId?: number;
|
||||||
|
noBorder?: boolean;
|
||||||
noIndent?: boolean;
|
noIndent?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
|
@ -42,6 +43,7 @@ export class CommentNodes extends Component<
|
||||||
<CommentNode
|
<CommentNode
|
||||||
key={node.comment.id}
|
key={node.comment.id}
|
||||||
node={node}
|
node={node}
|
||||||
|
noBorder={this.props.noBorder}
|
||||||
noIndent={this.props.noIndent}
|
noIndent={this.props.noIndent}
|
||||||
viewOnly={this.props.viewOnly}
|
viewOnly={this.props.viewOnly}
|
||||||
locked={this.props.locked}
|
locked={this.props.locked}
|
||||||
|
|
20
ui/src/components/communities.tsx
vendored
20
ui/src/components/communities.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
|
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
|
||||||
|
@ -25,6 +27,7 @@ interface CommunitiesState {
|
||||||
communities: Array<Community>;
|
communities: Array<Community>;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunitiesProps {
|
interface CommunitiesProps {
|
||||||
|
@ -37,6 +40,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
page: getPageFromProps(this.props),
|
page: getPageFromProps(this.props),
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -71,9 +75,18 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('communities')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5 class="">
|
<h5 class="">
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -157,7 +170,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -166,7 +179,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
|
|
||||||
{this.state.communities.length > 0 && (
|
{this.state.communities.length > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -240,7 +253,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('communities')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
ui/src/components/community.tsx
vendored
37
ui/src/components/community.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -174,9 +175,18 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.community.name) {
|
||||||
|
return `/c/${this.state.community.name} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
|
@ -271,7 +281,7 @@ export class Community extends Component<any, State> {
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -279,7 +289,7 @@ export class Community extends Component<any, State> {
|
||||||
)}
|
)}
|
||||||
{this.state.posts.length > 0 && (
|
{this.state.posts.length > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -355,12 +365,14 @@ export class Community extends Component<any, State> {
|
||||||
let data = res.data as GetCommunityResponse;
|
let data = res.data as GetCommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.state.moderators = data.moderators;
|
this.state.moderators = data.moderators;
|
||||||
this.state.admins = data.admins;
|
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.EditCommunity) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditCommunity ||
|
||||||
|
res.op == UserOperation.DeleteCommunity ||
|
||||||
|
res.op == UserOperation.RemoveCommunity
|
||||||
|
) {
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -376,7 +388,13 @@ export class Community extends Component<any, State> {
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
setupTippy();
|
setupTippy();
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditPost ||
|
||||||
|
res.op == UserOperation.DeletePost ||
|
||||||
|
res.op == UserOperation.RemovePost ||
|
||||||
|
res.op == UserOperation.LockPost ||
|
||||||
|
res.op == UserOperation.StickyPost
|
||||||
|
) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
editPostFindRes(data, this.state.posts);
|
editPostFindRes(data, this.state.posts);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -405,7 +423,11 @@ export class Community extends Component<any, State> {
|
||||||
this.state.comments = data.comments;
|
this.state.comments = data.comments;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditComment ||
|
||||||
|
res.op == UserOperation.DeleteComment ||
|
||||||
|
res.op == UserOperation.RemoveComment
|
||||||
|
) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
editCommentRes(data, this.state.comments);
|
editCommentRes(data, this.state.comments);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -428,6 +450,7 @@ export class Community extends Component<any, State> {
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.site = data.site;
|
this.state.site = data.site;
|
||||||
|
this.state.admins = data.admins;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
ui/src/components/create-community.tsx
vendored
33
ui/src/components/create-community.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
|
@ -7,19 +8,33 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { toast, wsJsonToRes } from '../utils';
|
import { toast, wsJsonToRes } from '../utils';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface CreateCommunityState {
|
interface CreateCommunityState {
|
||||||
enableNsfw: boolean;
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: CreateCommunityState = {
|
private emptyState: CreateCommunityState = {
|
||||||
enableNsfw: null,
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -46,15 +61,24 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('create_community')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_community')}</h5>
|
<h5>{i18n.t('create_community')}</h5>
|
||||||
<CommunityForm
|
<CommunityForm
|
||||||
onCreate={this.handleCommunityCreate}
|
onCreate={this.handleCommunityCreate}
|
||||||
enableNsfw={this.state.enableNsfw}
|
enableNsfw={this.state.site.enable_nsfw}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,9 +98,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enableNsfw = data.site.enable_nsfw;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
ui/src/components/create-post.tsx
vendored
13
ui/src/components/create-post.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PostForm } from './post-form';
|
import { PostForm } from './post-form';
|
||||||
|
@ -61,9 +62,18 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('create_post')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_post')}</h5>
|
<h5>{i18n.t('create_post')}</h5>
|
||||||
|
@ -100,7 +110,7 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
return lastLocation.split('/c/')[1];
|
return lastLocation.split('/c/')[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostCreate(id: number) {
|
handlePostCreate(id: number) {
|
||||||
|
@ -117,7 +127,6 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.site = data.site;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
ui/src/components/create-private-message.tsx
vendored
29
ui/src/components/create-private-message.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PrivateMessageForm } from './private-message-form';
|
import { PrivateMessageForm } from './private-message-form';
|
||||||
|
@ -7,15 +8,27 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
PrivateMessageFormParams,
|
PrivateMessageFormParams,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { toast, wsJsonToRes } from '../utils';
|
import { toast, wsJsonToRes } from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
export class CreatePrivateMessage extends Component<any, any> {
|
interface CreatePrivateMessageState {
|
||||||
|
site: Site;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreatePrivateMessage extends Component<
|
||||||
|
any,
|
||||||
|
CreatePrivateMessageState
|
||||||
|
> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
private emptyState: CreatePrivateMessageState = {
|
||||||
|
site: undefined,
|
||||||
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
@ -40,9 +53,18 @@ export class CreatePrivateMessage extends Component<any, any> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('create_private_message')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_private_message')}</h5>
|
<h5>{i18n.t('create_private_message')}</h5>
|
||||||
|
@ -80,9 +102,8 @@ export class CreatePrivateMessage extends Component<any, any> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('create_private_message')} - ${
|
this.state.site = data.site;
|
||||||
data.site.name
|
this.setState(this.state);
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
ui/src/components/data-type-select.tsx
vendored
4
ui/src/components/data-type-select.tsx
vendored
|
@ -35,7 +35,7 @@ export class DataTypeSelect extends Component<
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
<label
|
<label
|
||||||
className={`pointer btn btn-sm btn-secondary
|
className={`pointer btn btn-outline-secondary
|
||||||
${this.state.type_ == DataType.Post && 'active'}
|
${this.state.type_ == DataType.Post && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -48,7 +48,7 @@ export class DataTypeSelect extends Component<
|
||||||
{i18n.t('posts')}
|
{i18n.t('posts')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`pointer btn btn-sm btn-secondary ${
|
className={`pointer btn btn-outline-secondary ${
|
||||||
this.state.type_ == DataType.Comment && 'active'
|
this.state.type_ == DataType.Comment && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
2
ui/src/components/iframely-card.tsx
vendored
2
ui/src/components/iframely-card.tsx
vendored
|
@ -29,7 +29,7 @@ export class IFramelyCard extends Component<
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{post.embed_title && !this.state.expanded && (
|
{post.embed_title && !this.state.expanded && (
|
||||||
<div class="card mt-3 mb-2">
|
<div class="card bg-transparent border-secondary mt-3 mb-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
119
ui/src/components/inbox.tsx
vendored
119
ui/src/components/inbox.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
PrivateMessagesResponse,
|
PrivateMessagesResponse,
|
||||||
PrivateMessageResponse,
|
PrivateMessageResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
|
@ -57,7 +59,7 @@ interface InboxState {
|
||||||
messages: Array<PrivateMessageI>;
|
messages: Array<PrivateMessageI>;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
enableDownvotes: boolean;
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Inbox extends Component<any, InboxState> {
|
export class Inbox extends Component<any, InboxState> {
|
||||||
|
@ -70,7 +72,20 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
messages: [],
|
messages: [],
|
||||||
sort: SortType.New,
|
sort: SortType.New,
|
||||||
page: 1,
|
page: 1,
|
||||||
enableDownvotes: undefined,
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -95,9 +110,20 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `/u/${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
|
||||||
|
this.state.site.name
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h5 class="mb-1">
|
<h5 class="mb-1">
|
||||||
|
@ -147,7 +173,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -160,7 +186,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
{i18n.t('unread')}
|
{i18n.t('unread')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
|
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -180,7 +206,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.messageType == MessageType.All && 'active'}
|
${this.state.messageType == MessageType.All && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -193,7 +219,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
{i18n.t('all')}
|
{i18n.t('all')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.messageType == MessageType.Replies && 'active'}
|
${this.state.messageType == MessageType.Replies && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -206,7 +232,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
{i18n.t('replies')}
|
{i18n.t('replies')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.messageType == MessageType.Mentions && 'active'}
|
${this.state.messageType == MessageType.Mentions && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -219,7 +245,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
{i18n.t('mentions')}
|
{i18n.t('mentions')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.messageType == MessageType.Messages && 'active'}
|
${this.state.messageType == MessageType.Messages && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -269,7 +295,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PrivateMessage privateMessage={i} />
|
<PrivateMessage privateMessage={i} />
|
||||||
|
@ -288,7 +314,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -304,7 +330,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -326,7 +352,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -334,7 +360,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
)}
|
)}
|
||||||
{this.unreadCount() > 0 && (
|
{this.unreadCount() > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -446,27 +472,54 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
let found: PrivateMessageI = this.state.messages.find(
|
let found: PrivateMessageI = this.state.messages.find(
|
||||||
m => m.id === data.message.id
|
m => m.id === data.message.id
|
||||||
);
|
);
|
||||||
found.content = data.message.content;
|
if (found) {
|
||||||
found.updated = data.message.updated;
|
found.content = data.message.content;
|
||||||
found.deleted = data.message.deleted;
|
found.updated = data.message.updated;
|
||||||
// If youre in the unread view, just remove it from the list
|
}
|
||||||
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
|
this.setState(this.state);
|
||||||
this.state.messages = this.state.messages.filter(
|
} else if (res.op == UserOperation.DeletePrivateMessage) {
|
||||||
r => r.id !== data.message.id
|
let data = res.data as PrivateMessageResponse;
|
||||||
);
|
let found: PrivateMessageI = this.state.messages.find(
|
||||||
} else {
|
m => m.id === data.message.id
|
||||||
let found = this.state.messages.find(c => c.id == data.message.id);
|
);
|
||||||
found.read = data.message.read;
|
if (found) {
|
||||||
|
found.deleted = data.message.deleted;
|
||||||
|
found.updated = data.message.updated;
|
||||||
|
}
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
|
||||||
|
let data = res.data as PrivateMessageResponse;
|
||||||
|
let found: PrivateMessageI = this.state.messages.find(
|
||||||
|
m => m.id === data.message.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
found.updated = data.message.updated;
|
||||||
|
|
||||||
|
// If youre in the unread view, just remove it from the list
|
||||||
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
|
||||||
|
this.state.messages = this.state.messages.filter(
|
||||||
|
r => r.id !== data.message.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let found = this.state.messages.find(c => c.id == data.message.id);
|
||||||
|
found.read = data.message.read;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.sendUnreadCount();
|
this.sendUnreadCount();
|
||||||
window.scrollTo(0, 0);
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
setupTippy();
|
|
||||||
} else if (res.op == UserOperation.MarkAllAsRead) {
|
} else if (res.op == UserOperation.MarkAllAsRead) {
|
||||||
// Moved to be instant
|
// Moved to be instant
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditComment ||
|
||||||
|
res.op == UserOperation.DeleteComment ||
|
||||||
|
res.op == UserOperation.RemoveComment
|
||||||
|
) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
editCommentRes(data, this.state.replies);
|
editCommentRes(data, this.state.replies);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.MarkCommentAsRead) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
|
||||||
// If youre in the unread view, just remove it from the list
|
// If youre in the unread view, just remove it from the list
|
||||||
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
|
||||||
|
@ -480,7 +533,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.sendUnreadCount();
|
this.sendUnreadCount();
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
setupTippy();
|
setupTippy();
|
||||||
} else if (res.op == UserOperation.EditUserMention) {
|
} else if (res.op == UserOperation.MarkUserMentionAsRead) {
|
||||||
let data = res.data as UserMentionResponse;
|
let data = res.data as UserMentionResponse;
|
||||||
|
|
||||||
let found = this.state.mentions.find(c => c.id == data.mention.id);
|
let found = this.state.mentions.find(c => c.id == data.mention.id);
|
||||||
|
@ -530,19 +583,13 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enableDownvotes = data.site.enable_downvotes;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
|
|
||||||
'inbox'
|
|
||||||
)} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
sendUnreadCount() {
|
||||||
UserService.Instance.user.unreadCount = this.unreadCount();
|
UserService.Instance.unreadCountSub.next(this.unreadCount());
|
||||||
UserService.Instance.sub.next({
|
|
||||||
user: UserService.Instance.user,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unreadCount(): number {
|
unreadCount(): number {
|
||||||
|
|
4
ui/src/components/listing-type-select.tsx
vendored
4
ui/src/components/listing-type-select.tsx
vendored
|
@ -36,7 +36,7 @@ export class ListingTypeSelect extends Component<
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary
|
className={`btn btn-outline-secondary
|
||||||
${this.state.type_ == ListingType.Subscribed && 'active'}
|
${this.state.type_ == ListingType.Subscribed && 'active'}
|
||||||
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
|
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
|
||||||
`}
|
`}
|
||||||
|
@ -51,7 +51,7 @@ export class ListingTypeSelect extends Component<
|
||||||
{i18n.t('subscribed')}
|
{i18n.t('subscribed')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`pointer btn btn-sm btn-secondary ${
|
className={`pointer btn btn-outline-secondary ${
|
||||||
this.state.type_ == ListingType.All && 'active'
|
this.state.type_ == ListingType.All && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
33
ui/src/components/login.tsx
vendored
33
ui/src/components/login.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
PasswordResetForm,
|
PasswordResetForm,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, validEmail, toast } from '../utils';
|
import { wsJsonToRes, validEmail, toast } from '../utils';
|
||||||
|
@ -19,12 +21,12 @@ interface State {
|
||||||
registerForm: RegisterForm;
|
registerForm: RegisterForm;
|
||||||
loginLoading: boolean;
|
loginLoading: boolean;
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
enable_nsfw: boolean;
|
|
||||||
mathQuestion: {
|
mathQuestion: {
|
||||||
a: number;
|
a: number;
|
||||||
b: number;
|
b: number;
|
||||||
answer: number;
|
answer: number;
|
||||||
};
|
};
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
export class Login extends Component<any, State> {
|
||||||
|
@ -44,12 +46,25 @@ export class Login extends Component<any, State> {
|
||||||
},
|
},
|
||||||
loginLoading: false,
|
loginLoading: false,
|
||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
enable_nsfw: undefined,
|
|
||||||
mathQuestion: {
|
mathQuestion: {
|
||||||
a: Math.floor(Math.random() * 10) + 1,
|
a: Math.floor(Math.random() * 10) + 1,
|
||||||
b: Math.floor(Math.random() * 10) + 1,
|
b: Math.floor(Math.random() * 10) + 1,
|
||||||
answer: undefined,
|
answer: undefined,
|
||||||
},
|
},
|
||||||
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -72,9 +87,18 @@ export class Login extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('login')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
||||||
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
||||||
|
@ -251,7 +275,7 @@ export class Login extends Component<any, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.enable_nsfw && (
|
{this.state.site.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -392,9 +416,8 @@ export class Login extends Component<any, State> {
|
||||||
toast(i18n.t('reset_password_mail_sent'));
|
toast(i18n.t('reset_password_mail_sent'));
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enable_nsfw = data.site.enable_nsfw;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('login')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
43
ui/src/components/main.tsx
vendored
43
ui/src/components/main.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -177,9 +178,18 @@ export class Main extends Component<any, MainState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<main role="main" class="col-12 col-md-8">
|
<main role="main" class="col-12 col-md-8">
|
||||||
{this.posts()}
|
{this.posts()}
|
||||||
|
@ -195,7 +205,7 @@ export class Main extends Component<any, MainState> {
|
||||||
<div>
|
<div>
|
||||||
{!this.state.loading && (
|
{!this.state.loading && (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{this.trendingCommunities()}
|
{this.trendingCommunities()}
|
||||||
{UserService.Instance.user &&
|
{UserService.Instance.user &&
|
||||||
|
@ -226,7 +236,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
class="btn btn-sm btn-secondary btn-block"
|
class="btn btn-secondary btn-block"
|
||||||
to="/create_community"
|
to="/create_community"
|
||||||
>
|
>
|
||||||
{i18n.t('create_a_community')}
|
{i18n.t('create_a_community')}
|
||||||
|
@ -295,7 +305,7 @@ export class Main extends Component<any, MainState> {
|
||||||
siteInfo() {
|
siteInfo() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
|
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
|
||||||
{this.canAdmin && (
|
{this.canAdmin && (
|
||||||
|
@ -315,32 +325,32 @@ export class Main extends Component<any, MainState> {
|
||||||
)}
|
)}
|
||||||
<ul class="my-2 list-inline">
|
<ul class="my-2 list-inline">
|
||||||
{/*
|
{/*
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_online', { count: this.state.siteRes.online })}
|
{i18n.t('number_online', { count: this.state.siteRes.online })}
|
||||||
</li>
|
</li>
|
||||||
*/}
|
*/}
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_users', {
|
{i18n.t('number_of_users', {
|
||||||
count: this.state.siteRes.site.number_of_users,
|
count: this.state.siteRes.site.number_of_users,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_communities', {
|
{i18n.t('number_of_communities', {
|
||||||
count: this.state.siteRes.site.number_of_communities,
|
count: this.state.siteRes.site.number_of_communities,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_posts', {
|
{i18n.t('number_of_posts', {
|
||||||
count: this.state.siteRes.site.number_of_posts,
|
count: this.state.siteRes.site.number_of_posts,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_comments', {
|
{i18n.t('number_of_comments', {
|
||||||
count: this.state.siteRes.site.number_of_comments,
|
count: this.state.siteRes.site.number_of_comments,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="badge badge-secondary" to="/modlog">
|
<Link className="badge badge-light" to="/modlog">
|
||||||
{i18n.t('modlog')}
|
{i18n.t('modlog')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -364,7 +374,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.siteRes.site.description && (
|
{this.state.siteRes.site.description && (
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div
|
<div
|
||||||
className="md-div"
|
className="md-div"
|
||||||
|
@ -381,7 +391,7 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
landing() {
|
landing() {
|
||||||
return (
|
return (
|
||||||
<div class="card border-secondary">
|
<div class="card bg-transparent border-secondary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>
|
<h5>
|
||||||
{i18n.t('powered_by')}
|
{i18n.t('powered_by')}
|
||||||
|
@ -517,7 +527,7 @@ export class Main extends Component<any, MainState> {
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -525,7 +535,7 @@ export class Main extends Component<any, MainState> {
|
||||||
)}
|
)}
|
||||||
{this.state.posts.length > 0 && (
|
{this.state.posts.length > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -627,7 +637,6 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.siteRes.banned = data.banned;
|
this.state.siteRes.banned = data.banned;
|
||||||
this.state.siteRes.online = data.online;
|
this.state.siteRes.online = data.online;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${this.state.siteRes.site.name}`;
|
|
||||||
} else if (res.op == UserOperation.EditSite) {
|
} else if (res.op == UserOperation.EditSite) {
|
||||||
let data = res.data as SiteResponse;
|
let data = res.data as SiteResponse;
|
||||||
this.state.siteRes.site = data.site;
|
this.state.siteRes.site = data.site;
|
||||||
|
@ -702,7 +711,11 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.comments = data.comments;
|
this.state.comments = data.comments;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditComment ||
|
||||||
|
res.op == UserOperation.DeleteComment ||
|
||||||
|
res.op == UserOperation.RemoveComment
|
||||||
|
) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
editCommentRes(data, this.state.comments);
|
editCommentRes(data, this.state.comments);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
|
7
ui/src/components/markdown-textarea.tsx
vendored
7
ui/src/components/markdown-textarea.tsx
vendored
|
@ -21,7 +21,7 @@ interface MarkdownTextAreaProps {
|
||||||
replyType?: boolean;
|
replyType?: boolean;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onSubmit?(val: string): any;
|
onSubmit?(msg: { val: string; formId: string }): any;
|
||||||
onContentChange?(val: string): any;
|
onContentChange?(val: string): any;
|
||||||
onReplyCancel?(): any;
|
onReplyCancel?(): any;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ export class MarkdownTextArea extends Component<
|
||||||
/>
|
/>
|
||||||
{this.state.previewMode && (
|
{this.state.previewMode && (
|
||||||
<div
|
<div
|
||||||
className="card card-body md-div"
|
className="card bg-transparent border-secondary card-body md-div"
|
||||||
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
|
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -391,7 +391,8 @@ export class MarkdownTextArea extends Component<
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.props.onSubmit(i.state.content);
|
let msg = { val: i.state.content, formId: i.formId };
|
||||||
|
i.props.onSubmit(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyCancel(i: MarkdownTextArea) {
|
handleReplyCancel(i: MarkdownTextArea) {
|
||||||
|
|
20
ui/src/components/modlog.tsx
vendored
20
ui/src/components/modlog.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
ModAdd,
|
ModAdd,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
|
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
|
||||||
|
@ -38,6 +40,7 @@ interface ModlogState {
|
||||||
communityId?: number;
|
communityId?: number;
|
||||||
communityName?: string;
|
communityName?: string;
|
||||||
page: number;
|
page: number;
|
||||||
|
site: Site;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
combined: [],
|
combined: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -338,9 +342,18 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `Modlog - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5 class="">
|
<h5 class="">
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -384,14 +397,14 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -434,7 +447,8 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
this.setCombined(data);
|
this.setCombined(data);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `Modlog - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
453
ui/src/components/navbar.tsx
vendored
453
ui/src/components/navbar.tsx
vendored
|
@ -29,8 +29,9 @@ import {
|
||||||
toast,
|
toast,
|
||||||
messageToastify,
|
messageToastify,
|
||||||
md,
|
md,
|
||||||
|
setTheme,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n, i18nextSetup } from '../i18next';
|
||||||
|
|
||||||
interface NavbarState {
|
interface NavbarState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -44,14 +45,16 @@ interface NavbarState {
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
searchParam: string;
|
searchParam: string;
|
||||||
toggleSearch: boolean;
|
toggleSearch: boolean;
|
||||||
|
siteLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, NavbarState> {
|
export class Navbar extends Component<any, NavbarState> {
|
||||||
private wsSub: Subscription;
|
private wsSub: Subscription;
|
||||||
private userSub: Subscription;
|
private userSub: Subscription;
|
||||||
|
private unreadCountSub: Subscription;
|
||||||
private searchTextField: RefObject<HTMLInputElement>;
|
private searchTextField: RefObject<HTMLInputElement>;
|
||||||
emptyState: NavbarState = {
|
emptyState: NavbarState = {
|
||||||
isLoggedIn: UserService.Instance.user !== undefined,
|
isLoggedIn: false,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
replies: [],
|
replies: [],
|
||||||
mentions: [],
|
mentions: [],
|
||||||
|
@ -62,22 +65,13 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
admins: [],
|
admins: [],
|
||||||
searchParam: '',
|
searchParam: '',
|
||||||
toggleSearch: false,
|
toggleSearch: false,
|
||||||
|
siteLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
// Subscribe to user changes
|
|
||||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
|
||||||
this.state.isLoggedIn = user.user !== undefined;
|
|
||||||
if (this.state.isLoggedIn) {
|
|
||||||
this.state.unreadCount = user.user.unreadCount;
|
|
||||||
this.requestNotificationPermission();
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.wsSub = WebSocketService.Instance.subject
|
this.wsSub = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -86,17 +80,30 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.isLoggedIn) {
|
|
||||||
this.requestNotificationPermission();
|
|
||||||
// TODO couldn't get re-logging in to re-fetch unreads
|
|
||||||
this.fetchUnreads();
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSocketService.Instance.getSite();
|
WebSocketService.Instance.getSite();
|
||||||
|
|
||||||
this.searchTextField = createRef();
|
this.searchTextField = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Subscribe to jwt changes
|
||||||
|
this.userSub = UserService.Instance.jwtSub.subscribe(res => {
|
||||||
|
// A login
|
||||||
|
if (res !== undefined) {
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
} else {
|
||||||
|
this.state.isLoggedIn = false;
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
|
this.setState(this.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to unread count changes
|
||||||
|
this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
|
||||||
|
this.setState({ unreadCount: res });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleSearchParam(i: Navbar, event: any) {
|
handleSearchParam(i: Navbar, event: any) {
|
||||||
i.state.searchParam = event.target.value;
|
i.state.searchParam = event.target.value;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
@ -145,183 +152,203 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.wsSub.unsubscribe();
|
this.wsSub.unsubscribe();
|
||||||
this.userSub.unsubscribe();
|
this.userSub.unsubscribe();
|
||||||
|
this.unreadCountSub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
// TODO class active corresponding to current page
|
||||||
navbar() {
|
navbar() {
|
||||||
return (
|
return (
|
||||||
<nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
|
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
||||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
<div class="container">
|
||||||
{this.state.siteName}
|
{!this.state.siteLoading ? (
|
||||||
</Link>
|
<Link title={this.state.version} class="navbar-brand" to="/">
|
||||||
{this.state.isLoggedIn && (
|
{this.state.siteName}
|
||||||
<Link
|
</Link>
|
||||||
class="ml-auto p-0 navbar-toggler nav-link"
|
) : (
|
||||||
to="/inbox"
|
<div class="navbar-item">
|
||||||
title={i18n.t('inbox')}
|
<svg class="icon icon-spinner spin">
|
||||||
>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
<svg class="icon">
|
</svg>
|
||||||
<use xlinkHref="#icon-bell"></use>
|
</div>
|
||||||
</svg>
|
|
||||||
{this.state.unreadCount > 0 && (
|
|
||||||
<span class="ml-1 badge badge-light">
|
|
||||||
{this.state.unreadCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
aria-label="menu"
|
|
||||||
onClick={linkEvent(this, this.expandNavbar)}
|
|
||||||
data-tippy-content={i18n.t('expand_here')}
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
|
|
||||||
>
|
|
||||||
<ul class="navbar-nav my-2 mr-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<Link
|
|
||||||
class="nav-link"
|
|
||||||
to="/communities"
|
|
||||||
title={i18n.t('communities')}
|
|
||||||
>
|
|
||||||
{i18n.t('communities')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<Link
|
|
||||||
class="nav-link"
|
|
||||||
to={{
|
|
||||||
pathname: '/create_post',
|
|
||||||
state: { prevPath: this.currentLocation },
|
|
||||||
}}
|
|
||||||
title={i18n.t('create_post')}
|
|
||||||
>
|
|
||||||
{i18n.t('create_post')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<Link
|
|
||||||
class="nav-link"
|
|
||||||
to="/create_community"
|
|
||||||
title={i18n.t('create_community')}
|
|
||||||
>
|
|
||||||
{i18n.t('create_community')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link
|
|
||||||
class="nav-link"
|
|
||||||
to="/sponsors"
|
|
||||||
title={i18n.t('donate_to_lemmy')}
|
|
||||||
>
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-coffee"></use>
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{!this.context.router.history.location.pathname.match(
|
|
||||||
/^\/search/
|
|
||||||
) && (
|
|
||||||
<form
|
|
||||||
class="form-inline"
|
|
||||||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class={`form-control mr-0 search-input ${
|
|
||||||
this.state.toggleSearch ? 'show-input' : 'hide-input'
|
|
||||||
}`}
|
|
||||||
onInput={linkEvent(this, this.handleSearchParam)}
|
|
||||||
value={this.state.searchParam}
|
|
||||||
ref={this.searchTextField}
|
|
||||||
type="text"
|
|
||||||
placeholder={i18n.t('search')}
|
|
||||||
onBlur={linkEvent(this, this.handleSearchBlur)}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
name="search-btn"
|
|
||||||
onClick={linkEvent(this, this.handleSearchBtn)}
|
|
||||||
class="btn btn-link"
|
|
||||||
style="color: var(--gray)"
|
|
||||||
>
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-search"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
<ul class="navbar-nav my-2">
|
{this.state.isLoggedIn && (
|
||||||
{this.canAdmin && (
|
<Link
|
||||||
<li className="nav-item">
|
class="ml-auto p-0 navbar-toggler nav-link border-0"
|
||||||
<Link
|
to="/inbox"
|
||||||
class="nav-link"
|
title={i18n.t('inbox')}
|
||||||
to={`/admin`}
|
>
|
||||||
title={i18n.t('admin_settings')}
|
<svg class="icon">
|
||||||
>
|
<use xlinkHref="#icon-bell"></use>
|
||||||
<svg class="icon">
|
</svg>
|
||||||
<use xlinkHref="#icon-settings"></use>
|
{this.state.unreadCount > 0 && (
|
||||||
</svg>
|
<span class="ml-1 badge badge-light">
|
||||||
</Link>
|
{this.state.unreadCount}
|
||||||
</li>
|
</span>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</Link>
|
||||||
{this.state.isLoggedIn ? (
|
)}
|
||||||
<>
|
<button
|
||||||
<ul class="navbar-nav my-2">
|
class="navbar-toggler border-0"
|
||||||
<li className="nav-item">
|
type="button"
|
||||||
<Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
|
aria-label="menu"
|
||||||
<svg class="icon">
|
onClick={linkEvent(this, this.expandNavbar)}
|
||||||
<use xlinkHref="#icon-bell"></use>
|
data-tippy-content={i18n.t('expand_here')}
|
||||||
</svg>
|
>
|
||||||
{this.state.unreadCount > 0 && (
|
<span class="navbar-toggler-icon"></span>
|
||||||
<span class="ml-1 badge badge-light">
|
</button>
|
||||||
{this.state.unreadCount}
|
{!this.state.siteLoading && (
|
||||||
</span>
|
<div
|
||||||
)}
|
className={`${
|
||||||
|
!this.state.expanded && 'collapse'
|
||||||
|
} navbar-collapse`}
|
||||||
|
>
|
||||||
|
<ul class="navbar-nav my-2 mr-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to="/communities"
|
||||||
|
title={i18n.t('communities')}
|
||||||
|
>
|
||||||
|
{i18n.t('communities')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to={{
|
||||||
|
pathname: '/create_post',
|
||||||
|
state: { prevPath: this.currentLocation },
|
||||||
|
}}
|
||||||
|
title={i18n.t('create_post')}
|
||||||
|
>
|
||||||
|
{i18n.t('create_post')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to="/create_community"
|
||||||
|
title={i18n.t('create_community')}
|
||||||
|
>
|
||||||
|
{i18n.t('create_community')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link
|
<Link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
to={`/u/${UserService.Instance.user.username}`}
|
to="/sponsors"
|
||||||
title={i18n.t('settings')}
|
title={i18n.t('donate_to_lemmy')}
|
||||||
>
|
>
|
||||||
<span>
|
<svg class="icon">
|
||||||
{UserService.Instance.user.avatar && showAvatars() && (
|
<use xlinkHref="#icon-coffee"></use>
|
||||||
<img
|
</svg>
|
||||||
src={pictrsAvatarThumbnail(
|
|
||||||
UserService.Instance.user.avatar
|
|
||||||
)}
|
|
||||||
height="32"
|
|
||||||
width="32"
|
|
||||||
class="rounded-circle mr-2"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{UserService.Instance.user.username}
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
{!this.context.router.history.location.pathname.match(
|
||||||
) : (
|
/^\/search/
|
||||||
<ul class="navbar-nav my-2">
|
) && (
|
||||||
<li className="nav-item">
|
<form
|
||||||
<Link
|
class="form-inline"
|
||||||
class="nav-link"
|
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||||
to="/login"
|
|
||||||
title={i18n.t('login_sign_up')}
|
|
||||||
>
|
>
|
||||||
{i18n.t('login_sign_up')}
|
<input
|
||||||
</Link>
|
class={`form-control mr-0 search-input ${
|
||||||
</li>
|
this.state.toggleSearch ? 'show-input' : 'hide-input'
|
||||||
</ul>
|
}`}
|
||||||
|
onInput={linkEvent(this, this.handleSearchParam)}
|
||||||
|
value={this.state.searchParam}
|
||||||
|
ref={this.searchTextField}
|
||||||
|
type="text"
|
||||||
|
placeholder={i18n.t('search')}
|
||||||
|
onBlur={linkEvent(this, this.handleSearchBlur)}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
name="search-btn"
|
||||||
|
onClick={linkEvent(this, this.handleSearchBtn)}
|
||||||
|
class="btn btn-link"
|
||||||
|
style="color: var(--gray)"
|
||||||
|
>
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlinkHref="#icon-search"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<ul class="navbar-nav my-2">
|
||||||
|
{this.canAdmin && (
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to={`/admin`}
|
||||||
|
title={i18n.t('admin_settings')}
|
||||||
|
>
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlinkHref="#icon-settings"></use>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
{this.state.isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<ul class="navbar-nav my-2">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to="/inbox"
|
||||||
|
title={i18n.t('inbox')}
|
||||||
|
>
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlinkHref="#icon-bell"></use>
|
||||||
|
</svg>
|
||||||
|
{this.state.unreadCount > 0 && (
|
||||||
|
<span class="ml-1 badge badge-light">
|
||||||
|
{this.state.unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to={`/u/${UserService.Instance.user.name}`}
|
||||||
|
title={i18n.t('settings')}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{UserService.Instance.user.avatar &&
|
||||||
|
showAvatars() && (
|
||||||
|
<img
|
||||||
|
src={pictrsAvatarThumbnail(
|
||||||
|
UserService.Instance.user.avatar
|
||||||
|
)}
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
class="rounded-circle mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{UserService.Instance.user.name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ul class="navbar-nav my-2">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="btn btn-success"
|
||||||
|
to="/login"
|
||||||
|
title={i18n.t('login_sign_up')}
|
||||||
|
>
|
||||||
|
{i18n.t('login_sign_up')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -398,38 +425,53 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
this.state.siteName = data.site.name;
|
this.state.siteName = data.site.name;
|
||||||
this.state.version = data.version;
|
this.state.version = data.version;
|
||||||
this.state.admins = data.admins;
|
this.state.admins = data.admins;
|
||||||
this.setState(this.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The login
|
||||||
|
if (data.my_user) {
|
||||||
|
UserService.Instance.user = data.my_user;
|
||||||
|
// On the first load, check the unreads
|
||||||
|
if (this.state.isLoggedIn == false) {
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
this.fetchUnreads();
|
||||||
|
setTheme(data.my_user.theme, true);
|
||||||
|
}
|
||||||
|
this.state.isLoggedIn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nextSetup();
|
||||||
|
|
||||||
|
this.state.siteLoading = false;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUnreads() {
|
fetchUnreads() {
|
||||||
if (this.state.isLoggedIn) {
|
console.log('Fetching unreads...');
|
||||||
let repliesForm: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
let userMentionsForm: GetUserMentionsForm = {
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.currentLocation !== '/inbox') {
|
if (this.currentLocation !== '/inbox') {
|
||||||
WebSocketService.Instance.getReplies(repliesForm);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,10 +480,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
sendUnreadCount() {
|
||||||
UserService.Instance.user.unreadCount = this.state.unreadCount;
|
UserService.Instance.unreadCountSub.next(this.state.unreadCount);
|
||||||
UserService.Instance.sub.next({
|
|
||||||
user: UserService.Instance.user,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateUnreadCount(): number {
|
calculateUnreadCount(): number {
|
||||||
|
|
16
ui/src/components/password_change.tsx
vendored
16
ui/src/components/password_change.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -7,6 +8,7 @@ import {
|
||||||
PasswordChangeForm,
|
PasswordChangeForm,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
|
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
|
||||||
|
@ -15,6 +17,7 @@ import { i18n } from '../i18next';
|
||||||
interface State {
|
interface State {
|
||||||
passwordChangeForm: PasswordChangeForm;
|
passwordChangeForm: PasswordChangeForm;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PasswordChange extends Component<any, State> {
|
export class PasswordChange extends Component<any, State> {
|
||||||
|
@ -27,6 +30,7 @@ export class PasswordChange extends Component<any, State> {
|
||||||
password_verify: undefined,
|
password_verify: undefined,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -48,9 +52,18 @@ export class PasswordChange extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('password_change')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('password_change')}</h5>
|
<h5>{i18n.t('password_change')}</h5>
|
||||||
|
@ -142,7 +155,8 @@ export class PasswordChange extends Component<any, State> {
|
||||||
this.props.history.push('/');
|
this.props.history.push('/');
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('password_change')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
ui/src/components/post-form.tsx
vendored
4
ui/src/components/post-form.tsx
vendored
|
@ -71,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
auth: null,
|
auth: null,
|
||||||
community_id: null,
|
community_id: null,
|
||||||
creator_id: UserService.Instance.user
|
|
||||||
? UserService.Instance.user.id
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -99,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
name: this.props.post.name,
|
name: this.props.post.name,
|
||||||
community_id: this.props.post.community_id,
|
community_id: this.props.post.community_id,
|
||||||
edit_id: this.props.post.id,
|
edit_id: this.props.post.id,
|
||||||
creator_id: this.props.post.creator_id,
|
|
||||||
url: this.props.post.url,
|
url: this.props.post.url,
|
||||||
nsfw: this.props.post.nsfw,
|
nsfw: this.props.post.nsfw,
|
||||||
auth: null,
|
auth: null,
|
||||||
|
|
74
ui/src/components/post-listing.tsx
vendored
74
ui/src/components/post-listing.tsx
vendored
|
@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
Post,
|
Post,
|
||||||
CreatePostLikeForm,
|
CreatePostLikeForm,
|
||||||
PostForm as PostFormI,
|
DeletePostForm,
|
||||||
|
RemovePostForm,
|
||||||
|
LockPostForm,
|
||||||
|
StickyPostForm,
|
||||||
SavePostForm,
|
SavePostForm,
|
||||||
CommunityUser,
|
CommunityUser,
|
||||||
UserView,
|
UserView,
|
||||||
|
@ -33,7 +36,6 @@ import {
|
||||||
setupTippy,
|
setupTippy,
|
||||||
hostname,
|
hostname,
|
||||||
previewLines,
|
previewLines,
|
||||||
toast,
|
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
|
@ -238,9 +240,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
title={post.url}
|
title={post.url}
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<svg class="icon thumbnail">
|
<div class="thumbnail rounded bg-light d-flex justify-content-center">
|
||||||
<use xlinkHref="#icon-external-link"></use>
|
<svg class="icon d-flex align-items-center">
|
||||||
</svg>
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -251,9 +255,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
to={`/post/${post.id}`}
|
to={`/post/${post.id}`}
|
||||||
title={i18n.t('comments')}
|
title={i18n.t('comments')}
|
||||||
>
|
>
|
||||||
<svg class="icon thumbnail">
|
<div class="thumbnail rounded bg-light d-flex justify-content-center">
|
||||||
<use xlinkHref="#icon-message-square"></use>
|
<svg class="icon d-flex align-items-center">
|
||||||
</svg>
|
<use xlinkHref="#icon-message-square"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -296,7 +302,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!this.state.imageExpanded && (
|
{!this.state.imageExpanded && (
|
||||||
<div class="col-3 col-sm-2 pr-0 mt-1">
|
<div class="col-3 col-sm-2 pr-0">
|
||||||
<div class="position-relative">{this.thumbnail()}</div>
|
<div class="position-relative">{this.thumbnail()}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -560,7 +566,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleSavePostClick)}
|
onClick={linkEvent(this, this.handleSavePostClick)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
post.saved ? i18n.t('unsave') : i18n.t('save')
|
post.saved ? i18n.t('unsave') : i18n.t('save')
|
||||||
|
@ -577,7 +583,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link
|
<Link
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
to={`/create_post${this.crossPostParams}`}
|
to={`/create_post${this.crossPostParams}`}
|
||||||
title={i18n.t('cross_post')}
|
title={i18n.t('cross_post')}
|
||||||
>
|
>
|
||||||
|
@ -592,7 +598,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleEditClick)}
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
data-tippy-content={i18n.t('edit')}
|
data-tippy-content={i18n.t('edit')}
|
||||||
>
|
>
|
||||||
|
@ -603,7 +609,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
!post.deleted
|
!post.deleted
|
||||||
|
@ -626,7 +632,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{!this.state.showAdvanced && this.props.showBody ? (
|
{!this.state.showAdvanced && this.props.showBody ? (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||||
data-tippy-content={i18n.t('more')}
|
data-tippy-content={i18n.t('more')}
|
||||||
>
|
>
|
||||||
|
@ -640,7 +646,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.props.showBody && post.body && (
|
{this.props.showBody && post.body && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleViewSource)}
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
data-tippy-content={i18n.t('view_source')}
|
data-tippy-content={i18n.t('view_source')}
|
||||||
>
|
>
|
||||||
|
@ -658,7 +664,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleModLock)}
|
onClick={linkEvent(this, this.handleModLock)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
post.locked
|
post.locked
|
||||||
|
@ -677,7 +683,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleModSticky)}
|
onClick={linkEvent(this, this.handleModSticky)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
post.stickied
|
post.stickied
|
||||||
|
@ -1114,18 +1120,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick(i: PostListing) {
|
handleDeleteClick(i: PostListing) {
|
||||||
let deleteForm: PostFormI = {
|
let deleteForm: DeletePostForm = {
|
||||||
body: i.props.post.body,
|
|
||||||
community_id: i.props.post.community_id,
|
|
||||||
name: i.props.post.name,
|
|
||||||
url: i.props.post.url,
|
|
||||||
edit_id: i.props.post.id,
|
edit_id: i.props.post.id,
|
||||||
creator_id: i.props.post.creator_id,
|
|
||||||
deleted: !i.props.post.deleted,
|
deleted: !i.props.post.deleted,
|
||||||
nsfw: i.props.post.nsfw,
|
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(deleteForm);
|
WebSocketService.Instance.deletePost(deleteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSavePostClick(i: PostListing) {
|
handleSavePostClick(i: PostListing) {
|
||||||
|
@ -1163,46 +1163,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
handleModRemoveSubmit(i: PostListing) {
|
handleModRemoveSubmit(i: PostListing) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let form: PostFormI = {
|
let form: RemovePostForm = {
|
||||||
name: i.props.post.name,
|
|
||||||
community_id: i.props.post.community_id,
|
|
||||||
edit_id: i.props.post.id,
|
edit_id: i.props.post.id,
|
||||||
creator_id: i.props.post.creator_id,
|
|
||||||
removed: !i.props.post.removed,
|
removed: !i.props.post.removed,
|
||||||
reason: i.state.removeReason,
|
reason: i.state.removeReason,
|
||||||
nsfw: i.props.post.nsfw,
|
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(form);
|
WebSocketService.Instance.removePost(form);
|
||||||
|
|
||||||
i.state.showRemoveDialog = false;
|
i.state.showRemoveDialog = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModLock(i: PostListing) {
|
handleModLock(i: PostListing) {
|
||||||
let form: PostFormI = {
|
let form: LockPostForm = {
|
||||||
name: i.props.post.name,
|
|
||||||
community_id: i.props.post.community_id,
|
|
||||||
edit_id: i.props.post.id,
|
edit_id: i.props.post.id,
|
||||||
creator_id: i.props.post.creator_id,
|
|
||||||
nsfw: i.props.post.nsfw,
|
|
||||||
locked: !i.props.post.locked,
|
locked: !i.props.post.locked,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(form);
|
WebSocketService.Instance.lockPost(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModSticky(i: PostListing) {
|
handleModSticky(i: PostListing) {
|
||||||
let form: PostFormI = {
|
let form: StickyPostForm = {
|
||||||
name: i.props.post.name,
|
|
||||||
community_id: i.props.post.community_id,
|
|
||||||
edit_id: i.props.post.id,
|
edit_id: i.props.post.id,
|
||||||
creator_id: i.props.post.creator_id,
|
|
||||||
nsfw: i.props.post.nsfw,
|
|
||||||
stickied: !i.props.post.stickied,
|
stickied: !i.props.post.stickied,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(form);
|
WebSocketService.Instance.stickyPost(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModBanFromCommunityShow(i: PostListing) {
|
handleModBanFromCommunityShow(i: PostListing) {
|
||||||
|
|
2
ui/src/components/post-listings.tsx
vendored
2
ui/src/components/post-listings.tsx
vendored
|
@ -32,7 +32,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableNsfw={this.props.enableNsfw}
|
||||||
/>
|
/>
|
||||||
<hr class="my-2" />
|
<hr class="my-3" />
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
60
ui/src/components/post.tsx
vendored
60
ui/src/components/post.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -8,7 +9,7 @@ import {
|
||||||
GetPostResponse,
|
GetPostResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
Comment,
|
Comment,
|
||||||
CommentForm as CommentFormI,
|
MarkCommentAsReadForm,
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
CommentSortType,
|
CommentSortType,
|
||||||
CommentViewType,
|
CommentViewType,
|
||||||
|
@ -168,26 +169,30 @@ export class Post extends Component<any, PostState> {
|
||||||
UserService.Instance.user &&
|
UserService.Instance.user &&
|
||||||
UserService.Instance.user.id == parent_user_id
|
UserService.Instance.user.id == parent_user_id
|
||||||
) {
|
) {
|
||||||
let form: CommentFormI = {
|
let form: MarkCommentAsReadForm = {
|
||||||
content: found.content,
|
|
||||||
edit_id: found.id,
|
edit_id: found.id,
|
||||||
creator_id: found.creator_id,
|
|
||||||
post_id: found.post_id,
|
|
||||||
parent_id: found.parent_id,
|
|
||||||
read: true,
|
read: true,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.markCommentAsRead(form);
|
||||||
UserService.Instance.user.unreadCount--;
|
UserService.Instance.unreadCountSub.next(
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.unreadCountSub.value - 1
|
||||||
user: UserService.Instance.user,
|
);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.post) {
|
||||||
|
return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -229,7 +234,7 @@ export class Post extends Component<any, PostState> {
|
||||||
<>
|
<>
|
||||||
<div class="btn-group btn-group-toggle mr-3 mb-2">
|
<div class="btn-group btn-group-toggle mr-3 mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -242,7 +247,7 @@ export class Post extends Component<any, PostState> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentSort === CommentSortType.Top && 'active'
|
this.state.commentSort === CommentSortType.Top && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -255,7 +260,7 @@ export class Post extends Component<any, PostState> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentSort === CommentSortType.New && 'active'
|
this.state.commentSort === CommentSortType.New && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -268,7 +273,7 @@ export class Post extends Component<any, PostState> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentSort === CommentSortType.Old && 'active'
|
this.state.commentSort === CommentSortType.Old && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -283,7 +288,7 @@ export class Post extends Component<any, PostState> {
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-toggle mb-2">
|
<div class="btn-group btn-group-toggle mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentViewType === CommentViewType.Chat && 'active'
|
this.state.commentViewType === CommentViewType.Chat && 'active'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -409,10 +414,8 @@ export class Post extends Component<any, PostState> {
|
||||||
this.state.comments = data.comments;
|
this.state.comments = data.comments;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.state.moderators = data.moderators;
|
this.state.moderators = data.moderators;
|
||||||
this.state.siteRes.admins = data.admins;
|
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
|
|
||||||
|
|
||||||
// Get cross-posts
|
// Get cross-posts
|
||||||
if (this.state.post.url) {
|
if (this.state.post.url) {
|
||||||
|
@ -436,7 +439,11 @@ export class Post extends Component<any, PostState> {
|
||||||
this.state.comments.unshift(data.comment);
|
this.state.comments.unshift(data.comment);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditComment ||
|
||||||
|
res.op == UserOperation.DeleteComment ||
|
||||||
|
res.op == UserOperation.RemoveComment
|
||||||
|
) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
editCommentRes(data, this.state.comments);
|
editCommentRes(data, this.state.comments);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -453,7 +460,13 @@ export class Post extends Component<any, PostState> {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
createPostLikeRes(data, this.state.post);
|
createPostLikeRes(data, this.state.post);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditPost ||
|
||||||
|
res.op == UserOperation.DeletePost ||
|
||||||
|
res.op == UserOperation.RemovePost ||
|
||||||
|
res.op == UserOperation.LockPost ||
|
||||||
|
res.op == UserOperation.StickyPost
|
||||||
|
) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
this.state.post = data.post;
|
this.state.post = data.post;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -463,7 +476,11 @@ export class Post extends Component<any, PostState> {
|
||||||
this.state.post = data.post;
|
this.state.post = data.post;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
setupTippy();
|
setupTippy();
|
||||||
} else if (res.op == UserOperation.EditCommunity) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditCommunity ||
|
||||||
|
res.op == UserOperation.DeleteCommunity ||
|
||||||
|
res.op == UserOperation.RemoveCommunity
|
||||||
|
) {
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.state.post.community_id = data.community.id;
|
this.state.post.community_id = data.community.id;
|
||||||
|
@ -521,7 +538,6 @@ export class Post extends Component<any, PostState> {
|
||||||
let data = res.data as GetCommunityResponse;
|
let data = res.data as GetCommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.state.moderators = data.moderators;
|
this.state.moderators = data.moderators;
|
||||||
this.state.siteRes.admins = data.admins;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
ui/src/components/private-message-form.tsx
vendored
6
ui/src/components/private-message-form.tsx
vendored
|
@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component<
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.EditPrivateMessage) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditPrivateMessage ||
|
||||||
|
res.op == UserOperation.DeletePrivateMessage ||
|
||||||
|
res.op == UserOperation.MarkPrivateMessageAsRead
|
||||||
|
) {
|
||||||
let data = res.data as PrivateMessageResponse;
|
let data = res.data as PrivateMessageResponse;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.props.onEdit(data.message);
|
this.props.onEdit(data.message);
|
||||||
|
|
21
ui/src/components/private-message.tsx
vendored
21
ui/src/components/private-message.tsx
vendored
|
@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import {
|
import {
|
||||||
PrivateMessage as PrivateMessageI,
|
PrivateMessage as PrivateMessageI,
|
||||||
EditPrivateMessageForm,
|
DeletePrivateMessageForm,
|
||||||
|
MarkPrivateMessageAsReadForm,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
||||||
|
@ -130,7 +131,7 @@ export class PrivateMessage extends Component<
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleMarkRead)}
|
onClick={linkEvent(this, this.handleMarkRead)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
message.read
|
message.read
|
||||||
|
@ -149,7 +150,7 @@ export class PrivateMessage extends Component<
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleReplyClick)}
|
onClick={linkEvent(this, this.handleReplyClick)}
|
||||||
data-tippy-content={i18n.t('reply')}
|
data-tippy-content={i18n.t('reply')}
|
||||||
>
|
>
|
||||||
|
@ -164,7 +165,7 @@ export class PrivateMessage extends Component<
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleEditClick)}
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
data-tippy-content={i18n.t('edit')}
|
data-tippy-content={i18n.t('edit')}
|
||||||
>
|
>
|
||||||
|
@ -175,7 +176,7 @@ export class PrivateMessage extends Component<
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
!message.deleted
|
!message.deleted
|
||||||
|
@ -196,7 +197,7 @@ export class PrivateMessage extends Component<
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleViewSource)}
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
data-tippy-content={i18n.t('view_source')}
|
data-tippy-content={i18n.t('view_source')}
|
||||||
>
|
>
|
||||||
|
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick(i: PrivateMessage) {
|
handleDeleteClick(i: PrivateMessage) {
|
||||||
let form: EditPrivateMessageForm = {
|
let form: DeletePrivateMessageForm = {
|
||||||
edit_id: i.props.privateMessage.id,
|
edit_id: i.props.privateMessage.id,
|
||||||
deleted: !i.props.privateMessage.deleted,
|
deleted: !i.props.privateMessage.deleted,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPrivateMessage(form);
|
WebSocketService.Instance.deletePrivateMessage(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyCancel() {
|
handleReplyCancel() {
|
||||||
|
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkRead(i: PrivateMessage) {
|
handleMarkRead(i: PrivateMessage) {
|
||||||
let form: EditPrivateMessageForm = {
|
let form: MarkPrivateMessageAsReadForm = {
|
||||||
edit_id: i.props.privateMessage.id,
|
edit_id: i.props.privateMessage.id,
|
||||||
read: !i.props.privateMessage.read,
|
read: !i.props.privateMessage.read,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPrivateMessage(form);
|
WebSocketService.Instance.markPrivateMessageAsRead(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessageCollapse(i: PrivateMessage) {
|
handleMessageCollapse(i: PrivateMessage) {
|
||||||
|
|
27
ui/src/components/search.tsx
vendored
27
ui/src/components/search.tsx
vendored
|
@ -1,5 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -156,9 +156,24 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
if (this.state.q) {
|
||||||
|
return `${i18n.t('search')} - ${this.state.q} - ${
|
||||||
|
this.state.site.name
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
return `${i18n.t('search')} - ${this.state.site.name}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<h5>{i18n.t('search')}</h5>
|
<h5>{i18n.t('search')}</h5>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.searchForm()}
|
{this.searchForm()}
|
||||||
|
@ -207,7 +222,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.type_}
|
value={this.state.type_}
|
||||||
onChange={linkEvent(this, this.handleTypeChange)}
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
class="custom-select custom-select-sm w-auto"
|
class="custom-select w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('type')}</option>
|
<option disabled>{i18n.t('type')}</option>
|
||||||
<option value={SearchType.All}>{i18n.t('all')}</option>
|
<option value={SearchType.All}>{i18n.t('all')}</option>
|
||||||
|
@ -402,7 +417,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 && (
|
{this.state.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -411,7 +426,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
{this.resultsCount() > 0 && (
|
{this.resultsCount() > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -500,9 +515,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
let data = res.data as SearchResponse;
|
let data = res.data as SearchResponse;
|
||||||
this.state.searchResponse = data;
|
this.state.searchResponse = data;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${i18n.t('search')} - ${this.state.q} - ${
|
|
||||||
this.state.site.name
|
|
||||||
}`;
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
|
@ -517,7 +529,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.site = data.site;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('search')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
ui/src/components/setup.tsx
vendored
6
ui/src/components/setup.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -51,13 +52,14 @@ export class Setup extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
get documentTitle(): string {
|
||||||
document.title = `${i18n.t('setup')} - Lemmy`;
|
return `${i18n.t('setup')} - Lemmy`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 offset-lg-3 col-lg-6">
|
<div class="col-12 offset-lg-3 col-lg-6">
|
||||||
<h3>{i18n.t('lemmy_instance_setup')}</h3>
|
<h3>{i18n.t('lemmy_instance_setup')}</h3>
|
||||||
|
|
43
ui/src/components/sidebar.tsx
vendored
43
ui/src/components/sidebar.tsx
vendored
|
@ -4,7 +4,8 @@ import {
|
||||||
Community,
|
Community,
|
||||||
CommunityUser,
|
CommunityUser,
|
||||||
FollowCommunityForm,
|
FollowCommunityForm,
|
||||||
CommunityForm as CommunityFormI,
|
DeleteCommunityForm,
|
||||||
|
RemoveCommunityForm,
|
||||||
UserView,
|
UserView,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
@ -74,7 +75,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
<span>{community.title}</span>
|
<span>{community.title}</span>
|
||||||
|
@ -176,33 +177,33 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
)}
|
)}
|
||||||
<ul class="my-1 list-inline">
|
<ul class="my-1 list-inline">
|
||||||
{/*
|
{/*
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_online', { count: this.props.online })}
|
{i18n.t('number_online', { count: this.props.online })}
|
||||||
</li>
|
</li>
|
||||||
*/}
|
*/}
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_subscribers', {
|
{i18n.t('number_of_subscribers', {
|
||||||
count: community.number_of_subscribers,
|
count: community.number_of_subscribers,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_posts', {
|
{i18n.t('number_of_posts', {
|
||||||
count: community.number_of_posts,
|
count: community.number_of_posts,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-light">
|
||||||
{i18n.t('number_of_comments', {
|
{i18n.t('number_of_comments', {
|
||||||
count: community.number_of_comments,
|
count: community.number_of_comments,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="badge badge-secondary" to="/communities">
|
<Link className="badge badge-light" to="/communities">
|
||||||
{community.category_name}
|
{community.category_name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link
|
<Link
|
||||||
className="badge badge-secondary"
|
className="badge badge-light"
|
||||||
to={`/modlog/community/${this.props.community.id}`}
|
to={`/modlog/community/${this.props.community.id}`}
|
||||||
>
|
>
|
||||||
{i18n.t('modlog')}
|
{i18n.t('modlog')}
|
||||||
|
@ -227,7 +228,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
</ul>
|
</ul>
|
||||||
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
|
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
|
||||||
<Link
|
<Link
|
||||||
class={`btn btn-sm btn-secondary btn-block mb-3 ${
|
class={`btn btn-secondary btn-block mb-3 ${
|
||||||
(community.deleted || community.removed) && 'no-click'
|
(community.deleted || community.removed) && 'no-click'
|
||||||
}`}
|
}`}
|
||||||
to={`/create_post?community=${community.name}`}
|
to={`/create_post?community=${community.name}`}
|
||||||
|
@ -237,14 +238,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<div>
|
<div>
|
||||||
{community.subscribed ? (
|
{community.subscribed ? (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary btn-block"
|
class="btn btn-secondary btn-block"
|
||||||
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
||||||
>
|
>
|
||||||
{i18n.t('unsubscribe')}
|
{i18n.t('unsubscribe')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary btn-block"
|
class="btn btn-secondary btn-block"
|
||||||
onClick={linkEvent(community.id, this.handleSubscribe)}
|
onClick={linkEvent(community.id, this.handleSubscribe)}
|
||||||
>
|
>
|
||||||
{i18n.t('subscribe')}
|
{i18n.t('subscribe')}
|
||||||
|
@ -254,7 +255,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{community.description && (
|
{community.description && (
|
||||||
<div class="card border-secondary">
|
<div class="card bg-transparent border-secondary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div
|
<div
|
||||||
className="md-div"
|
className="md-div"
|
||||||
|
@ -284,16 +285,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
handleDeleteClick(i: Sidebar) {
|
handleDeleteClick(i: Sidebar) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let deleteForm: CommunityFormI = {
|
let deleteForm: DeleteCommunityForm = {
|
||||||
name: i.props.community.name,
|
|
||||||
title: i.props.community.title,
|
|
||||||
category_id: i.props.community.category_id,
|
|
||||||
edit_id: i.props.community.id,
|
edit_id: i.props.community.id,
|
||||||
deleted: !i.props.community.deleted,
|
deleted: !i.props.community.deleted,
|
||||||
nsfw: i.props.community.nsfw,
|
|
||||||
auth: null,
|
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editCommunity(deleteForm);
|
WebSocketService.Instance.deleteCommunity(deleteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
|
@ -350,18 +346,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
handleModRemoveSubmit(i: Sidebar) {
|
handleModRemoveSubmit(i: Sidebar) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let deleteForm: CommunityFormI = {
|
let removeForm: RemoveCommunityForm = {
|
||||||
name: i.props.community.name,
|
|
||||||
title: i.props.community.title,
|
|
||||||
category_id: i.props.community.category_id,
|
|
||||||
edit_id: i.props.community.id,
|
edit_id: i.props.community.id,
|
||||||
removed: !i.props.community.removed,
|
removed: !i.props.community.removed,
|
||||||
reason: i.state.removeReason,
|
reason: i.state.removeReason,
|
||||||
expires: getUnixTime(i.state.removeExpires),
|
expires: getUnixTime(i.state.removeExpires),
|
||||||
nsfw: i.props.community.nsfw,
|
|
||||||
auth: null,
|
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editCommunity(deleteForm);
|
WebSocketService.Instance.removeCommunity(removeForm);
|
||||||
|
|
||||||
i.state.showRemoveDialog = false;
|
i.state.showRemoveDialog = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
|
2
ui/src/components/sort-select.tsx
vendored
2
ui/src/components/sort-select.tsx
vendored
|
@ -35,7 +35,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.sort}
|
value={this.state.sort}
|
||||||
onChange={linkEvent(this, this.handleSortChange)}
|
onChange={linkEvent(this, this.handleSortChange)}
|
||||||
class="custom-select custom-select-sm w-auto mr-2"
|
class="custom-select w-auto mr-2"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('sort_type')}</option>
|
<option disabled>{i18n.t('sort_type')}</option>
|
||||||
{!this.props.hideHot && (
|
{!this.props.hideHot && (
|
||||||
|
|
31
ui/src/components/sponsors.tsx
vendored
31
ui/src/components/sponsors.tsx
vendored
|
@ -1,9 +1,11 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import {
|
import {
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
@ -17,6 +19,7 @@ interface SilverUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
let general = [
|
let general = [
|
||||||
|
'William Moore',
|
||||||
'Rachel Schmitz',
|
'Rachel Schmitz',
|
||||||
'comradeda',
|
'comradeda',
|
||||||
'ybaumy',
|
'ybaumy',
|
||||||
|
@ -31,7 +34,7 @@ let general = [
|
||||||
'Andre Vallestero',
|
'Andre Vallestero',
|
||||||
'NotTooHighToHack',
|
'NotTooHighToHack',
|
||||||
];
|
];
|
||||||
let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
|
let highlighted = ['DQW', 'DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
|
||||||
let silver: Array<SilverUser> = [
|
let silver: Array<SilverUser> = [
|
||||||
{
|
{
|
||||||
name: 'Redjoker',
|
name: 'Redjoker',
|
||||||
|
@ -41,10 +44,18 @@ let silver: Array<SilverUser> = [
|
||||||
// let gold = [];
|
// let gold = [];
|
||||||
// let latinum = [];
|
// let latinum = [];
|
||||||
|
|
||||||
export class Sponsors extends Component<any, any> {
|
interface SponsorsState {
|
||||||
|
site: Site;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Sponsors extends Component<any, SponsorsState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
private emptyState: SponsorsState = {
|
||||||
|
site: undefined,
|
||||||
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -64,9 +75,18 @@ export class Sponsors extends Component<any, any> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('sponsors')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.topMessage()}
|
{this.topMessage()}
|
||||||
<hr />
|
<hr />
|
||||||
{this.sponsors()}
|
{this.sponsors()}
|
||||||
|
@ -108,7 +128,7 @@ export class Sponsors extends Component<any, any> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h5>{i18n.t('sponsors')}</h5>
|
<h5>{i18n.t('sponsors')}</h5>
|
||||||
<p>{i18n.t('silver_sponsors')}</p>
|
<p>{i18n.t('silver_sponsors')}</p>
|
||||||
<div class="row card-columns">
|
<div class="row justify-content-md-center card-columns">
|
||||||
{silver.map(s => (
|
{silver.map(s => (
|
||||||
<div class="card col-12 col-md-2">
|
<div class="card col-12 col-md-2">
|
||||||
<div>
|
<div>
|
||||||
|
@ -124,7 +144,7 @@ export class Sponsors extends Component<any, any> {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p>{i18n.t('general_sponsors')}</p>
|
<p>{i18n.t('general_sponsors')}</p>
|
||||||
<div class="row card-columns">
|
<div class="row justify-content-md-center card-columns">
|
||||||
{highlighted.map(s => (
|
{highlighted.map(s => (
|
||||||
<div class="card bg-primary col-12 col-md-2 font-weight-bold">
|
<div class="card bg-primary col-12 col-md-2 font-weight-bold">
|
||||||
<div>{s}</div>
|
<div>{s}</div>
|
||||||
|
@ -182,7 +202,8 @@ export class Sponsors extends Component<any, any> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('sponsors')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
85
ui/src/components/user-details.tsx
vendored
85
ui/src/components/user-details.tsx
vendored
|
@ -1,7 +1,7 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take, last } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import {
|
import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
|
@ -16,7 +16,6 @@ import {
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
BanUserResponse,
|
BanUserResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
AddAdminResponse,
|
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import {
|
import {
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
|
@ -41,6 +40,7 @@ interface UserDetailsProps {
|
||||||
enableNsfw: boolean;
|
enableNsfw: boolean;
|
||||||
view: UserDetailsView;
|
view: UserDetailsView;
|
||||||
onPageChange(page: number): number | any;
|
onPageChange(page: number): number | any;
|
||||||
|
admins: Array<UserView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDetailsState {
|
interface UserDetailsState {
|
||||||
|
@ -49,7 +49,6 @@ interface UserDetailsState {
|
||||||
comments: Array<Comment>;
|
comments: Array<Comment>;
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
saved?: Array<Post>;
|
saved?: Array<Post>;
|
||||||
admins: Array<UserView>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
|
@ -63,7 +62,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
comments: [],
|
comments: [],
|
||||||
posts: [],
|
posts: [],
|
||||||
saved: [],
|
saved: [],
|
||||||
admins: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
|
@ -148,25 +146,30 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{combined.map(i => (
|
{combined.map(i => (
|
||||||
<div>
|
<>
|
||||||
{i.type === 'posts' ? (
|
<div>
|
||||||
<PostListing
|
{i.type === 'posts' ? (
|
||||||
post={i.data as Post}
|
<PostListing
|
||||||
admins={this.state.admins}
|
post={i.data as Post}
|
||||||
showCommunity
|
admins={this.props.admins}
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
showCommunity
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
/>
|
enableNsfw={this.props.enableNsfw}
|
||||||
) : (
|
/>
|
||||||
<CommentNodes
|
) : (
|
||||||
nodes={[{ comment: i.data as Comment }]}
|
<CommentNodes
|
||||||
admins={this.state.admins}
|
nodes={[{ comment: i.data as Comment }]}
|
||||||
noIndent
|
admins={this.props.admins}
|
||||||
showContext
|
noBorder
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
noIndent
|
||||||
/>
|
showCommunity
|
||||||
)}
|
showContext
|
||||||
</div>
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<hr class="my-3" />
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -177,8 +180,9 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
<div>
|
<div>
|
||||||
<CommentNodes
|
<CommentNodes
|
||||||
nodes={commentsToFlatNodes(this.state.comments)}
|
nodes={commentsToFlatNodes(this.state.comments)}
|
||||||
admins={this.state.admins}
|
admins={this.props.admins}
|
||||||
noIndent
|
noIndent
|
||||||
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
/>
|
/>
|
||||||
|
@ -190,13 +194,16 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.posts.map(post => (
|
{this.state.posts.map(post => (
|
||||||
<PostListing
|
<>
|
||||||
post={post}
|
<PostListing
|
||||||
admins={this.state.admins}
|
post={post}
|
||||||
showCommunity
|
admins={this.props.admins}
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
showCommunity
|
||||||
enableNsfw={this.props.enableNsfw}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
/>
|
enableNsfw={this.props.enableNsfw}
|
||||||
|
/>
|
||||||
|
<hr class="my-3" />
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -207,7 +214,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.props.page > 1 && (
|
{this.props.page > 1 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
onClick={linkEvent(this, this.prevPage)}
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('prev')}
|
{i18n.t('prev')}
|
||||||
|
@ -215,7 +222,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
)}
|
)}
|
||||||
{this.state.comments.length + this.state.posts.length > 0 && (
|
{this.state.comments.length + this.state.posts.length > 0 && (
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.nextPage)}
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
>
|
>
|
||||||
{i18n.t('next')}
|
{i18n.t('next')}
|
||||||
|
@ -252,7 +259,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
follows: data.follows,
|
follows: data.follows,
|
||||||
moderates: data.moderates,
|
moderates: data.moderates,
|
||||||
posts: data.posts,
|
posts: data.posts,
|
||||||
admins: data.admins,
|
|
||||||
});
|
});
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
const data = res.data as CommentResponse;
|
const data = res.data as CommentResponse;
|
||||||
|
@ -260,7 +266,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
comments: this.state.comments,
|
comments: this.state.comments,
|
||||||
});
|
});
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (
|
||||||
|
res.op == UserOperation.EditComment ||
|
||||||
|
res.op == UserOperation.DeleteComment ||
|
||||||
|
res.op == UserOperation.RemoveComment
|
||||||
|
) {
|
||||||
const data = res.data as CommentResponse;
|
const data = res.data as CommentResponse;
|
||||||
editCommentRes(data, this.state.comments);
|
editCommentRes(data, this.state.comments);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -298,11 +308,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
|
||||||
posts: this.state.posts,
|
posts: this.state.posts,
|
||||||
comments: this.state.comments,
|
comments: this.state.comments,
|
||||||
});
|
});
|
||||||
} else if (res.op == UserOperation.AddAdmin) {
|
|
||||||
const data = res.data as AddAdminResponse;
|
|
||||||
this.setState({
|
|
||||||
admins: data.admins,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
ui/src/components/user-listing.tsx
vendored
5
ui/src/components/user-listing.tsx
vendored
|
@ -43,11 +43,10 @@ export class UserListing extends Component<UserListingProps, any> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link className="text-body font-weight-bold" to={link}>
|
<Link className="text-info" to={link}>
|
||||||
{user.avatar && showAvatars() && (
|
{user.avatar && showAvatars() && (
|
||||||
<img
|
<img
|
||||||
height="32"
|
style="width: 2rem; height: 2rem;"
|
||||||
width="32"
|
|
||||||
src={pictrsAvatarThumbnail(user.avatar)}
|
src={pictrsAvatarThumbnail(user.avatar)}
|
||||||
class="rounded-circle mr-2"
|
class="rounded-circle mr-2"
|
||||||
/>
|
/>
|
||||||
|
|
85
ui/src/components/user.tsx
vendored
85
ui/src/components/user.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -13,9 +14,9 @@ import {
|
||||||
DeleteAccountForm,
|
DeleteAccountForm,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Site,
|
|
||||||
UserDetailsView,
|
UserDetailsView,
|
||||||
UserDetailsResponse,
|
UserDetailsResponse,
|
||||||
|
AddAdminResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
|
@ -54,7 +55,7 @@ interface UserState {
|
||||||
deleteAccountLoading: boolean;
|
deleteAccountLoading: boolean;
|
||||||
deleteAccountShowConfirm: boolean;
|
deleteAccountShowConfirm: boolean;
|
||||||
deleteAccountForm: DeleteAccountForm;
|
deleteAccountForm: DeleteAccountForm;
|
||||||
site: Site;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserProps {
|
interface UserProps {
|
||||||
|
@ -114,19 +115,25 @@ export class User extends Component<any, UserState> {
|
||||||
deleteAccountForm: {
|
deleteAccountForm: {
|
||||||
password: null,
|
password: null,
|
||||||
},
|
},
|
||||||
site: {
|
siteRes: {
|
||||||
id: undefined,
|
admins: [],
|
||||||
name: undefined,
|
banned: [],
|
||||||
creator_id: undefined,
|
online: undefined,
|
||||||
published: undefined,
|
site: {
|
||||||
creator_name: undefined,
|
id: undefined,
|
||||||
number_of_users: undefined,
|
name: undefined,
|
||||||
number_of_posts: undefined,
|
creator_id: undefined,
|
||||||
number_of_comments: undefined,
|
published: undefined,
|
||||||
number_of_communities: undefined,
|
creator_name: undefined,
|
||||||
enable_downvotes: undefined,
|
number_of_users: undefined,
|
||||||
open_registration: undefined,
|
number_of_posts: undefined,
|
||||||
enable_nsfw: undefined,
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
|
version: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -201,13 +208,21 @@ export class User extends Component<any, UserState> {
|
||||||
// Couldnt get a refresh working. This does for now.
|
// Couldnt get a refresh working. This does for now.
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
document.title = `/u/${this.state.username} - ${this.state.site.name}`;
|
|
||||||
setupTippy();
|
setupTippy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<h5>
|
<h5>
|
||||||
|
@ -236,8 +251,9 @@ export class User extends Component<any, UserState> {
|
||||||
sort={SortType[this.state.sort]}
|
sort={SortType[this.state.sort]}
|
||||||
page={this.state.page}
|
page={this.state.page}
|
||||||
limit={fetchLimit}
|
limit={fetchLimit}
|
||||||
enableDownvotes={this.state.site.enable_downvotes}
|
enableDownvotes={this.state.siteRes.site.enable_downvotes}
|
||||||
enableNsfw={this.state.site.enable_nsfw}
|
enableNsfw={this.state.siteRes.site.enable_nsfw}
|
||||||
|
admins={this.state.siteRes.admins}
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
onPageChange={this.handlePageChange}
|
onPageChange={this.handlePageChange}
|
||||||
/>
|
/>
|
||||||
|
@ -260,7 +276,7 @@ export class User extends Component<any, UserState> {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.view == UserDetailsView.Overview && 'active'}
|
${this.state.view == UserDetailsView.Overview && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -273,7 +289,7 @@ export class User extends Component<any, UserState> {
|
||||||
{i18n.t('overview')}
|
{i18n.t('overview')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.view == UserDetailsView.Comments && 'active'}
|
${this.state.view == UserDetailsView.Comments && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -286,7 +302,7 @@ export class User extends Component<any, UserState> {
|
||||||
{i18n.t('comments')}
|
{i18n.t('comments')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.view == UserDetailsView.Posts && 'active'}
|
${this.state.view == UserDetailsView.Posts && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -299,7 +315,7 @@ export class User extends Component<any, UserState> {
|
||||||
{i18n.t('posts')}
|
{i18n.t('posts')}
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.view == UserDetailsView.Saved && 'active'}
|
${this.state.view == UserDetailsView.Saved && 'active'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -344,7 +360,7 @@ export class User extends Component<any, UserState> {
|
||||||
let user = this.state.user;
|
let user = this.state.user;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>
|
<h5>
|
||||||
<ul class="list-inline mb-0">
|
<ul class="list-inline mb-0">
|
||||||
|
@ -441,7 +457,7 @@ export class User extends Component<any, UserState> {
|
||||||
userSettings() {
|
userSettings() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>{i18n.t('settings')}</h5>
|
<h5>{i18n.t('settings')}</h5>
|
||||||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||||
|
@ -453,7 +469,7 @@ export class User extends Component<any, UserState> {
|
||||||
class="pointer ml-4 text-muted small font-weight-bold"
|
class="pointer ml-4 text-muted small font-weight-bold"
|
||||||
>
|
>
|
||||||
{!this.checkSettingsAvatar ? (
|
{!this.checkSettingsAvatar ? (
|
||||||
<span class="btn btn-sm btn-secondary">
|
<span class="btn btn-secondary">
|
||||||
{i18n.t('upload_avatar')}
|
{i18n.t('upload_avatar')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -493,7 +509,7 @@ export class User extends Component<any, UserState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.userSettingsForm.lang}
|
value={this.state.userSettingsForm.lang}
|
||||||
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
|
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
|
||||||
class="ml-2 custom-select custom-select-sm w-auto"
|
class="ml-2 custom-select w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('language')}</option>
|
<option disabled>{i18n.t('language')}</option>
|
||||||
<option value="browser">{i18n.t('browser_default')}</option>
|
<option value="browser">{i18n.t('browser_default')}</option>
|
||||||
|
@ -508,7 +524,7 @@ export class User extends Component<any, UserState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.userSettingsForm.theme}
|
value={this.state.userSettingsForm.theme}
|
||||||
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
|
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
|
||||||
class="ml-2 custom-select custom-select-sm w-auto"
|
class="ml-2 custom-select w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('theme')}</option>
|
<option disabled>{i18n.t('theme')}</option>
|
||||||
{themes.map(theme => (
|
{themes.map(theme => (
|
||||||
|
@ -637,7 +653,7 @@ export class User extends Component<any, UserState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.site.enable_nsfw && (
|
{this.state.siteRes.site.enable_nsfw && (
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
|
@ -769,7 +785,7 @@ export class User extends Component<any, UserState> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.moderates.length > 0 && (
|
{this.state.moderates.length > 0 && (
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>{i18n.t('moderates')}</h5>
|
<h5>{i18n.t('moderates')}</h5>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
|
@ -792,7 +808,7 @@ export class User extends Component<any, UserState> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.follows.length > 0 && (
|
{this.state.follows.length > 0 && (
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>{i18n.t('subscribed')}</h5>
|
<h5>{i18n.t('subscribed')}</h5>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
|
@ -1063,9 +1079,12 @@ export class User extends Component<any, UserState> {
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
const data = res.data as GetSiteResponse;
|
const data = res.data as GetSiteResponse;
|
||||||
this.setState({
|
this.state.siteRes = data;
|
||||||
site: data.site,
|
this.setState(this.state);
|
||||||
});
|
} else if (res.op == UserOperation.AddAdmin) {
|
||||||
|
const data = res.data as AddAdminResponse;
|
||||||
|
this.state.siteRes.admins = data.admins;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
ui/src/i18next.ts
vendored
21
ui/src/i18next.ts
vendored
|
@ -65,15 +65,16 @@ function format(value: any, format: any, lng: any): any {
|
||||||
return format === 'uppercase' ? value.toUpperCase() : value;
|
return format === 'uppercase' ? value.toUpperCase() : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
i18next.init({
|
export function i18nextSetup() {
|
||||||
debug: false,
|
i18next.init({
|
||||||
// load: 'languageOnly',
|
debug: false,
|
||||||
|
// load: 'languageOnly',
|
||||||
// initImmediate: false,
|
|
||||||
lng: getLanguage(),
|
|
||||||
fallbackLng: 'en',
|
|
||||||
resources,
|
|
||||||
interpolation: { format },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// initImmediate: false,
|
||||||
|
lng: getLanguage(),
|
||||||
|
fallbackLng: 'en',
|
||||||
|
resources,
|
||||||
|
interpolation: { format },
|
||||||
|
});
|
||||||
|
}
|
||||||
export { i18next as i18n, resources };
|
export { i18next as i18n, resources };
|
||||||
|
|
154
ui/src/interfaces.ts
vendored
154
ui/src/interfaces.ts
vendored
|
@ -9,19 +9,28 @@ export enum UserOperation {
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
CreateComment,
|
CreateComment,
|
||||||
EditComment,
|
EditComment,
|
||||||
|
DeleteComment,
|
||||||
|
RemoveComment,
|
||||||
|
MarkCommentAsRead,
|
||||||
SaveComment,
|
SaveComment,
|
||||||
CreateCommentLike,
|
CreateCommentLike,
|
||||||
GetPosts,
|
GetPosts,
|
||||||
CreatePostLike,
|
CreatePostLike,
|
||||||
EditPost,
|
EditPost,
|
||||||
|
DeletePost,
|
||||||
|
RemovePost,
|
||||||
|
LockPost,
|
||||||
|
StickyPost,
|
||||||
SavePost,
|
SavePost,
|
||||||
EditCommunity,
|
EditCommunity,
|
||||||
|
DeleteCommunity,
|
||||||
|
RemoveCommunity,
|
||||||
FollowCommunity,
|
FollowCommunity,
|
||||||
GetFollowedCommunities,
|
GetFollowedCommunities,
|
||||||
GetUserDetails,
|
GetUserDetails,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
GetUserMentions,
|
GetUserMentions,
|
||||||
EditUserMention,
|
MarkUserMentionAsRead,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
@ -40,6 +49,8 @@ export enum UserOperation {
|
||||||
PasswordChange,
|
PasswordChange,
|
||||||
CreatePrivateMessage,
|
CreatePrivateMessage,
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
|
DeletePrivateMessage,
|
||||||
|
MarkPrivateMessageAsRead,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
GetComments,
|
GetComments,
|
||||||
|
@ -89,18 +100,33 @@ export enum SearchType {
|
||||||
Url,
|
Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface Claims {
|
||||||
id: number;
|
id: number;
|
||||||
iss: string;
|
iss: string;
|
||||||
username: string;
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
email?: string;
|
||||||
|
avatar?: string;
|
||||||
|
admin: boolean;
|
||||||
|
banned: boolean;
|
||||||
|
published: string;
|
||||||
|
updated?: string;
|
||||||
show_nsfw: boolean;
|
show_nsfw: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
default_sort_type: SortType;
|
default_sort_type: SortType;
|
||||||
default_listing_type: ListingType;
|
default_listing_type: ListingType;
|
||||||
lang: string;
|
lang: string;
|
||||||
avatar?: string;
|
|
||||||
show_avatars: boolean;
|
show_avatars: boolean;
|
||||||
unreadCount?: number;
|
send_notifications_to_email: boolean;
|
||||||
|
matrix_user_id?: string;
|
||||||
|
actor_id: string;
|
||||||
|
bio?: string;
|
||||||
|
local: boolean;
|
||||||
|
last_refreshed_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserView {
|
export interface UserView {
|
||||||
|
@ -355,9 +381,9 @@ export interface GetUserMentionsResponse {
|
||||||
mentions: Array<Comment>;
|
mentions: Array<Comment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditUserMentionForm {
|
export interface MarkUserMentionAsReadForm {
|
||||||
user_mention_id: number;
|
user_mention_id: number;
|
||||||
read?: boolean;
|
read: boolean;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,13 +597,23 @@ export interface UserSettingsForm {
|
||||||
|
|
||||||
export interface CommunityForm {
|
export interface CommunityForm {
|
||||||
name: string;
|
name: string;
|
||||||
|
edit_id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
edit_id?: number;
|
|
||||||
removed?: boolean;
|
|
||||||
deleted?: boolean;
|
|
||||||
nsfw: boolean;
|
nsfw: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCommunityForm {
|
||||||
|
edit_id: number;
|
||||||
|
deleted: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveCommunityForm {
|
||||||
|
edit_id: number;
|
||||||
|
removed: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
expires?: number;
|
expires?: number;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
|
@ -592,7 +628,6 @@ export interface GetCommunityForm {
|
||||||
export interface GetCommunityResponse {
|
export interface GetCommunityResponse {
|
||||||
community: Community;
|
community: Community;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
|
||||||
online: number;
|
online: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,19 +654,37 @@ export interface PostForm {
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
community_id: number;
|
community_id?: number;
|
||||||
updated?: number;
|
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
creator_id: number;
|
|
||||||
removed?: boolean;
|
|
||||||
deleted?: boolean;
|
|
||||||
nsfw: boolean;
|
nsfw: boolean;
|
||||||
locked?: boolean;
|
auth: string;
|
||||||
stickied?: boolean;
|
}
|
||||||
|
|
||||||
|
export interface DeletePostForm {
|
||||||
|
edit_id: number;
|
||||||
|
deleted: boolean;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemovePostForm {
|
||||||
|
edit_id: number;
|
||||||
|
removed: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LockPostForm {
|
||||||
|
edit_id: number;
|
||||||
|
locked: boolean;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StickyPostForm {
|
||||||
|
edit_id: number;
|
||||||
|
stickied: boolean;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PostFormParams {
|
export interface PostFormParams {
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
@ -649,7 +702,6 @@ export interface GetPostResponse {
|
||||||
comments: Array<Comment>;
|
comments: Array<Comment>;
|
||||||
community: Community;
|
community: Community;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
|
||||||
online: number;
|
online: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,14 +717,30 @@ export interface PostResponse {
|
||||||
|
|
||||||
export interface CommentForm {
|
export interface CommentForm {
|
||||||
content: string;
|
content: string;
|
||||||
post_id: number;
|
post_id?: number;
|
||||||
parent_id?: number;
|
parent_id?: number;
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
creator_id?: number;
|
creator_id?: number;
|
||||||
removed?: boolean;
|
form_id?: string;
|
||||||
deleted?: boolean;
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCommentForm {
|
||||||
|
edit_id: number;
|
||||||
|
deleted: boolean;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveCommentForm {
|
||||||
|
edit_id: number;
|
||||||
|
removed: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
read?: boolean;
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkCommentAsReadForm {
|
||||||
|
edit_id: number;
|
||||||
|
read: boolean;
|
||||||
auth: string;
|
auth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,11 +753,11 @@ export interface SaveCommentForm {
|
||||||
export interface CommentResponse {
|
export interface CommentResponse {
|
||||||
comment: Comment;
|
comment: Comment;
|
||||||
recipient_ids: Array<number>;
|
recipient_ids: Array<number>;
|
||||||
|
form_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommentLikeForm {
|
export interface CommentLikeForm {
|
||||||
comment_id: number;
|
comment_id: number;
|
||||||
post_id: number;
|
|
||||||
score: number;
|
score: number;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
@ -744,6 +812,10 @@ export interface GetSiteConfig {
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetSiteForm {
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetSiteConfigResponse {
|
export interface GetSiteConfigResponse {
|
||||||
config_hjson: string;
|
config_hjson: string;
|
||||||
}
|
}
|
||||||
|
@ -759,6 +831,7 @@ export interface GetSiteResponse {
|
||||||
banned: Array<UserView>;
|
banned: Array<UserView>;
|
||||||
online: number;
|
online: number;
|
||||||
version: string;
|
version: string;
|
||||||
|
my_user?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SiteResponse {
|
export interface SiteResponse {
|
||||||
|
@ -835,9 +908,19 @@ export interface PrivateMessageFormParams {
|
||||||
|
|
||||||
export interface EditPrivateMessageForm {
|
export interface EditPrivateMessageForm {
|
||||||
edit_id: number;
|
edit_id: number;
|
||||||
content?: string;
|
content: string;
|
||||||
deleted?: boolean;
|
auth?: string;
|
||||||
read?: boolean;
|
}
|
||||||
|
|
||||||
|
export interface DeletePrivateMessageForm {
|
||||||
|
edit_id: number;
|
||||||
|
deleted: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkPrivateMessageAsReadForm {
|
||||||
|
edit_id: number;
|
||||||
|
read: boolean;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -865,18 +948,26 @@ export interface UserJoinResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageType =
|
export type MessageType =
|
||||||
| EditPrivateMessageForm
|
|
||||||
| LoginForm
|
| LoginForm
|
||||||
| RegisterForm
|
| RegisterForm
|
||||||
| CommunityForm
|
| CommunityForm
|
||||||
|
| DeleteCommunityForm
|
||||||
|
| RemoveCommunityForm
|
||||||
| FollowCommunityForm
|
| FollowCommunityForm
|
||||||
| ListCommunitiesForm
|
| ListCommunitiesForm
|
||||||
| GetFollowedCommunitiesForm
|
| GetFollowedCommunitiesForm
|
||||||
| PostForm
|
| PostForm
|
||||||
|
| DeletePostForm
|
||||||
|
| RemovePostForm
|
||||||
|
| LockPostForm
|
||||||
|
| StickyPostForm
|
||||||
| GetPostForm
|
| GetPostForm
|
||||||
| GetPostsForm
|
| GetPostsForm
|
||||||
| GetCommunityForm
|
| GetCommunityForm
|
||||||
| CommentForm
|
| CommentForm
|
||||||
|
| DeleteCommentForm
|
||||||
|
| RemoveCommentForm
|
||||||
|
| MarkCommentAsReadForm
|
||||||
| CommentLikeForm
|
| CommentLikeForm
|
||||||
| SaveCommentForm
|
| SaveCommentForm
|
||||||
| CreatePostLikeForm
|
| CreatePostLikeForm
|
||||||
|
@ -891,7 +982,7 @@ export type MessageType =
|
||||||
| GetUserDetailsForm
|
| GetUserDetailsForm
|
||||||
| GetRepliesForm
|
| GetRepliesForm
|
||||||
| GetUserMentionsForm
|
| GetUserMentionsForm
|
||||||
| EditUserMentionForm
|
| MarkUserMentionAsReadForm
|
||||||
| GetModlogForm
|
| GetModlogForm
|
||||||
| SiteForm
|
| SiteForm
|
||||||
| SearchForm
|
| SearchForm
|
||||||
|
@ -901,6 +992,8 @@ export type MessageType =
|
||||||
| PasswordChangeForm
|
| PasswordChangeForm
|
||||||
| PrivateMessageForm
|
| PrivateMessageForm
|
||||||
| EditPrivateMessageForm
|
| EditPrivateMessageForm
|
||||||
|
| DeletePrivateMessageForm
|
||||||
|
| MarkPrivateMessageAsReadForm
|
||||||
| GetPrivateMessagesForm
|
| GetPrivateMessagesForm
|
||||||
| SiteConfigForm;
|
| SiteConfigForm;
|
||||||
|
|
||||||
|
@ -925,7 +1018,8 @@ type ResponseType =
|
||||||
| AddAdminResponse
|
| AddAdminResponse
|
||||||
| PrivateMessageResponse
|
| PrivateMessageResponse
|
||||||
| PrivateMessagesResponse
|
| PrivateMessagesResponse
|
||||||
| GetSiteConfigResponse;
|
| GetSiteConfigResponse
|
||||||
|
| GetSiteResponse;
|
||||||
|
|
||||||
export interface WebSocketResponse {
|
export interface WebSocketResponse {
|
||||||
op: UserOperation;
|
op: UserOperation;
|
||||||
|
|
27
ui/src/services/UserService.ts
vendored
27
ui/src/services/UserService.ts
vendored
|
@ -1,20 +1,22 @@
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { User, LoginResponse } from '../interfaces';
|
import { User, Claims, LoginResponse } from '../interfaces';
|
||||||
import { setTheme } from '../utils';
|
import { setTheme } from '../utils';
|
||||||
import jwt_decode from 'jwt-decode';
|
import jwt_decode from 'jwt-decode';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private static _instance: UserService;
|
private static _instance: UserService;
|
||||||
public user: User;
|
public user: User;
|
||||||
public sub: Subject<{ user: User }> = new Subject<{
|
public claims: Claims;
|
||||||
user: User;
|
public jwtSub: Subject<string> = new Subject<string>();
|
||||||
}>();
|
public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
let jwt = Cookies.get('jwt');
|
let jwt = Cookies.get('jwt');
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.setUser(jwt);
|
this.setClaims(jwt);
|
||||||
} else {
|
} else {
|
||||||
setTheme();
|
setTheme();
|
||||||
console.log('No JWT cookie found.');
|
console.log('No JWT cookie found.');
|
||||||
|
@ -22,16 +24,17 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
this.setUser(res.jwt);
|
this.setClaims(res.jwt);
|
||||||
Cookies.set('jwt', res.jwt, { expires: 365 });
|
Cookies.set('jwt', res.jwt, { expires: 365 });
|
||||||
console.log('jwt cookie set');
|
console.log('jwt cookie set');
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout() {
|
public logout() {
|
||||||
|
this.claims = undefined;
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
Cookies.remove('jwt');
|
Cookies.remove('jwt');
|
||||||
setTheme();
|
setTheme();
|
||||||
this.sub.next({ user: undefined });
|
this.jwtSub.next();
|
||||||
console.log('Logged out.');
|
console.log('Logged out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +42,9 @@ export class UserService {
|
||||||
return Cookies.get('jwt');
|
return Cookies.get('jwt');
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUser(jwt: string) {
|
private setClaims(jwt: string) {
|
||||||
this.user = jwt_decode(jwt);
|
this.claims = jwt_decode(jwt);
|
||||||
setTheme(this.user.theme, true);
|
this.jwtSub.next(jwt);
|
||||||
this.sub.next({ user: this.user });
|
|
||||||
console.log(this.user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance() {
|
public static get Instance() {
|
||||||
|
|
120
ui/src/services/WebSocketService.ts
vendored
120
ui/src/services/WebSocketService.ts
vendored
|
@ -4,9 +4,18 @@ import {
|
||||||
RegisterForm,
|
RegisterForm,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
CommunityForm,
|
CommunityForm,
|
||||||
|
DeleteCommunityForm,
|
||||||
|
RemoveCommunityForm,
|
||||||
PostForm,
|
PostForm,
|
||||||
|
DeletePostForm,
|
||||||
|
RemovePostForm,
|
||||||
|
LockPostForm,
|
||||||
|
StickyPostForm,
|
||||||
SavePostForm,
|
SavePostForm,
|
||||||
CommentForm,
|
CommentForm,
|
||||||
|
DeleteCommentForm,
|
||||||
|
RemoveCommentForm,
|
||||||
|
MarkCommentAsReadForm,
|
||||||
SaveCommentForm,
|
SaveCommentForm,
|
||||||
CommentLikeForm,
|
CommentLikeForm,
|
||||||
GetPostForm,
|
GetPostForm,
|
||||||
|
@ -28,7 +37,7 @@ import {
|
||||||
UserView,
|
UserView,
|
||||||
GetRepliesForm,
|
GetRepliesForm,
|
||||||
GetUserMentionsForm,
|
GetUserMentionsForm,
|
||||||
EditUserMentionForm,
|
MarkUserMentionAsReadForm,
|
||||||
SearchForm,
|
SearchForm,
|
||||||
UserSettingsForm,
|
UserSettingsForm,
|
||||||
DeleteAccountForm,
|
DeleteAccountForm,
|
||||||
|
@ -36,10 +45,13 @@ import {
|
||||||
PasswordChangeForm,
|
PasswordChangeForm,
|
||||||
PrivateMessageForm,
|
PrivateMessageForm,
|
||||||
EditPrivateMessageForm,
|
EditPrivateMessageForm,
|
||||||
|
DeletePrivateMessageForm,
|
||||||
|
MarkPrivateMessageAsReadForm,
|
||||||
GetPrivateMessagesForm,
|
GetPrivateMessagesForm,
|
||||||
GetCommentsForm,
|
GetCommentsForm,
|
||||||
UserJoinForm,
|
UserJoinForm,
|
||||||
GetSiteConfig,
|
GetSiteConfig,
|
||||||
|
GetSiteForm,
|
||||||
SiteConfigForm,
|
SiteConfigForm,
|
||||||
MessageType,
|
MessageType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
@ -103,18 +115,24 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createCommunity(communityForm: CommunityForm) {
|
public createCommunity(form: CommunityForm) {
|
||||||
this.setAuth(communityForm);
|
this.setAuth(form);
|
||||||
this.ws.send(
|
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
|
||||||
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public editCommunity(communityForm: CommunityForm) {
|
public editCommunity(form: CommunityForm) {
|
||||||
this.setAuth(communityForm);
|
this.setAuth(form);
|
||||||
this.ws.send(
|
this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form));
|
||||||
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
|
}
|
||||||
);
|
|
||||||
|
public deleteCommunity(form: DeleteCommunityForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.DeleteCommunity, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeCommunity(form: RemoveCommunityForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.RemoveCommunity, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public followCommunity(followCommunityForm: FollowCommunityForm) {
|
public followCommunity(followCommunityForm: FollowCommunityForm) {
|
||||||
|
@ -140,9 +158,9 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
|
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createPost(postForm: PostForm) {
|
public createPost(form: PostForm) {
|
||||||
this.setAuth(postForm);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPost(form: GetPostForm) {
|
public getPost(form: GetPostForm) {
|
||||||
|
@ -155,14 +173,29 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createComment(commentForm: CommentForm) {
|
public createComment(form: CommentForm) {
|
||||||
this.setAuth(commentForm);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public editComment(commentForm: CommentForm) {
|
public editComment(form: CommentForm) {
|
||||||
this.setAuth(commentForm);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteComment(form: DeleteCommentForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.DeleteComment, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeComment(form: RemoveCommentForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.RemoveComment, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public markCommentAsRead(form: MarkCommentAsReadForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.MarkCommentAsRead, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public likeComment(form: CommentLikeForm) {
|
public likeComment(form: CommentLikeForm) {
|
||||||
|
@ -190,9 +223,29 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public editPost(postForm: PostForm) {
|
public editPost(form: PostForm) {
|
||||||
this.setAuth(postForm);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public deletePost(form: DeletePostForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.DeletePost, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public removePost(form: RemovePostForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.RemovePost, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public lockPost(form: LockPostForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.LockPost, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public stickyPost(form: StickyPostForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.StickyPost, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public savePost(form: SavePostForm) {
|
public savePost(form: SavePostForm) {
|
||||||
|
@ -245,9 +298,9 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public editUserMention(form: EditUserMentionForm) {
|
public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getModlog(form: GetModlogForm) {
|
public getModlog(form: GetModlogForm) {
|
||||||
|
@ -264,8 +317,9 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSite() {
|
public getSite(form: GetSiteForm = {}) {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
|
this.setAuth(form, false);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSiteConfig() {
|
public getSiteConfig() {
|
||||||
|
@ -315,6 +369,18 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deletePrivateMessage(form: DeletePrivateMessageForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.ws.send(
|
||||||
|
this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getPrivateMessages(form: GetPrivateMessagesForm) {
|
public getPrivateMessages(form: GetPrivateMessagesForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
|
||||||
|
|
1
ui/translations/en.json
vendored
1
ui/translations/en.json
vendored
|
@ -256,6 +256,7 @@
|
||||||
"couldnt_save_post": "Couldn't save post.",
|
"couldnt_save_post": "Couldn't save post.",
|
||||||
"no_slurs": "No slurs.",
|
"no_slurs": "No slurs.",
|
||||||
"not_an_admin": "Not an admin.",
|
"not_an_admin": "Not an admin.",
|
||||||
|
"not_a_moderator": "Not a moderator.",
|
||||||
"site_already_exists": "Site already exists.",
|
"site_already_exists": "Site already exists.",
|
||||||
"couldnt_update_site": "Couldn't update site.",
|
"couldnt_update_site": "Couldn't update site.",
|
||||||
"couldnt_find_that_username_or_email":
|
"couldnt_find_that_username_or_email":
|
||||||
|
|
42
ui/translations/eu.json
vendored
42
ui/translations/eu.json
vendored
|
@ -46,7 +46,7 @@
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"chat": "Txata",
|
"chat": "Txata",
|
||||||
"your_site": "zure gunea",
|
"your_site": "zure gunea",
|
||||||
"nsfw": "NSFW (eduki hunkigarria)",
|
"nsfw": "NSFW (eduki hunkigarriak)",
|
||||||
"block_leaving": "Ziur al zaude atera nahi duzula?",
|
"block_leaving": "Ziur al zaude atera nahi duzula?",
|
||||||
"bitcoin": "Bitcoin",
|
"bitcoin": "Bitcoin",
|
||||||
"ethereum": "Ethereum",
|
"ethereum": "Ethereum",
|
||||||
|
@ -117,14 +117,14 @@
|
||||||
"remove_community": "Ezabatu komunitatea",
|
"remove_community": "Ezabatu komunitatea",
|
||||||
"subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
|
"subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
|
||||||
"trending_communities": "<1>Komunitateen</1> joerak",
|
"trending_communities": "<1>Komunitateen</1> joerak",
|
||||||
"list_of_communities": "Komunitate-zerrenda",
|
"list_of_communities": "Komunitateen zerrenda",
|
||||||
"community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
|
"community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
|
||||||
"create_private_message": "Sortu mezu pribatua",
|
"create_private_message": "Sortu mezu pribatua",
|
||||||
"cancel": "Ezeztatu",
|
"cancel": "Ezeztatu",
|
||||||
"stickied": "finkatuta",
|
"stickied": "finkatuta",
|
||||||
"reason": "Arrazoia",
|
"reason": "Arrazoia",
|
||||||
"mark_as_read": "markatu irakurrita gisa",
|
"mark_as_read": "markatu irakurrita gisa",
|
||||||
"deleted": "sortzaileak ezabatua",
|
"deleted": "sortzaileak ezabatu du",
|
||||||
"delete_account_confirm": "Abisua: honek zure datu guztiak betirako ezabatu ditu. Sartu zure pasahitza baieztatzeko.",
|
"delete_account_confirm": "Abisua: honek zure datu guztiak betirako ezabatu ditu. Sartu zure pasahitza baieztatzeko.",
|
||||||
"restore": "leheneratu",
|
"restore": "leheneratu",
|
||||||
"unban_from_site": "kendu debekua gunean",
|
"unban_from_site": "kendu debekua gunean",
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
"reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
|
"reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
|
||||||
"no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
|
"no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
|
||||||
"matrix_user_id": "Matrix erabiltzailea",
|
"matrix_user_id": "Matrix erabiltzailea",
|
||||||
"private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1>en mezu seguruak trukatzeko.",
|
"private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Element.io</1>n mezu seguruak trukatzeko.",
|
||||||
"send_notifications_to_email": "Bidali jakinarazpenak epostara",
|
"send_notifications_to_email": "Bidali jakinarazpenak epostara",
|
||||||
"optional": "Hautazkoa",
|
"optional": "Hautazkoa",
|
||||||
"browser_default": "Nabigatzaileko lehenetsia",
|
"browser_default": "Nabigatzaileko lehenetsia",
|
||||||
|
@ -180,7 +180,7 @@
|
||||||
"number_of_upvotes_plural": "{{count}} aldeko bozka",
|
"number_of_upvotes_plural": "{{count}} aldeko bozka",
|
||||||
"open_registration": "Izen-ematea irekia",
|
"open_registration": "Izen-ematea irekia",
|
||||||
"registration_closed": "Izen-ematea itxira",
|
"registration_closed": "Izen-ematea itxira",
|
||||||
"enable_nsfw": "Gaitu NSFW (eduki hunkigarria)",
|
"enable_nsfw": "Gaitu NSFW (eduki hunkigarriak)",
|
||||||
"body": "Gorputza",
|
"body": "Gorputza",
|
||||||
"copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
|
"copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
|
||||||
"community": "Komunitatea",
|
"community": "Komunitatea",
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
"lemmy_instance_setup": "Lemmy instantziaren ezarpena",
|
"lemmy_instance_setup": "Lemmy instantziaren ezarpena",
|
||||||
"setup_admin": "Ezarri gunearen administratzailea",
|
"setup_admin": "Ezarri gunearen administratzailea",
|
||||||
"modified": "aldatuta",
|
"modified": "aldatuta",
|
||||||
"show_nsfw": "Erakutsi eduki hunkigarria (NSFW)",
|
"show_nsfw": "Erakutsi eduki hunkigarriak (NSFW)",
|
||||||
"expires": "Noiz iraungitzen da:",
|
"expires": "Noiz iraungitzen da:",
|
||||||
"theme": "Itxura",
|
"theme": "Itxura",
|
||||||
"sponsors": "Babesleak",
|
"sponsors": "Babesleak",
|
||||||
|
@ -204,19 +204,19 @@
|
||||||
"support_on_open_collective": "OpenCollective bitartez lagundu",
|
"support_on_open_collective": "OpenCollective bitartez lagundu",
|
||||||
"donate_to_lemmy": "Egin dohaintza bat Lemmyri",
|
"donate_to_lemmy": "Egin dohaintza bat Lemmyri",
|
||||||
"donate": "Dohaintza egin",
|
"donate": "Dohaintza egin",
|
||||||
"general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman zizkiotenak dira.",
|
"general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman dizkiotenak dira.",
|
||||||
"silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman zizkiotenak dira.",
|
"silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman dizkiotenak dira.",
|
||||||
"crypto": "Kriptomonetak",
|
"crypto": "Kriptomonetak",
|
||||||
"code": "Kodea",
|
"code": "Kodea",
|
||||||
"joined": "Batuta",
|
"joined": "Batuta",
|
||||||
"by": "egilea",
|
"by": "egilea:",
|
||||||
"to": "nori",
|
"to": "non:",
|
||||||
"from": "nork",
|
"from": "nork",
|
||||||
"transfer_community": "transferentzia-komunitatea",
|
"transfer_community": "transferentzia-komunitatea",
|
||||||
"transfer_site": "transferentzia-gunea",
|
"transfer_site": "transferentzia-gunea",
|
||||||
"are_you_sure": "ziur al zaude?",
|
"are_you_sure": "ziur al zaude?",
|
||||||
"powered_by": "Egilea",
|
"powered_by": "Egilea:",
|
||||||
"landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina.",
|
"landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina. <14></14> <15>Eskerrak ematen dizkiegu gure laguntzaileei: </15> dessalines, Nutomic, asonix, zacanger eta iav.",
|
||||||
"logged_in": "Saioa hasi duzu.",
|
"logged_in": "Saioa hasi duzu.",
|
||||||
"not_logged_in": "Ez duzu saiorik hasi.",
|
"not_logged_in": "Ez duzu saiorik hasi.",
|
||||||
"site_saved": "Gunea gorde da.",
|
"site_saved": "Gunea gorde da.",
|
||||||
|
@ -257,5 +257,21 @@
|
||||||
"couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
|
"couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
|
||||||
"emoji_picker": "Emoji hautagailua",
|
"emoji_picker": "Emoji hautagailua",
|
||||||
"invalid_username": "Erabiltzaile-izen baliogabea.",
|
"invalid_username": "Erabiltzaile-izen baliogabea.",
|
||||||
"what_is": "Zer da"
|
"what_is": "Zenbat da",
|
||||||
|
"bold": "lodia",
|
||||||
|
"italic": "etzana",
|
||||||
|
"subscript": "Azpi-indizea",
|
||||||
|
"superscript": "Goi-indizea",
|
||||||
|
"header": "goiburua",
|
||||||
|
"quote": "aipua",
|
||||||
|
"strikethrough": "marratua",
|
||||||
|
"list": "zerrenda",
|
||||||
|
"spoiler": "spoiler",
|
||||||
|
"not_a_moderator": "Ez zara moderatzailea.",
|
||||||
|
"invalid_url": "URL baliogabea.",
|
||||||
|
"must_login": "<1>Saioa hasi edo izena eman</1> behar duzu iruzkinak egiteko.",
|
||||||
|
"no_password_reset": "Ezingo duzu zure pasahitza berrezarri epostarik ez baduzu.",
|
||||||
|
"invalid_post_title": "Bidalketa izenburu baliogabea",
|
||||||
|
"cake_day_title": "Urtebetetze eguna:",
|
||||||
|
"cake_day_info": "{{ creator_name }}(e)ren urtebetetzea da gaur!"
|
||||||
}
|
}
|
||||||
|
|
5
ui/translations/pt_BR.json
vendored
5
ui/translations/pt_BR.json
vendored
|
@ -111,7 +111,7 @@
|
||||||
"all": "Tudo",
|
"all": "Tudo",
|
||||||
"top": "Top",
|
"top": "Top",
|
||||||
"api": "API",
|
"api": "API",
|
||||||
"docs": "Docs",
|
"docs": "Documentação",
|
||||||
"inbox": "Caixa de entrada",
|
"inbox": "Caixa de entrada",
|
||||||
"inbox_for": "Caixa de entrada de <1>{{user}}</1>",
|
"inbox_for": "Caixa de entrada de <1>{{user}}</1>",
|
||||||
"mark_all_as_read": "marcar tudo como lido",
|
"mark_all_as_read": "marcar tudo como lido",
|
||||||
|
@ -261,5 +261,6 @@
|
||||||
"no_password_reset": "Você não conseguirá redefinir sua senha sem um e-mail.",
|
"no_password_reset": "Você não conseguirá redefinir sua senha sem um e-mail.",
|
||||||
"invalid_post_title": "Título de publicação inválido",
|
"invalid_post_title": "Título de publicação inválido",
|
||||||
"cake_day_info": "Hoje é o dia do bolo de {{ creator_name }}!",
|
"cake_day_info": "Hoje é o dia do bolo de {{ creator_name }}!",
|
||||||
"cake_day_title": "Dia do bolo:"
|
"cake_day_title": "Dia do bolo:",
|
||||||
|
"what_is": "Quanto é"
|
||||||
}
|
}
|
||||||
|
|
5
ui/translations/ru.json
vendored
5
ui/translations/ru.json
vendored
|
@ -266,5 +266,8 @@
|
||||||
"emoji_picker": "Сборщик эмодзи",
|
"emoji_picker": "Сборщик эмодзи",
|
||||||
"select_a_community": "Выбрать сообщество",
|
"select_a_community": "Выбрать сообщество",
|
||||||
"invalid_username": "Неверное имя пользователя.",
|
"invalid_username": "Неверное имя пользователя.",
|
||||||
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать."
|
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
|
||||||
|
"no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
|
||||||
|
"cake_day_title": "День торта:",
|
||||||
|
"what_is": "Что такое"
|
||||||
}
|
}
|
||||||
|
|
15
ui/translations/sv.json
vendored
15
ui/translations/sv.json
vendored
|
@ -224,7 +224,7 @@
|
||||||
"no_email_setup": "Denna server har inte satt upp e-post korrekt.",
|
"no_email_setup": "Denna server har inte satt upp e-post korrekt.",
|
||||||
"matrix_user_id": "Matrix-användare",
|
"matrix_user_id": "Matrix-användare",
|
||||||
"show_context": "Visa sammanhang",
|
"show_context": "Visa sammanhang",
|
||||||
"private_message_disclaimer": "Varning: Privata meddelanden på Lemmy är inte säkra. Vänligen skapa ett konto på <1>Riot.im</1> för att skicka säkra meddelanden.",
|
"private_message_disclaimer": "Varning: Privata meddelanden på Lemmy är inte säkra. Vänligen skapa ett konto på <1>Element.io</1> för att skicka säkra meddelanden.",
|
||||||
"send_notifications_to_email": "Skicka aviseringar till e-postadress",
|
"send_notifications_to_email": "Skicka aviseringar till e-postadress",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"browser_default": "Webbläsarens språk",
|
"browser_default": "Webbläsarens språk",
|
||||||
|
@ -262,5 +262,16 @@
|
||||||
"no_password_reset": "Du kommer inte kunna återställa ditt lösenord utan en e-postadress.",
|
"no_password_reset": "Du kommer inte kunna återställa ditt lösenord utan en e-postadress.",
|
||||||
"what_is": "Vad är",
|
"what_is": "Vad är",
|
||||||
"cake_day_title": "Tårtdag:",
|
"cake_day_title": "Tårtdag:",
|
||||||
"invalid_post_title": "Ogiltig inläggstitel"
|
"invalid_post_title": "Ogiltig inläggstitel",
|
||||||
|
"bold": "fetstil",
|
||||||
|
"italic": "kursiv stil",
|
||||||
|
"header": "rubrik",
|
||||||
|
"quote": "citat",
|
||||||
|
"subscript": "nedsänkt (indexläge)",
|
||||||
|
"superscript": "upphöjt (exponentläge)",
|
||||||
|
"strikethrough": "genomstruket",
|
||||||
|
"spoiler": "innehållsvarning",
|
||||||
|
"list": "lista",
|
||||||
|
"not_a_moderator": "Inte en moderator.",
|
||||||
|
"invalid_url": "Ogiltig URL."
|
||||||
}
|
}
|
||||||
|
|
5596
ui/yarn.lock
vendored
5596
ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue