Merge branch 'main' into inbox-refactoring-merge

This commit is contained in:
Dessalines 2020-07-28 12:08:28 -04:00
commit e605d58888
97 changed files with 7530 additions and 3488 deletions

59
.travis.yml vendored
View file

@ -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
View file

@ -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
View file

@ -1 +1 @@
v0.7.26 v0.7.30

40
docker/prod/deploy.sh vendored
View file

@ -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

View file

@ -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
View 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
View 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
View 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/

View file

@ -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

View file

@ -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
View file

@ -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",

View file

@ -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"

View file

@ -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)]

View file

@ -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)]

View file

@ -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)
} }

View file

@ -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"));
}
} }

View file

@ -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 {

View file

@ -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);
} }
} }

View file

@ -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)
} }

View file

@ -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::{

View file

@ -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,

View file

@ -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"] }

View file

@ -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"));

View file

@ -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

View file

@ -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)

View file

@ -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!(

View file

@ -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,
});
}
}

View file

@ -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(())
}

View file

@ -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;

View file

@ -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()?;

View 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,
}); });
} }

View file

@ -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(())
} }
} }

View file

@ -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),
} }
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

@ -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();

View file

@ -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,

View file

@ -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?;

View file

@ -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",

View file

@ -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::*;

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.26"; pub const VERSION: &str = "v0.7.30";

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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;

File diff suppressed because one or more lines are too long

21
ui/package.json vendored
View file

@ -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",

View file

@ -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,
}; };

View file

@ -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;

View file

@ -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 });
}
} }
} }
} }

View file

@ -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;

View file

@ -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}

View file

@ -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);
} }
} }
} }

View file

@ -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);
} }
} }

View file

@ -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}`;
} }
} }
} }

View file

@ -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}`;
} }
} }
} }

View file

@ -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);
}`;
} }
} }
} }

View file

@ -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'
}`} }`}
> >

View file

@ -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">

View file

@ -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 {

View file

@ -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'
}`} }`}
> >

View file

@ -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}`;
} }
} }
} }

View file

@ -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);

View file

@ -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) {

View file

@ -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);
} }
} }
} }

View file

@ -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 {

View file

@ -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);
} }
} }
} }

View file

@ -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,

View file

@ -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) {

View file

@ -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" />
</> </>
)) ))
) : ( ) : (

View file

@ -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);
} }
} }

View file

@ -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);

View file

@ -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) {

View file

@ -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}`;
} }
} }
} }

View file

@ -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>

View file

@ -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);

View file

@ -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 && (

View file

@ -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);
} }
} }
} }

View file

@ -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,
});
} }
} }
} }

View file

@ -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"
/> />

View file

@ -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
View file

@ -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
View file

@ -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;

View file

@ -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() {

View file

@ -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));

View file

@ -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":

View file

@ -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!"
} }

View file

@ -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 é"
} }

View file

@ -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": "Что такое"
} }

View file

@ -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

File diff suppressed because it is too large Load diff