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
rust:
- stable
matrix:
allow_failures:
- rust: nightly
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
sudo: required
language: node_js
node_js:
- 14
services:
- docker
env:
matrix:
- DOCKER_COMPOSE_VERSION=1.25.5
global:
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- RUST_TEST_THREADS=1
addons:
postgresql: "9.4"
- secure: nzmFoTxPn7OT+qcTULezSCT6B44j/q8RxERBQSr1FVXaCcDrBr6q9ewhGy7BHWP74r4qbif4m9r3sNELZCoFYFP3JwLnrZfX/xUwU8p61eFD2PMOJAdOywDxb94SvooOSnjBmxNvRsuqf6Zmnw378mbsSVCi9Xbx9jpoV4Jq8zKgO0M8WIl/lj2dijD95WIMrHcorbzKS3+2zW3LkPiC2bnfDAUmUDfaCj1gh9FCvzZMtrSxu7kxAeFCkR16TJUciIcGgag8rLHfxwG0h2uEJJ+3/62qCWUdgnj171oTE4ZRi0hdvt2HOY5wjHfS2y1ZxWYgo31uws3pyoTNeQZi0o7Q9Xe/4JXYZXvDfuscSZ9RiuhAstCVswtXPJJVVJQ9cdl5eX1TI0bz8eVRvRy4p40OIBjKiobkmRjl8sXjFbpYAIvFr+TgSa/K/bxm3POfI0B8bIHI85zFxUMrWt5i2IJ0dWvDNHrz+CWWKn1vVFYbBNPgDDHtE0P3LWLEioWFf+ULycjW8DefWc+b63Lf9SSaEE7FnX2mc+BaHCgubCDkJy9Au4xP8zQlJjgZwOdTedw5jvmwz3fqMZBpHypVUXzZs7cRhMWtQ7TAoGb8TOqXNgPEVW+BARNXl0wAamTgjt9v20x0wkp+/SLJwMNY+zvwmzxzd5R9TPgDOqyIRTU=
- secure: ALZqC4OYV315P7EZyk+c/PLJdneeU7jMC30TTzMcX3hospIu7naWekZ+HUnziFDQKZxIHWKZsq1R52DWhsERLrPF3SVa+QiXu8vTTPrETBWnu9VgyFzgdEbUKRas1X3qerEAHcNBms1EAl2FOiQM1k5EDygrClv4KWgyzntEtKJbN2UCFKxtoBSdMZA6fcGtCwffcj8uIAIP2NhZixbU+smVgVbpMpe6QEuuEoVlVrfH8iXxb8Gi+qkd0YIYAHkjtTqQ/nHuAUhcuEE0mORTNGPv7CmTwpuQiGCCdtySZc7Qq8z1x2y7RLy0+RVxM0PR8UV6iy4ipyTgZ6wTF30ksLDxOI3GlRaKF3F6kLErOiEiEUOqa+zLgUM0OLGTn+KLATQDx74in5NcKjKUAnkuxdZyuDbifvQb5tqfrGdXd22pzVZbielRJRW59ig0Nr5cxEpRtoRkoFKNk7o3XlD6JmIBjKn1UHkZ4H/oLUKIXT2qOP2fIEzgLjfpSuGwhvJRz1KRP49HYVl7Gkd45/RdZ519W0gnMkIrEaod90iXSFNTgmJTGeH0Mv0jHameN47PIT3c49MOy5Hj0XCHUPfc6qqrdGnliS5hTnrFThCfn5ZuSZxVdgGLJUQvV+D+5KDqjFdGyNGVGoEg0YdrDtGXmpojbyQDJAT7ToL3yIBF7co=
before_install:
# Install docker-compose
- sudo rm /usr/local/bin/docker-compose
- 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)
- [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
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
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:v.*/dessalines\/lemmy:$new_tag/" ../travis/docker_push.sh
git add ../prod/docker-compose.yml
git add ../../ansible/templates/docker-compose.yml
git add ../travis/docker_push.sh
# The commit
git commit -m"Version $new_tag"
git tag $new_tag
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
# Now doing the building on travis, but leave this in for when you need to do an arm build
# Rebuilding docker
if [ $third_semver -eq 0 ]; then
# TODO get linux/arm/v7 build working
# Build for Raspberry Pi / other archs too
docker buildx build --platform linux/amd64,linux/arm64 ../../ \
--file Dockerfile \
--tag dessalines/lemmy:$new_tag \
--push
else
docker buildx build --platform linux/amd64 ../../ \
--file Dockerfile \
--tag dessalines/lemmy:$new_tag \
--push
fi
# export COMPOSE_DOCKER_CLI_BUILD=1
# export DOCKER_BUILDKIT=1
# # Rebuilding docker
# if [ $third_semver -eq 0 ]; then
# # TODO get linux/arm/v7 build working
# # Build for Raspberry Pi / other archs too
# docker buildx build --platform linux/amd64,linux/arm64 ../../ \
# --file Dockerfile \
# --tag dessalines/lemmy:$new_tag \
# --push
# else
# docker buildx build --platform linux/amd64 ../../ \
# --file Dockerfile \
# --tag dessalines/lemmy:$new_tag \
# --push
# fi
# Push
git push origin $new_tag
git push
# Pushing to any ansible deploys
cd ../../../lemmy-ansible || exit
ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
# cd ../../../lemmy-ansible || exit
# ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass

View File

@ -12,7 +12,7 @@ services:
restart: always
lemmy:
image: dessalines/lemmy:v0.7.26
image: dessalines/lemmy:v0.7.30
ports:
- "127.0.0.1:8536:8536"
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
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
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the

View File

@ -17,6 +17,7 @@
- [Errors](#errors)
- [API documentation](#api-documentation)
* [Sort Types](#sort-types)
* [Undoing actions](#undoing-actions)
* [Websocket vs HTTP](#websocket-vs-http)
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
+ [Login](#login)
@ -43,142 +44,198 @@
- [Request](#request-5)
- [Response](#response-5)
- [HTTP](#http-6)
+ [Edit User Mention](#edit-user-mention)
+ [Mark User Mention as read](#mark-user-mention-as-read)
- [Request](#request-6)
- [Response](#response-6)
- [HTTP](#http-7)
+ [Mark All As Read](#mark-all-as-read)
+ [Get Private Messages](#get-private-messages)
- [Request](#request-7)
- [Response](#response-7)
- [HTTP](#http-8)
+ [Delete Account](#delete-account)
+ [Create Private Message](#create-private-message)
- [Request](#request-8)
- [Response](#response-8)
- [HTTP](#http-9)
+ [Add admin](#add-admin)
+ [Edit Private Message](#edit-private-message)
- [Request](#request-9)
- [Response](#response-9)
- [HTTP](#http-10)
+ [Ban user](#ban-user)
+ [Delete Private Message](#delete-private-message)
- [Request](#request-10)
- [Response](#response-10)
- [HTTP](#http-11)
* [Site](#site)
+ [List Categories](#list-categories)
+ [Mark Private Message as Read](#mark-private-message-as-read)
- [Request](#request-11)
- [Response](#response-11)
- [HTTP](#http-12)
+ [Search](#search)
+ [Mark All As Read](#mark-all-as-read)
- [Request](#request-12)
- [Response](#response-12)
- [HTTP](#http-13)
+ [Get Modlog](#get-modlog)
+ [Delete Account](#delete-account)
- [Request](#request-13)
- [Response](#response-13)
- [HTTP](#http-14)
+ [Create Site](#create-site)
+ [Add admin](#add-admin)
- [Request](#request-14)
- [Response](#response-14)
- [HTTP](#http-15)
+ [Edit Site](#edit-site)
+ [Ban user](#ban-user)
- [Request](#request-15)
- [Response](#response-15)
- [HTTP](#http-16)
+ [Get Site](#get-site)
* [Site](#site)
+ [List Categories](#list-categories)
- [Request](#request-16)
- [Response](#response-16)
- [HTTP](#http-17)
+ [Transfer Site](#transfer-site)
+ [Search](#search)
- [Request](#request-17)
- [Response](#response-17)
- [HTTP](#http-18)
+ [Get Site Config](#get-site-config)
+ [Get Modlog](#get-modlog)
- [Request](#request-18)
- [Response](#response-18)
- [HTTP](#http-19)
+ [Save Site Config](#save-site-config)
+ [Create Site](#create-site)
- [Request](#request-19)
- [Response](#response-19)
- [HTTP](#http-20)
* [Community](#community)
+ [Get Community](#get-community)
+ [Edit Site](#edit-site)
- [Request](#request-20)
- [Response](#response-20)
- [HTTP](#http-21)
+ [Create Community](#create-community)
+ [Get Site](#get-site)
- [Request](#request-21)
- [Response](#response-21)
- [HTTP](#http-22)
+ [List Communities](#list-communities)
+ [Transfer Site](#transfer-site)
- [Request](#request-22)
- [Response](#response-22)
- [HTTP](#http-23)
+ [Ban from Community](#ban-from-community)
+ [Get Site Config](#get-site-config)
- [Request](#request-23)
- [Response](#response-23)
- [HTTP](#http-24)
+ [Add Mod to Community](#add-mod-to-community)
+ [Save Site Config](#save-site-config)
- [Request](#request-24)
- [Response](#response-24)
- [HTTP](#http-25)
+ [Edit Community](#edit-community)
* [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-25)
- [Response](#response-25)
- [HTTP](#http-26)
+ [Follow Community](#follow-community)
+ [Create Community](#create-community)
- [Request](#request-26)
- [Response](#response-26)
- [HTTP](#http-27)
+ [Get Followed Communities](#get-followed-communities)
+ [List Communities](#list-communities)
- [Request](#request-27)
- [Response](#response-27)
- [HTTP](#http-28)
+ [Transfer Community](#transfer-community)
+ [Ban from Community](#ban-from-community)
- [Request](#request-28)
- [Response](#response-28)
- [HTTP](#http-29)
* [Post](#post)
+ [Create Post](#create-post)
+ [Add Mod to Community](#add-mod-to-community)
- [Request](#request-29)
- [Response](#response-29)
- [HTTP](#http-30)
+ [Get Post](#get-post)
+ [Edit Community](#edit-community)
- [Request](#request-30)
- [Response](#response-30)
- [HTTP](#http-31)
+ [Get Posts](#get-posts)
+ [Delete Community](#delete-community)
- [Request](#request-31)
- [Response](#response-31)
- [HTTP](#http-32)
+ [Create Post Like](#create-post-like)
+ [Remove Community](#remove-community)
- [Request](#request-32)
- [Response](#response-32)
- [HTTP](#http-33)
+ [Edit Post](#edit-post)
+ [Follow Community](#follow-community)
- [Request](#request-33)
- [Response](#response-33)
- [HTTP](#http-34)
+ [Save Post](#save-post)
+ [Get Followed Communities](#get-followed-communities)
- [Request](#request-34)
- [Response](#response-34)
- [HTTP](#http-35)
* [Comment](#comment)
+ [Create Comment](#create-comment)
+ [Transfer Community](#transfer-community)
- [Request](#request-35)
- [Response](#response-35)
- [HTTP](#http-36)
+ [Edit Comment](#edit-comment)
* [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-36)
- [Response](#response-36)
- [HTTP](#http-37)
+ [Save Comment](#save-comment)
+ [Get Post](#get-post)
- [Request](#request-37)
- [Response](#response-37)
- [HTTP](#http-38)
+ [Create Comment Like](#create-comment-like)
+ [Get Posts](#get-posts)
- [Request](#request-38)
- [Response](#response-38)
- [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)
+ [All](#all)
+ [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.
- `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
- 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`
#### Edit User Mention
#### Mark User Mention as read
Only the recipient can do this.
##### Request
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
user_mention_id: i32,
read: Option<bool>,
read: bool,
auth: String,
}
}
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
##### Response
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
mention: UserMentionView,
}
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
```
##### 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
@ -744,6 +942,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
```rust
{
op: "GetSite"
data: {
auth: Option<String>,
}
}
```
##### Response
@ -756,6 +958,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
banned: Vec<UserView>,
online: usize, // This is currently broken
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: {
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -973,7 +1175,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /community/mod`
#### Edit Community
Mods and admins can remove and lock a community, creators can delete it.
Only mods can edit a community.
##### Request
```rust
@ -984,10 +1186,6 @@ Mods and admins can remove and lock a community, creators can delete it.
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
expires: Option<i64>,
auth: String
}
}
@ -1005,6 +1203,62 @@ Mods and admins can remove and lock a community, creators can delete it.
`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
##### Request
```rust
@ -1090,8 +1344,9 @@ Mods and admins can remove and lock a community, creators can delete it.
name: String,
url: Option<String>,
body: Option<String>,
nsfw: bool,
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>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -1197,25 +1451,17 @@ Post listing types are `All, Subscribed, Community`
`POST /post/like`
#### Edit Post
Mods and admins can remove and lock a post, creators can delete it.
##### Request
```rust
{
op: "EditPost",
data: {
edit_id: i32,
creator_id: i32,
community_id: i32,
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
locked: Option<bool>,
reason: Option<String>,
auth: String
nsfw: bool,
auth: String,
}
}
```
@ -1233,6 +1479,120 @@ Mods and admins can remove and lock a post, creators can delete it.
`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
##### Request
```rust
@ -1267,8 +1627,8 @@ Mods and admins can remove and lock a post, creators can delete it.
data: {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>,
post_id: i32,
form_id: Option<String>, // An optional form id, so you know which message came back
auth: String
}
}
@ -1289,7 +1649,7 @@ Mods and admins can remove and lock a post, creators can delete it.
#### Edit Comment
Mods and admins can remove a comment, creators can delete it.
Only the creator can edit the comment.
##### Request
```rust
@ -1297,15 +1657,9 @@ Mods and admins can remove a comment, creators can delete it.
op: "EditComment",
data: {
content: String,
parent_id: Option<i32>,
edit_id: i32,
creator_id: i32,
post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
read: Option<bool>,
auth: String
form_id: Option<String>,
auth: String,
}
}
```
@ -1322,6 +1676,92 @@ Mods and admins can remove a comment, creators can delete it.
`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
##### Request
```rust
@ -1357,7 +1797,6 @@ Mods and admins can remove a comment, creators can delete it.
op: "CreateCommentLike",
data: {
comment_id: i32,
post_id: i32,
score: i16,
auth: String
}

6
server/Cargo.lock generated vendored
View File

@ -1397,9 +1397,9 @@ dependencies = [
[[package]]
name = "http-signature-normalization"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648233553603e7bb55bc1ea08a514661e212c09c10f6434507894273d8b5e773"
checksum = "ee917294413cec0db93a8af6ecfa63730c1d2bb604bd1da69ba75b342fb23f21"
dependencies = [
"chrono",
"thiserror",
@ -1559,7 +1559,9 @@ dependencies = [
"bcrypt",
"chrono",
"diesel",
"lazy_static",
"log",
"regex",
"serde 1.0.114",
"serde_json",
"sha2",

View File

@ -13,4 +13,6 @@ strum_macros = "0.18.0"
log = "0.4.0"
sha2 = "0.9"
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)
}
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> {
use crate::schema::comment::dsl::*;
@ -116,6 +108,46 @@ impl Comment {
))
.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)]

View File

@ -1,4 +1,5 @@
use crate::{
naive_now,
schema::{community, community_follower, community_moderator, community_user_ban},
Bannable,
Crud,
@ -29,7 +30,6 @@ pub struct Community {
pub last_refreshed_at: chrono::NaiveDateTime,
}
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
@ -99,6 +99,57 @@ impl Community {
use crate::schema::community::dsl::*;
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)]

View File

@ -295,18 +295,18 @@ pub struct 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::*;
community_moderator_view
.filter(community_id.eq(from_community_id))
.filter(community_id.eq(for_community_id))
.order_by(published)
.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::*;
community_moderator_view
.filter(user_id.eq(from_user_id))
.filter(user_id.eq(for_user_id))
.order_by(published)
.load::<Self>(conn)
}

View File

@ -2,9 +2,12 @@
pub extern crate diesel;
#[macro_use]
pub extern crate strum_macros;
#[macro_use]
pub extern crate lazy_static;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate log;
pub extern crate regex;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
@ -12,6 +15,7 @@ pub extern crate strum;
use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
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)]
mod tests {
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};
pub fn establish_unpooled_connection() -> PgConnection {
@ -194,4 +207,10 @@ mod tests {
let test = "This is a fuzzy search";
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)
}
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 {

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 serde::{Deserialize, Serialize};
@ -80,6 +80,50 @@ impl PrivateMessage {
.filter(ap_id.eq(object_id))
.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)]
@ -180,6 +224,10 @@ mod tests {
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
let updated_private_message =
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();
User_::delete(&conn, inserted_creator.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, updated_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);
}
}

View File

@ -1,12 +1,14 @@
use crate::{
is_email_regex,
naive_now,
schema::{user_, user_::dsl::*},
Crud,
};
use bcrypt::{hash, DEFAULT_COST};
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_"]
pub struct User_ {
pub id: i32,
@ -125,9 +127,18 @@ impl User_ {
use crate::schema::user_::dsl::*;
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> {
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)]
mod tests {
use crate::{

View File

@ -56,14 +56,14 @@ pub struct UserView {
pub actor_id: String,
pub name: 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 bio: Option<String>,
pub local: bool,
pub admin: bool,
pub banned: bool,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
pub send_notifications_to_email: bool, // TODO also never used
pub published: chrono::NaiveDateTime,
pub number_of_posts: i64,
pub post_score: i64,

View File

@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]}
comrak = "0.7"
lazy_static = "1.3.0"
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())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
#[cfg(test)]
mod tests {
use crate::{
is_email_regex,
is_valid_community_name,
is_valid_post_title,
is_valid_username,
@ -185,12 +180,6 @@ mod tests {
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]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));

View File

@ -81,9 +81,9 @@ impl Settings {
fn init() -> Result<Self, ConfigError> {
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)
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
@ -115,16 +115,16 @@ impl Settings {
format!("{}/api/v1", self.hostname)
}
pub fn get_config_location() -> String {
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
pub fn get_config_defaults_location() -> String {
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE_DEFAULTS.to_string())
}
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> {
fs::write(Self::get_config_location(), data)?;
fs::write(CONFIG_FILE, data)?;
// Reload the new settings
// 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 jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings};
use lemmy_utils::settings::Settings;
use serde::{Deserialize, Serialize};
type Jwt = String;
@ -9,15 +9,7 @@ type Jwt = String;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub id: i32,
pub username: 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 {
@ -36,15 +28,7 @@ impl Claims {
pub fn jwt(user: User_, hostname: String) -> Jwt {
let my_claims = Claims {
id: user.id,
username: user.name.to_owned(),
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(
&Header::default(),
@ -54,18 +38,6 @@ impl Claims {
.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> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id)

View File

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
blocking,
websocket::{
@ -15,12 +15,10 @@ use lemmy_db::{
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
@ -44,22 +42,38 @@ use std::str::FromStr;
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>, // TODO this isn't used
pub post_id: i32,
form_id: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct EditComment {
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,
creator_id: i32,
pub post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
form_id: Option<String>,
auth: String,
}
#[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>,
read: Option<bool>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkCommentAsRead {
edit_id: i32,
read: bool,
auth: String,
}
@ -74,12 +88,12 @@ pub struct SaveComment {
pub struct CommentResponse {
pub comment: CommentView,
pub recipient_ids: Vec<i32>,
pub form_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentLike {
comment_id: i32,
pub post_id: i32,
score: i16,
auth: String,
}
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
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 inserted_comment =
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()),
};
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| {
let apub_id =
@ -176,7 +197,7 @@ impl Perform for Oper<CreateComment> {
// Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content);
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
let like_form = CommentLikeForm {
@ -201,6 +222,7 @@ impl Perform for Oper<CreateComment> {
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
if let Some(ws) = websocket_info {
@ -237,122 +259,34 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
let mut editors: Vec<i32> = vec![orig_comment.creator_id];
let mut moderators: Vec<i32> = vec![];
let community_id = orig_comment.community_id;
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 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 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 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| {
Comment::update(conn, edit_id, &comment_form2)
Comment::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
@ -360,54 +294,19 @@ impl Perform for Oper<EditComment> {
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
if data.read.is_none() {
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_comment
.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?;
}
// Send the apub update
updated_comment
.send_update(&user, &self.client, pool)
.await?;
// Mod tables
if moderators.contains(&user_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;
// Do the mentions / recipients
let post_id = orig_comment.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, &user, post, pool).await?;
let updated_comment_content = updated_comment.content.to_owned();
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 comment_view = blocking(pool, move |conn| {
@ -418,6 +317,7 @@ impl Perform for Oper<EditComment> {
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
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)]
impl Perform for Oper<SaveComment> {
type Response = CommentResponse;
@ -480,6 +665,7 @@ impl Perform for Oper<SaveComment> {
Ok(CommentResponse {
comment: comment_view,
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
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 community_id = post.community_id;
let is_banned =
@ -550,7 +740,7 @@ impl Perform for Oper<CreateCommentLike> {
let like_form = CommentLikeForm {
comment_id: data.comment_id,
post_id: data.post_id,
post_id,
user_id,
score: data.score,
};
@ -587,6 +777,7 @@ impl Perform for Oper<CreateCommentLike> {
let mut res = CommentResponse {
comment: liked_comment,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
@ -675,10 +866,11 @@ pub async fn send_local_notifs(
user: &User_,
post: Post,
pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<i32>, LemmyError> {
let user2 = user.clone();
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?;
@ -691,6 +883,7 @@ fn do_send_local_notifs(
comment: &Comment,
user: &User_,
post: &Post,
do_send_email: bool,
) -> Vec<i32> {
let mut recipient_ids = Vec::new();
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
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 {
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
let html = &format!(
@ -745,7 +938,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_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 {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(
@ -768,7 +961,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, post.creator_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 {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(

View File

@ -1,6 +1,6 @@
use super::*;
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
apub::ActorType,
blocking,
websocket::{
@ -34,7 +34,6 @@ pub struct GetCommunity {
pub struct GetCommunityResponse {
pub community: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
pub admins: Vec<UserView>,
pub online: usize,
}
@ -101,9 +100,21 @@ pub struct EditCommunity {
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
deleted: Option<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>,
expires: Option<i64>,
auth: String,
@ -184,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
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 {
if let Some(id) = ws.id {
ws.chatserver.do_send(JoinCommunityRoom {
@ -212,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
let res = GetCommunityResponse {
community: community_view,
moderators,
admins,
online,
};
@ -366,24 +369,15 @@ impl Perform for Oper<EditCommunity> {
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 mut editors: Vec<i32> = Vec::new();
editors.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
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 mods: Vec<i32> = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??;
if !mods.contains(&user_id) {
return Err(APIError::err("not_a_moderator").into());
}
let edit_id = data.edit_id;
@ -395,8 +389,8 @@ impl Perform for Oper<EditCommunity> {
description: data.description.to_owned(),
category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
nsfw: data.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
@ -408,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
};
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)
})
.await?
@ -417,42 +411,77 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
if let Some(removed) = data.removed.to_owned() {
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??;
// TODO there needs to be some kind of an apub update
// process for communities and users
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::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() {
if deleted {
updated_community
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Verify its the creator (only a creator can delete the community)
let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
if read_community.creator_id != user_id {
return Err(APIError::err("no_community_edit_allowed").into());
}
// Do the delete
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_community = match blocking(pool, move |conn| {
Community::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// 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;
@ -465,20 +494,88 @@ impl Perform for Oper<EditCommunity> {
community: community_view,
};
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;
send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
ws.chatserver.do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res_sent,
community_id: data.edit_id,
my_id: ws.id,
});
Ok(res)
}
}
#[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)
}
}
@ -494,21 +591,26 @@ impl Perform for Oper<ListCommunities> {
) -> Result<ListCommunitiesResponse, LemmyError> {
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) {
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,
},
None => None,
};
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
let user_id = match &user {
Some(user) => Some(user.id),
None => None,
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
let show_nsfw = match &user {
Some(user) => user.show_nsfw,
None => false,
};
@ -654,27 +756,10 @@ impl Perform for Oper<BanFromCommunity> {
let user_id = claims.id;
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id;
community_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??,
);
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());
}
// Verify that only mods or admins can ban
is_mod_or_admin(pool, user_id, community_id).await?;
let community_user_ban_form = CommunityUserBanForm {
community_id: data.community_id,
@ -694,6 +779,7 @@ impl Perform for Oper<BanFromCommunity> {
}
// Mod tables
// TODO eventually do correct expires
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
@ -753,27 +839,10 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id,
};
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id;
community_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??,
);
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());
}
// Verify that only mods or admins can add mod
is_mod_or_admin(pool, user_id, community_id).await?;
if data.added {
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());
}
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 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() {
return Err(APIError::err("couldnt_update_community").into());
};
@ -941,8 +993,27 @@ impl Perform for Oper<TransferCommunity> {
Ok(GetCommunityResponse {
community: community_view,
moderators,
admins,
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 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 comment;
@ -44,3 +52,25 @@ pub trait Perform {
websocket_info: Option<WebsocketInfo>,
) -> 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::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
blocking,
fetch_iframely_and_pictrs_data,
@ -18,10 +18,8 @@ use lemmy_db::{
naive_now,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
@ -66,7 +64,6 @@ pub struct GetPostResponse {
comments: Vec<CommentView>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
pub online: usize,
}
@ -96,20 +93,42 @@ pub struct CreatePostLike {
#[derive(Serialize, Deserialize)]
pub struct EditPost {
pub edit_id: i32,
creator_id: i32,
community_id: i32,
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool,
locked: Option<bool>,
stickied: Option<bool>,
auth: String,
}
#[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>,
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)]
pub struct SavePost {
post_id: i32,
@ -311,14 +330,6 @@ impl Perform for Oper<GetPost> {
})
.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 {
if let Some(id) = ws.id {
ws.chatserver.do_send(JoinPostRoom {
@ -343,7 +354,6 @@ impl Perform for Oper<GetPost> {
comments,
community,
moderators,
admins,
online,
})
}
@ -360,21 +370,26 @@ impl Perform for Oper<GetPosts> {
) -> Result<GetPostsResponse, LemmyError> {
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) {
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,
},
None => None,
};
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
let user_id = match &user {
Some(user) => Some(user.id),
None => None,
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
let show_nsfw = match &user {
Some(user) => user.show_nsfw,
None => false,
};
@ -549,35 +564,10 @@ impl Perform for Oper<EditPost> {
let user_id = claims.id;
let edit_id = data.edit_id;
let read_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());
}
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a community ban
let community_id = read_post.community_id;
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? {
@ -590,55 +580,34 @@ impl Perform for Oper<EditPost> {
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
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = {
// only modify some properties if they are a moderator
if moderators.contains(&user_id) {
PostForm {
name: data.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: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
locked: data.locked.to_owned(),
stickied: data.stickied.to_owned(),
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,
}
} 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 post_form = PostForm {
name: data.name.trim().to_owned(),
url: data.url.to_owned(),
body: data.body.to_owned(),
nsfw: data.nsfw,
creator_id: orig_post.creator_id.to_owned(),
community_id: orig_post.community_id,
removed: Some(orig_post.removed),
deleted: Some(orig_post.deleted),
locked: Some(orig_post.locked),
stickied: Some(orig_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: orig_post.ap_id,
local: orig_post.local,
published: None,
};
let edit_id = data.edit_id;
@ -656,58 +625,8 @@ impl Perform for Oper<EditPost> {
}
};
if moderators.contains(&user_id) {
// Mod tables
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?;
}
// Send apub update
updated_post.send_update(&user, &self.client, pool).await?;
let edit_id = data.edit_id;
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)]
impl Perform for Oper<SavePost> {
type Response = PostResponse;

View File

@ -1,6 +1,6 @@
use super::user::Register;
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id,
blocking,
version,
@ -18,6 +18,7 @@ use lemmy_db::{
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
SearchType,
@ -98,7 +99,9 @@ pub struct EditSite {
}
#[derive(Serialize, Deserialize)]
pub struct GetSite {}
pub struct GetSite {
auth: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct SiteResponse {
@ -112,6 +115,7 @@ pub struct GetSiteResponse {
banned: Vec<UserView>,
pub online: usize,
version: String,
my_user: Option<User_>,
}
#[derive(Serialize, Deserialize)]
@ -257,10 +261,7 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id;
// Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let site_form = SiteForm {
name: data.name.to_owned(),
@ -311,10 +312,7 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id;
// Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
@ -358,7 +356,7 @@ impl Perform for Oper<GetSite> {
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteResponse, LemmyError> {
let _data: &GetSite = &self.data;
let data: &GetSite = &self.data;
// TODO refactor this a little
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
@ -421,12 +419,29 @@ impl Perform for Oper<GetSite> {
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 {
site: site_view,
admins,
banned,
online,
version: version::VERSION.to_string(),
my_user,
})
}
}
@ -620,6 +635,11 @@ impl Perform for Oper<TransferSite> {
};
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??;
@ -670,6 +690,7 @@ impl Perform for Oper<TransferSite> {
banned,
online: 0,
version: version::VERSION.to_string(),
my_user: Some(user),
})
}
}
@ -693,12 +714,7 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id;
// Only let admins read this
let admins = blocking(pool, move |conn| UserView::admins(conn)).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());
}
is_admin(pool, user_id).await?;
let config_hjson = Settings::read_config_file()?;

View File

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::ApubObjectType,
blocking,
websocket::{
@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>,
posts: Vec<PostView>,
admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
@ -174,9 +173,9 @@ pub struct GetUserMentions {
}
#[derive(Serialize, Deserialize)]
pub struct EditUserMention {
pub struct MarkUserMentionAsRead {
user_mention_id: i32,
read: Option<bool>,
read: bool,
auth: String,
}
@ -216,9 +215,21 @@ pub struct CreatePrivateMessage {
#[derive(Serialize, Deserialize)]
pub struct EditPrivateMessage {
edit_id: i32,
content: Option<String>,
deleted: Option<bool>,
read: Option<bool>,
content: String,
auth: String,
}
#[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,
}
@ -264,7 +275,7 @@ impl Perform for Oper<Login> {
// Fetch that username / email
let username_or_email = data.username_or_email.clone();
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?
{
@ -550,21 +561,26 @@ impl Perform for Oper<GetUserDetails> {
) -> Result<GetUserDetailsResponse, LemmyError> {
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) {
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,
},
None => None,
};
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
let user_id = match &user {
Some(user) => Some(user.id),
None => None,
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
let show_nsfw = match &user {
Some(user) => user.show_nsfw,
None => false,
};
@ -631,14 +647,6 @@ impl Perform for Oper<GetUserDetails> {
})
.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
// TODO an if let chain would be better here, but can't figure it out
// TODO separate out settings into its own thing
@ -653,7 +661,6 @@ impl Perform for Oper<GetUserDetails> {
moderates,
comments,
posts,
admins,
})
}
}
@ -677,10 +684,7 @@ impl Perform for Oper<AddAdmin> {
let user_id = claims.id;
// Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let added = data.added;
let added_user_id = data.user_id;
@ -739,10 +743,7 @@ impl Perform for Oper<BanUser> {
let user_id = claims.id;
// Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let ban = data.ban;
let banned_user_id = data.user_id;
@ -862,7 +863,7 @@ impl Perform for Oper<GetUserMentions> {
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditUserMention> {
impl Perform for Oper<MarkUserMentionAsRead> {
type Response = UserMentionResponse;
async fn perform(
@ -870,7 +871,7 @@ impl Perform for Oper<EditUserMention> {
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
) -> Result<UserMentionResponse, LemmyError> {
let data: &EditUserMention = &self.data;
let data: &MarkUserMentionAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
@ -887,15 +888,9 @@ impl Perform for Oper<EditUserMention> {
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 update_mention =
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
let read = data.read;
let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
};
@ -940,70 +935,26 @@ impl Perform for Oper<MarkAllAsRead> {
.await??;
// 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 {
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() {
return Err(APIError::err("couldnt_update_comment").into());
}
}
// Mentions
let mentions = blocking(pool, move |conn| {
UserMentionQueryBuilder::create(conn, user_id)
.unread_only(true)
.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());
}
// Mark all user mentions as read
let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
if blocking(pool, update_user_mentions).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
}
// messages
let messages = blocking(pool, move |conn| {
PrivateMessageQueryBuilder::create(conn, user_id)
.page(1)
.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());
}
// Mark all private_messages as read
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
}
Ok(GetRepliesResponse { replies: vec![] })
@ -1242,11 +1193,11 @@ impl Perform for Oper<CreatePrivateMessage> {
let subject = &format!(
"{} - Private Message from {}",
Settings::get().hostname,
claims.username
user.name,
);
let html = &format!(
"<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) {
Ok(_o) => _o,
@ -1293,59 +1244,25 @@ impl Perform for Oper<EditPrivateMessage> {
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
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check to make sure they are the creator (or the recipient marking as read
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|| orig_private_message.creator_id.eq(&user_id))
{
// 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());
}
let content_slurs_removed = match &data.content {
Some(content) => remove_slurs(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,
}
}
};
// Doing the update
let content_slurs_removed = remove_slurs(&data.content);
let edit_id = data.edit_id;
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?
{
@ -1353,30 +1270,14 @@ impl Perform for Oper<EditPrivateMessage> {
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
if data.read.is_none() {
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_private_message
.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?;
}
// Send the apub update
updated_private_message
.send_update(&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 };
@ -1384,7 +1285,146 @@ impl Perform for Oper<EditPrivateMessage> {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
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,
});
}

View File

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub struct PageExtension {
pub comments_enabled: bool,
pub sensitive: bool,
pub stickied: bool,
}
impl<U> UnparsedExtension<U> for PageExtension
@ -19,12 +20,14 @@ where
Ok(PageExtension {
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
sensitive: unparsed_mut.remove("sensitive")?,
stickied: unparsed_mut.remove("stickied")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
unparsed_mut.insert("stickied", self.stickied)?;
Ok(())
}
}

View File

@ -1,6 +1,6 @@
use crate::{
apub::{
inbox::activities::{
apub::inbox::{
activities::{
create::receive_create,
delete::receive_delete,
dislike::receive_dislike,
@ -9,15 +9,14 @@ use crate::{
undo::receive_undo,
update::receive_update,
},
inbox::shared_inbox::receive_unhandled_activity,
shared_inbox::receive_unhandled_activity,
},
routes::ChatServerParam,
DbPool,
LemmyError,
};
use activitystreams_new::{activity::*, prelude::ExtendsExt};
use activitystreams_new::{activity::*, base::AnyBase, prelude::ExtendsExt};
use actix_web::{client::Client, HttpResponse};
use activitystreams_new::base::AnyBase;
pub async fn receive_announce(
activity: AnyBase,
@ -30,27 +29,13 @@ pub async fn receive_announce(
let object = announce.object();
let object2 = object.clone().one().unwrap();
match kind {
Some("Create") => {
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("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
}
Some("Create") => 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("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),
}
}

View File

@ -3,8 +3,12 @@ use crate::{
comment::{send_local_notifs, CommentResponse},
post::PostResponse,
},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
inbox::shared_inbox::{
announce_if_community_is_local,
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
PageExt,
@ -18,7 +22,7 @@ use crate::{
DbPool,
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 lemmy_db::{
comment::{Comment, CommentForm},
@ -28,8 +32,6 @@ use lemmy_db::{
Crud,
};
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(
activity: AnyBase,
@ -100,7 +102,7 @@ async fn receive_create_comment(
// anyway.
let mentions = scrape_text_for_mentions(&inserted_comment.content);
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
let comment_view = blocking(pool, move |conn| {
@ -111,6 +113,7 @@ async fn receive_create_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,9 +1,12 @@
use crate::{
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
apub::inbox::
shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
GroupExt,
@ -18,7 +21,7 @@ use crate::{
DbPool,
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 lemmy_db::{
comment::{Comment, CommentForm},
@ -30,8 +33,6 @@ use lemmy_db::{
post_view::PostView,
Crud,
};
use activitystreams_new::base::AnyBase;
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
pub async fn receive_delete(
activity: AnyBase,
@ -146,6 +147,7 @@ async fn receive_delete_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,8 +1,12 @@
use crate::{
api::{comment::CommentResponse, post::PostResponse},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
PageExt,
@ -16,7 +20,7 @@ use crate::{
DbPool,
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 lemmy_db::{
comment::{CommentForm, CommentLike, CommentLikeForm},
@ -25,8 +29,6 @@ use lemmy_db::{
post_view::PostView,
Likeable,
};
use activitystreams_new::base::AnyBase;
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
pub async fn receive_dislike(
activity: AnyBase,
@ -119,6 +121,7 @@ async fn receive_dislike_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,8 +1,12 @@
use crate::{
api::{comment::CommentResponse, post::PostResponse},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
PageExt,
@ -16,7 +20,7 @@ use crate::{
DbPool,
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 lemmy_db::{
comment::{CommentForm, CommentLike, CommentLikeForm},
@ -25,8 +29,6 @@ use lemmy_db::{
post_view::PostView,
Likeable,
};
use activitystreams_new::base::AnyBase;
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
pub async fn receive_like(
activity: AnyBase,
@ -119,6 +121,7 @@ async fn receive_like_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,8 +1,12 @@
use crate::{
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
GroupExt,
@ -17,7 +21,7 @@ use crate::{
DbPool,
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 lemmy_db::{
comment::{Comment, CommentForm},
@ -29,8 +33,6 @@ use lemmy_db::{
post_view::PostView,
Crud,
};
use activitystreams_new::base::AnyBase;
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
pub async fn receive_remove(
activity: AnyBase,
@ -145,6 +147,7 @@ async fn receive_remove_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,8 +1,12 @@
use crate::{
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
GroupExt,
@ -17,7 +21,7 @@ use crate::{
DbPool,
LemmyError,
};
use activitystreams_new::{activity::*, object::Note, prelude::*};
use activitystreams_new::{activity::*, base::AnyBase, object::Note, prelude::*};
use actix_web::{client::Client, HttpResponse};
use lemmy_db::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
@ -30,8 +34,6 @@ use lemmy_db::{
Crud,
Likeable,
};
use activitystreams_new::base::AnyBase;
use crate::apub::inbox::shared_inbox::announce_if_community_is_local;
pub async fn receive_undo(
activity: AnyBase,
@ -101,17 +103,14 @@ async fn receive_undo_like(
async fn receive_undo_dislike(
undo: Undo,
client: &Client,
pool: &DbPool,
chat_server: ChatServerParam,
_client: &Client,
_pool: &DbPool,
_chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
let type_ = dislike.object().as_single_kind_str().unwrap();
match type_ {
// TODO: handle dislike
d => Err(format_err!("Undo Delete type {} not supported", d).into()),
}
Err(format_err!("Undo Delete type {} not supported", type_).into())
}
async fn receive_undo_delete_comment(
@ -159,6 +158,7 @@ async fn receive_undo_delete_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -216,6 +216,7 @@ async fn receive_undo_remove_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -499,6 +500,7 @@ async fn receive_undo_like_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -3,9 +3,13 @@ use crate::{
comment::{send_local_notifs, CommentResponse},
post::PostResponse,
},
apub::inbox::shared_inbox::{get_user_from_activity, receive_unhandled_activity},
apub::{
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,
FromApub,
PageExt,
@ -19,11 +23,7 @@ use crate::{
DbPool,
LemmyError,
};
use activitystreams_new::{
activity::{Update},
object::Note,
prelude::*,
};
use activitystreams_new::{activity::Update, base::AnyBase, object::Note, prelude::*};
use actix_web::{client::Client, HttpResponse};
use lemmy_db::{
comment::{Comment, CommentForm},
@ -33,8 +33,6 @@ use lemmy_db::{
Crud,
};
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(
activity: AnyBase,
@ -106,7 +104,8 @@ async fn receive_update_comment(
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
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
let comment_view =
@ -115,6 +114,7 @@ async fn receive_update_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View File

@ -1,4 +1,4 @@
pub mod activities;
pub mod community_inbox;
pub mod shared_inbox;
pub mod user_inbox;
pub mod user_inbox;

View File

@ -1,8 +1,10 @@
use crate::{
apub::{
community::do_announce,
extensions::signatures::verify,
fetcher::{
get_or_fetch_and_upsert_remote_actor,
get_or_fetch_and_upsert_remote_community,
get_or_fetch_and_upsert_remote_user,
},
inbox::activities::{
@ -23,18 +25,15 @@ use crate::{
};
use activitystreams_new::{
activity::{ActorAndObject, ActorAndObjectRef},
base::{AsBase},
base::{AsBase, Extends},
object::AsObject,
prelude::*,
};
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{user::User_};
use lemmy_db::user::User_;
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 std::fmt::Debug;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "PascalCase")]
@ -78,9 +77,7 @@ pub async fn shared_inbox(
let kind = activity.kind().unwrap();
dbg!(kind);
match kind {
ValidTypes::Announce => {
receive_announce(any_base, &client, &pool, chat_server).await
}
ValidTypes::Announce => receive_announce(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::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
A: Debug,
{
@ -104,8 +103,8 @@ pub(in crate::apub::inbox) async fn get_user_from_activity<T, A>(
client: &Client,
pool: &DbPool,
) -> Result<User_, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef,
where
T: AsBase<A> + ActorAndObjectRef,
{
let actor = activity.actor()?;
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,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: AsObject<Kind>,
T: Extends<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
where
T: AsObject<Kind>,
T: Extends<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let cc = activity.cc().unwrap();
let cc = cc.as_many().unwrap();

View File

@ -133,6 +133,7 @@ impl ToApub for Post {
let ext = PageExtension {
comments_enabled: !self.locked,
sensitive: self.nsfw,
stickied: self.stickied,
};
Ok(Ext1::new(page, ext))
}
@ -244,7 +245,7 @@ impl FromApub for PostForm {
.map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: ext.sensitive,
stickied: None, // -> put it in "featured" collection of the community
stickied: Some(ext.stickied),
embed_title,
embed_description,
embed_html,

View File

@ -1,5 +1,4 @@
use crate::{
api::claims::Claims,
apub::{
activities::send_activity,
create_apub_response,
@ -257,7 +256,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name;
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??;
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("/list", web::get().to(route_get::<ListCommunities>))
.route("/follow", web::post().to(route_post::<FollowCommunity>))
.route("/delete", web::post().to(route_post::<DeleteCommunity>))
// Mod Actions
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
.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())
.route("", web::get().to(route_get::<GetPost>))
.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("/like", web::post().to(route_post::<CreatePostLike>))
.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())
.route("", web::post().to(route_post::<CreateComment>))
.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("/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())
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
.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
.service(
@ -107,7 +127,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetUserDetails>))
.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(
"/followed_communities",

View File

@ -1,11 +1,9 @@
use crate::apub::{
comment::get_apub_comment,
community::*,
inbox::community_inbox::community_inbox,
inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox},
post::get_apub_post,
inbox::shared_inbox::shared_inbox,
user::*,
inbox::user_inbox::user_inbox,
APUB_JSON_CONTENT_TYPE,
};
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,
CreateComment,
EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost,
EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
MarkUserMentionAsRead,
GetModlog,
BanFromCommunity,
AddModToCommunity,
@ -59,6 +68,8 @@ pub enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,

View File

@ -212,6 +212,9 @@ impl ChatServer {
// Also leave all communities
// 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() {
sessions.remove(&id);
}
@ -443,18 +446,28 @@ impl ChatServer {
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
UserOperation::BanUser => do_user_operation::<BanUser>(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::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(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 => {
do_user_operation::<CreatePrivateMessage>(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::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
@ -473,6 +486,8 @@ impl ChatServer {
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(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::GetFollowedCommunities => {
do_user_operation::<GetFollowedCommunities>(args).await
@ -485,12 +500,19 @@ impl ChatServer {
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
UserOperation::GetPosts => do_user_operation::<GetPosts>(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::SavePost => do_user_operation::<SavePost>(args).await,
// Comment ops
UserOperation::CreateComment => do_user_operation::<CreateComment>(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::GetComments => do_user_operation::<GetComments>(args).await,
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,

View File

@ -2,6 +2,11 @@
border: 0px;
}
.navbar-expand-lg .navbar-nav .nav-link {
padding-right: .75rem !important;
padding-left: .75rem !important;
}
.pointer {
cursor: pointer;
}
@ -134,12 +139,14 @@ blockquote {
.thumbnail {
object-fit: cover;
min-height: 60px;
max-height: 80px;
width: 100%;
}
svg.thumbnail {
height: 40px;
.thumbnail svg {
height: 1.25rem;
width: 1.25rem;
}
.no-s-hows {

View File

@ -1,17 +1,17 @@
$white: #ffffff;
$orange: #faa077;
$orange: #f1641e;
$cyan: #02bdc2;
$green: #d4e9d7;
$green: #00C853;
$secondary: $green;
$body-color: $gray-700;
$link-color: theme-color("danger");;
$link-color: theme-color("primary");;
$primary: $orange;
$red: #d8486a;
$border-radius: 1.5rem;
$border-radius-lg: 1.5rem;
$border-radius: 0.5rem;
$border-radius-lg: 0.5rem;
$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;
$input-btn-focus-color: rgba($component-active-bg, .75);
$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;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$info: darken($green, 25%);;
$body-bg: #f2f0f0;
$success: darken($green, 25%);;
$info: $blue;
$body-bg: #fff;
$success: $indigo;
$danger: darken($primary, 25%);
$navbar-light-hover-color: $gray-900;
$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": [],
"dependencies": {
"@types/autosize": "^3.0.6",
"@types/jest": "^26.0.7",
"@types/js-cookie": "^2.2.6",
"@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/node": "^13.11.1",
"@types/node": "^14.0.26",
"@types/node-fetch": "^2.5.6",
"autosize": "^4.0.2",
"bootswatch": "^4.3.1",
"choices.js": "^9.0.1",
@ -30,12 +32,13 @@
"husky": "^4.2.5",
"i18next": "^19.4.1",
"inferno": "^7.4.2",
"inferno-helmet": "^5.2.1",
"inferno-i18next": "nimbusec-oss/inferno-i18next",
"inferno-router": "^7.4.2",
"js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0",
"markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0",
"markdown-it": "^11.0.0",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^1.4.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
@ -51,16 +54,14 @@
"ws": "^7.2.3"
},
"devDependencies": {
"@types/jest": "^25.2.1",
"@types/node-fetch": "^2.5.6",
"eslint": "^6.5.1",
"eslint": "^7.5.0",
"eslint-plugin-inferno": "^7.14.3",
"eslint-plugin-jane": "^7.2.1",
"eslint-plugin-jane": "^8.0.4",
"fuse-box": "^3.1.3",
"jest": "^25.4.0",
"jest": "^26.0.7",
"lint-staged": "^10.1.3",
"sortpack": "^2.1.4",
"ts-jest": "^25.4.0",
"ts-jest": "^26.1.3",
"ts-node": "^8.8.2",
"ts-transform-classcat": "^1.0.0",
"ts-transform-inferno": "^4.0.3",

View File

@ -4,22 +4,29 @@ import {
LoginForm,
LoginResponse,
PostForm,
DeletePostForm,
RemovePostForm,
StickyPostForm,
LockPostForm,
PostResponse,
SearchResponse,
FollowCommunityForm,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse,
CommentForm,
DeleteCommentForm,
RemoveCommentForm,
CommentResponse,
CommunityForm,
GetCommunityForm,
DeleteCommunityForm,
RemoveCommunityForm,
GetCommunityResponse,
CommentLikeForm,
CreatePostLikeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
PrivateMessageResponse,
PrivateMessagesResponse,
GetUserMentionsResponse,
@ -97,7 +104,6 @@ describe('main', () => {
name,
auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};
@ -266,7 +272,6 @@ describe('main', () => {
name,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
@ -323,7 +328,6 @@ describe('main', () => {
edit_id: 2,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
@ -342,6 +346,27 @@ describe('main', () => {
expect(updateResponse.post.community_local).toBe(false);
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 getPostRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
@ -350,6 +375,76 @@ describe('main', () => {
expect(getPostRes.post.name).toBe(name);
expect(getPostRes.post.community_local).toBe(true);
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 = {
comment_id: createResponse.comment.id,
score: 0,
post_id: 2,
auth: lemmyAlphaAuth,
};
@ -585,7 +679,6 @@ describe('main', () => {
name: postName,
auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false,
};
@ -620,19 +713,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta deletes the comment
let deleteCommentForm: CommentForm = {
content: commentContent,
let deleteCommentForm: DeleteCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: true,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let deleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -649,19 +739,16 @@ describe('main', () => {
expect(getPostRes.comments[0].deleted).toBe(true);
// lemmy_beta undeletes the comment
let undeleteCommentForm: CommentForm = {
content: commentContent,
let undeleteCommentForm: DeleteCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: false,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let undeleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -677,23 +764,22 @@ describe('main', () => {
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
// lemmy_beta deletes the post
let deletePostForm: PostForm = {
name: postName,
let deletePostForm: DeletePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: true,
auth: lemmyBetaAuth,
};
let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(deletePostForm),
}).then(d => d.json());
let deletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post/delete`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(deletePostForm),
}
).then(d => d.json());
expect(deletePostRes.post.deleted).toBe(true);
// Make sure lemmy_alpha sees the post is deleted
@ -703,20 +789,16 @@ describe('main', () => {
expect(getPostResAgain.post.deleted).toBe(true);
// lemmy_beta undeletes the post
let undeletePostForm: PostForm = {
name: postName,
let undeletePostForm: DeletePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: false,
auth: lemmyBetaAuth,
};
let undeletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`,
`${lemmyBetaApiUrl}/post/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -732,20 +814,16 @@ describe('main', () => {
expect(getPostResAgainTwo.post.deleted).toBe(false);
// lemmy_beta deletes the community
let deleteCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let deleteCommunityForm: DeleteCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: true,
auth: lemmyBetaAuth,
};
let deleteResponse: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -765,20 +843,16 @@ describe('main', () => {
expect(getCommunityRes.community.deleted).toBe(true);
// lemmy_beta undeletes the community
let undeleteCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let undeleteCommunityForm: DeleteCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: false,
auth: lemmyBetaAuth,
};
let undeleteCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -861,7 +935,6 @@ describe('main', () => {
name: postName,
auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false,
};
@ -896,19 +969,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta removes the comment
let removeCommentForm: CommentForm = {
content: commentContent,
let removeCommentForm: RemoveCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: true,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let removeCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -925,19 +995,16 @@ describe('main', () => {
expect(getPostRes.comments[0].removed).toBe(true);
// lemmy_beta undeletes the comment
let unremoveCommentForm: CommentForm = {
content: commentContent,
let unremoveCommentForm: RemoveCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: false,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let unremoveCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -953,23 +1020,22 @@ describe('main', () => {
expect(getPostUnremoveRes.comments[0].removed).toBe(false);
// lemmy_beta deletes the post
let removePostForm: PostForm = {
name: postName,
let removePostForm: RemovePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: true,
auth: lemmyBetaAuth,
};
let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(removePostForm),
}).then(d => d.json());
let removePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post/remove`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(removePostForm),
}
).then(d => d.json());
expect(removePostRes.post.removed).toBe(true);
// Make sure lemmy_alpha sees the post is deleted
@ -979,20 +1045,16 @@ describe('main', () => {
expect(getPostResAgain.post.removed).toBe(true);
// lemmy_beta unremoves the post
let unremovePostForm: PostForm = {
name: postName,
let unremovePostForm: RemovePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: false,
auth: lemmyBetaAuth,
};
let unremovePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`,
`${lemmyBetaApiUrl}/post/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1007,21 +1069,17 @@ describe('main', () => {
}).then(d => d.json());
expect(getPostResAgainTwo.post.removed).toBe(false);
// lemmy_beta deletes the community
let removeCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
// lemmy_beta removes the community
let removeCommunityForm: RemoveCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
removed: true,
auth: lemmyBetaAuth,
};
let removeCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1029,7 +1087,7 @@ describe('main', () => {
}
).then(d => d.json());
// Make sure the delete went through
// Make sure the remove went through
expect(removeCommunityRes.community.removed).toBe(true);
// Re-get it from alpha, make sure its removed there too
@ -1041,20 +1099,16 @@ describe('main', () => {
expect(getCommunityRes.community.removed).toBe(true);
// lemmy_beta unremoves the community
let unremoveCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let unremoveCommunityForm: RemoveCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
removed: false,
auth: lemmyBetaAuth,
};
let unremoveCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1149,16 +1203,16 @@ describe('main', () => {
);
// lemmy alpha deletes the private message
let deletePrivateMessageForm: EditPrivateMessageForm = {
let deletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: true,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let deleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1182,16 +1236,16 @@ describe('main', () => {
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
// lemmy alpha undeletes the private message
let undeletePrivateMessageForm: EditPrivateMessageForm = {
let undeletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: false,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let undeleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1252,7 +1306,6 @@ describe('main', () => {
name: postName,
auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};
@ -1363,7 +1416,6 @@ describe('main', () => {
name: betaPostName,
auth: lemmyBetaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -80,9 +81,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -92,7 +102,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
) : (
<div class="row">
<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.bannedUsers()}
</div>
@ -220,9 +232,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
}
this.state.siteRes = data;
this.setState(this.state);
document.title = `${i18n.t('admin_settings')} - ${
this.state.siteRes.site.name
}`;
} else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse;
this.state.siteRes.site = data.site;

View File

@ -115,34 +115,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
);
}
handleFinished(op: UserOperation, data: CommentResponse) {
let isReply =
this.props.node !== undefined && data.comment.parent_id !== null;
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;
handleCommentSubmit(msg: { val: string; formId: string }) {
this.state.commentForm.content = msg.val;
this.state.commentForm.form_id = msg.formId;
if (this.props.edit) {
WebSocketService.Instance.editComment(this.state.commentForm);
} else {
@ -160,12 +135,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// Only do the showing and hiding if logged in
if (UserService.Instance.user) {
if (res.op == UserOperation.CreateComment) {
if (
res.op == UserOperation.CreateComment ||
res.op == UserOperation.EditComment
) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
// This only finishes this form, if the randomly generated form_id matches the one received
if (this.state.commentForm.form_id == data.form_id) {
this.setState({ finished: true });
}
}
}
}

View File

@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
import {
CommentNode as CommentNodeI,
CommentLikeForm,
CommentForm as CommentFormI,
EditUserMentionForm,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentAsReadForm,
MarkUserMentionAsReadForm,
SaveCommentForm,
BanFromCommunityForm,
BanUserForm,
@ -62,6 +64,7 @@ interface CommentNodeState {
interface CommentNodeProps {
node: CommentNodeI;
noBorder?: boolean;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;
@ -134,9 +137,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
>
<div
id={`comment-${node.comment.id}`}
className={`details comment-node border-top border-light py-2 ${
this.isCommentNew ? 'mark' : ''
}`}
className={`details comment-node py-2 ${
!this.props.noBorder ? 'border-top border-light' : ''
} ${this.isCommentNew ? 'mark' : ''}`}
style={
!this.props.noIndent &&
this.props.node.comment.parent_id &&
@ -202,7 +205,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</>
)}
<button
class="btn btn-sm text-muted"
class="btn text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
@ -218,7 +221,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* This is an expanding spacer for mobile */}
<div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
<button
className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
className={`btn p-0 unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy}
>
@ -848,16 +851,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = {
content: i.props.node.comment.content,
let deleteForm: DeleteCommentForm = {
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,
auth: null,
};
WebSocketService.Instance.editComment(deleteForm);
WebSocketService.Instance.deleteComment(deleteForm);
}
handleSaveCommentClick(i: CommentNode) {
@ -901,7 +900,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = {
comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote,
};
@ -929,7 +927,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = {
comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote,
};
@ -950,17 +947,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModRemoveSubmit(i: CommentNode) {
event.preventDefault();
let form: CommentFormI = {
content: i.props.node.comment.content,
let form: RemoveCommentForm = {
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,
reason: i.state.removeReason,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.removeComment(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
@ -969,22 +962,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleMarkRead(i: CommentNode) {
// if it has a user_mention_id field, then its a mention
if (i.props.node.comment.user_mention_id) {
let form: EditUserMentionForm = {
let form: MarkUserMentionAsReadForm = {
user_mention_id: i.props.node.comment.user_mention_id,
read: !i.props.node.comment.read,
};
WebSocketService.Instance.editUserMention(form);
WebSocketService.Instance.markUserMentionAsRead(form);
} else {
let form: CommentFormI = {
content: i.props.node.comment.content,
let form: MarkCommentAsReadForm = {
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,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.markCommentAsRead(form);
}
i.state.readLoading = true;

View File

@ -16,6 +16,7 @@ interface CommentNodesProps {
moderators?: Array<CommunityUser>;
admins?: Array<UserView>;
postCreatorId?: number;
noBorder?: boolean;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;
@ -42,6 +43,7 @@ export class CommentNodes extends Component<
<CommentNode
key={node.comment.id}
node={node}
noBorder={this.props.noBorder}
noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly}
locked={this.props.locked}

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -11,6 +12,7 @@ import {
SortType,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { WebSocketService } from '../services';
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
@ -25,6 +27,7 @@ interface CommunitiesState {
communities: Array<Community>;
page: number;
loading: boolean;
site: Site;
}
interface CommunitiesProps {
@ -37,6 +40,7 @@ export class Communities extends Component<any, CommunitiesState> {
communities: [],
loading: true,
page: getPageFromProps(this.props),
site: undefined,
};
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5 class="">
<svg class="icon icon-spinner spin">
@ -157,7 +170,7 @@ export class Communities extends Component<any, CommunitiesState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -166,7 +179,7 @@ export class Communities extends Component<any, CommunitiesState> {
{this.state.communities.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -240,7 +253,8 @@ export class Communities extends Component<any, CommunitiesState> {
this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
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 { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.selects()}
{this.state.loading ? (
<h5>
@ -271,7 +281,7 @@ export class Community extends Component<any, State> {
<div class="my-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -279,7 +289,7 @@ export class Community extends Component<any, State> {
)}
{this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -355,12 +365,14 @@ export class Community extends Component<any, State> {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state);
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;
this.state.community = data.community;
this.setState(this.state);
@ -376,7 +388,13 @@ export class Community extends Component<any, State> {
this.state.loading = false;
this.setState(this.state);
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;
editPostFindRes(data, this.state.posts);
this.setState(this.state);
@ -405,7 +423,11 @@ export class Community extends Component<any, State> {
this.state.comments = data.comments;
this.state.loading = false;
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;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -428,6 +450,7 @@ export class Community extends Component<any, State> {
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.state.admins = data.admins;
this.setState(this.state);
}
}

View File

@ -1,4 +1,5 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm } from './community-form';
@ -7,19 +8,33 @@ import {
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next';
interface CreateCommunityState {
enableNsfw: boolean;
site: Site;
}
export class CreateCommunity extends Component<any, CreateCommunityState> {
private subscription: Subscription;
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) {
super(props, context);
@ -46,15 +61,24 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site.name) {
return `${i18n.t('create_community')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_community')}</h5>
<CommunityForm
onCreate={this.handleCommunityCreate}
enableNsfw={this.state.enableNsfw}
enableNsfw={this.state.site.enable_nsfw}
/>
</div>
</div>
@ -74,9 +98,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableNsfw = data.site.enable_nsfw;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
}
}
}

View File

@ -1,4 +1,5 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form';
@ -61,9 +62,18 @@ export class CreatePost extends Component<any, CreatePostState> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site.name) {
return `${i18n.t('create_post')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_post')}</h5>
@ -100,7 +110,7 @@ export class CreatePost extends Component<any, CreatePostState> {
return lastLocation.split('/c/')[1];
}
}
return undefined;
return;
}
handlePostCreate(id: number) {
@ -117,7 +127,6 @@ export class CreatePost extends Component<any, CreatePostState> {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
}
}
}

View File

@ -1,4 +1,5 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form';
@ -7,15 +8,27 @@ import {
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
Site,
PrivateMessageFormParams,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
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 emptyState: CreatePrivateMessageState = {
site: undefined,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
this
);
@ -40,9 +53,18 @@ export class CreatePrivateMessage extends Component<any, any> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site) {
return `${i18n.t('create_private_message')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_private_message')}</h5>
@ -80,9 +102,8 @@ export class CreatePrivateMessage extends Component<any, any> {
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('create_private_message')} - ${
data.site.name
}`;
this.state.site = data.site;
this.setState(this.state);
}
}
}

View File

@ -35,7 +35,7 @@ export class DataTypeSelect extends Component<
return (
<div class="btn-group btn-group-toggle">
<label
className={`pointer btn btn-sm btn-secondary
className={`pointer btn btn-outline-secondary
${this.state.type_ == DataType.Post && 'active'}
`}
>
@ -48,7 +48,7 @@ export class DataTypeSelect extends Component<
{i18n.t('posts')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${
className={`pointer btn btn-outline-secondary ${
this.state.type_ == DataType.Comment && 'active'
}`}
>

View File

@ -29,7 +29,7 @@ export class IFramelyCard extends Component<
return (
<>
{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="col-12">
<div class="card-body">

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -17,6 +18,7 @@ import {
PrivateMessagesResponse,
PrivateMessageResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@ -57,7 +59,7 @@ interface InboxState {
messages: Array<PrivateMessageI>;
sort: SortType;
page: number;
enableDownvotes: boolean;
site: Site;
}
export class Inbox extends Component<any, InboxState> {
@ -70,7 +72,20 @@ export class Inbox extends Component<any, InboxState> {
messages: [],
sort: SortType.New,
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) {
@ -95,9 +110,20 @@ export class Inbox extends Component<any, InboxState> {
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12">
<h5 class="mb-1">
@ -147,7 +173,7 @@ export class Inbox extends Component<any, InboxState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
`}
>
@ -160,7 +186,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('unread')}
</label>
<label
className={`btn btn-sm btn-secondary pointer
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
`}
>
@ -180,7 +206,7 @@ export class Inbox extends Component<any, InboxState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && 'active'}
`}
>
@ -193,7 +219,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('all')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Replies && 'active'}
`}
>
@ -206,7 +232,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('replies')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Mentions && 'active'}
`}
>
@ -219,7 +245,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('mentions')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Messages && 'active'}
`}
>
@ -269,7 +295,7 @@ export class Inbox extends Component<any, InboxState> {
markable
showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
enableDownvotes={this.state.site.enable_downvotes}
/>
) : (
<PrivateMessage privateMessage={i} />
@ -288,7 +314,7 @@ export class Inbox extends Component<any, InboxState> {
markable
showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
enableDownvotes={this.state.site.enable_downvotes}
/>
</div>
);
@ -304,7 +330,7 @@ export class Inbox extends Component<any, InboxState> {
markable
showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
enableDownvotes={this.state.site.enable_downvotes}
/>
))}
</div>
@ -326,7 +352,7 @@ export class Inbox extends Component<any, InboxState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -334,7 +360,7 @@ export class Inbox extends Component<any, InboxState> {
)}
{this.unreadCount() > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -446,27 +472,54 @@ export class Inbox extends Component<any, InboxState> {
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
found.content = data.message.content;
found.updated = data.message.updated;
found.deleted = data.message.deleted;
// 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;
if (found) {
found.content = data.message.content;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.DeletePrivateMessage) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
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();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) {
// 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;
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 (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
@ -480,7 +533,7 @@ export class Inbox extends Component<any, InboxState> {
this.sendUnreadCount();
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditUserMention) {
} else if (res.op == UserOperation.MarkUserMentionAsRead) {
let data = res.data as UserMentionResponse;
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);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableDownvotes = data.site.enable_downvotes;
this.state.site = data.site;
this.setState(this.state);
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${data.site.name}`;
}
}
sendUnreadCount() {
UserService.Instance.user.unreadCount = this.unreadCount();
UserService.Instance.sub.next({
user: UserService.Instance.user,
});
UserService.Instance.unreadCountSub.next(this.unreadCount());
}
unreadCount(): number {

View File

@ -36,7 +36,7 @@ export class ListingTypeSelect extends Component<
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary
className={`btn btn-outline-secondary
${this.state.type_ == ListingType.Subscribed && 'active'}
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
`}
@ -51,7 +51,7 @@ export class ListingTypeSelect extends Component<
{i18n.t('subscribed')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${
className={`pointer btn btn-outline-secondary ${
this.state.type_ == ListingType.All && 'active'
}`}
>

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -9,6 +10,7 @@ import {
PasswordResetForm,
GetSiteResponse,
WebSocketJsonResponse,
Site,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { wsJsonToRes, validEmail, toast } from '../utils';
@ -19,12 +21,12 @@ interface State {
registerForm: RegisterForm;
loginLoading: boolean;
registerLoading: boolean;
enable_nsfw: boolean;
mathQuestion: {
a: number;
b: number;
answer: number;
};
site: Site;
}
export class Login extends Component<any, State> {
@ -44,12 +46,25 @@ export class Login extends Component<any, State> {
},
loginLoading: false,
registerLoading: false,
enable_nsfw: undefined,
mathQuestion: {
a: Math.floor(Math.random() * 10) + 1,
b: Math.floor(Math.random() * 10) + 1,
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) {
@ -72,9 +87,18 @@ export class Login extends Component<any, State> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site.name) {
return `${i18n.t('login')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
<div class="col-12 col-lg-6">{this.registerForm()}</div>
@ -251,7 +275,7 @@ export class Login extends Component<any, State> {
/>
</div>
</div>
{this.state.enable_nsfw && (
{this.state.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
@ -392,9 +416,8 @@ export class Login extends Component<any, State> {
toast(i18n.t('reset_password_mail_sent'));
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('login')} - ${data.site.name}`;
}
}
}

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<main role="main" class="col-12 col-md-8">
{this.posts()}
@ -195,7 +205,7 @@ export class Main extends Component<any, MainState> {
<div>
{!this.state.loading && (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
{this.trendingCommunities()}
{UserService.Instance.user &&
@ -226,7 +236,7 @@ export class Main extends Component<any, MainState> {
</div>
)}
<Link
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
to="/create_community"
>
{i18n.t('create_a_community')}
@ -295,7 +305,7 @@ export class Main extends Component<any, MainState> {
siteInfo() {
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
{this.canAdmin && (
@ -315,32 +325,32 @@ export class Main extends Component<any, MainState> {
)}
<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 })}
</li>
*/}
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_users', {
count: this.state.siteRes.site.number_of_users,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_communities', {
count: this.state.siteRes.site.number_of_communities,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_posts', {
count: this.state.siteRes.site.number_of_posts,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_comments', {
count: this.state.siteRes.site.number_of_comments,
})}
</li>
<li className="list-inline-item">
<Link className="badge badge-secondary" to="/modlog">
<Link className="badge badge-light" to="/modlog">
{i18n.t('modlog')}
</Link>
</li>
@ -364,7 +374,7 @@ export class Main extends Component<any, MainState> {
</div>
</div>
{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
className="md-div"
@ -381,7 +391,7 @@ export class Main extends Component<any, MainState> {
landing() {
return (
<div class="card border-secondary">
<div class="card bg-transparent border-secondary">
<div class="card-body">
<h5>
{i18n.t('powered_by')}
@ -517,7 +527,7 @@ export class Main extends Component<any, MainState> {
<div class="my-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -525,7 +535,7 @@ export class Main extends Component<any, MainState> {
)}
{this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -627,7 +637,6 @@ export class Main extends Component<any, MainState> {
this.state.siteRes.banned = data.banned;
this.state.siteRes.online = data.online;
this.setState(this.state);
document.title = `${this.state.siteRes.site.name}`;
} else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse;
this.state.siteRes.site = data.site;
@ -702,7 +711,11 @@ export class Main extends Component<any, MainState> {
this.state.comments = data.comments;
this.state.loading = false;
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;
editCommentRes(data, this.state.comments);
this.setState(this.state);

View File

@ -21,7 +21,7 @@ interface MarkdownTextAreaProps {
replyType?: boolean;
focus?: boolean;
disabled?: boolean;
onSubmit?(val: string): any;
onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any;
onReplyCancel?(): any;
}
@ -125,7 +125,7 @@ export class MarkdownTextArea extends Component<
/>
{this.state.previewMode && (
<div
className="card card-body md-div"
className="card bg-transparent border-secondary card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
/>
)}
@ -391,7 +391,8 @@ export class MarkdownTextArea extends Component<
event.preventDefault();
i.state.loading = true;
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) {

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
@ -17,6 +18,7 @@ import {
ModAdd,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { WebSocketService } from '../services';
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
@ -38,6 +40,7 @@ interface ModlogState {
communityId?: number;
communityName?: string;
page: number;
site: Site;
loading: boolean;
}
@ -47,6 +50,7 @@ export class Modlog extends Component<any, ModlogState> {
combined: [],
page: 1,
loading: true,
site: undefined,
};
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5 class="">
<svg class="icon icon-spinner spin">
@ -384,14 +397,14 @@ export class Modlog extends Component<any, ModlogState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -434,7 +447,8 @@ export class Modlog extends Component<any, ModlogState> {
this.setCombined(data);
} else if (res.op == UserOperation.GetSite) {
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,
messageToastify,
md,
setTheme,
} from '../utils';
import { i18n } from '../i18next';
import { i18n, i18nextSetup } from '../i18next';
interface NavbarState {
isLoggedIn: boolean;
@ -44,14 +45,16 @@ interface NavbarState {
admins: Array<UserView>;
searchParam: string;
toggleSearch: boolean;
siteLoading: boolean;
}
export class Navbar extends Component<any, NavbarState> {
private wsSub: Subscription;
private userSub: Subscription;
private unreadCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = {
isLoggedIn: UserService.Instance.user !== undefined,
isLoggedIn: false,
unreadCount: 0,
replies: [],
mentions: [],
@ -62,22 +65,13 @@ export class Navbar extends Component<any, NavbarState> {
admins: [],
searchParam: '',
toggleSearch: false,
siteLoading: true,
};
constructor(props: any, context: any) {
super(props, context);
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
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
@ -86,17 +80,30 @@ export class Navbar extends Component<any, NavbarState> {
() => 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();
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) {
i.state.searchParam = event.target.value;
i.setState(i.state);
@ -145,183 +152,203 @@ export class Navbar extends Component<any, NavbarState> {
componentWillUnmount() {
this.wsSub.unsubscribe();
this.userSub.unsubscribe();
this.unreadCountSub.unsubscribe();
}
// TODO class active corresponding to current page
navbar() {
return (
<nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
<Link title={this.state.version} class="navbar-brand" to="/">
{this.state.siteName}
</Link>
{this.state.isLoggedIn && (
<Link
class="ml-auto p-0 navbar-toggler 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>
)}
<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>
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
<div class="container">
{!this.state.siteLoading ? (
<Link title={this.state.version} class="navbar-brand" to="/">
{this.state.siteName}
</Link>
) : (
<div class="navbar-item">
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</div>
)}
<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>
)}
{this.state.isLoggedIn && (
<Link
class="ml-auto p-0 navbar-toggler nav-link border-0"
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>
)}
<button
class="navbar-toggler border-0"
type="button"
aria-label="menu"
onClick={linkEvent(this, this.expandNavbar)}
data-tippy-content={i18n.t('expand_here')}
>
<span class="navbar-toggler-icon"></span>
</button>
{!this.state.siteLoading && (
<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>
</ul>
<ul class="navbar-nav">
<li className="nav-item">
<Link
class="nav-link"
to={`/u/${UserService.Instance.user.username}`}
title={i18n.t('settings')}
to="/sponsors"
title={i18n.t('donate_to_lemmy')}
>
<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.username}
</span>
<svg class="icon">
<use xlinkHref="#icon-coffee"></use>
</svg>
</Link>
</li>
</ul>
</>
) : (
<ul class="navbar-nav my-2">
<li className="nav-item">
<Link
class="nav-link"
to="/login"
title={i18n.t('login_sign_up')}
{!this.context.router.history.location.pathname.match(
/^\/search/
) && (
<form
class="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
{i18n.t('login_sign_up')}
</Link>
</li>
</ul>
<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.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>
</nav>
@ -398,38 +425,53 @@ export class Navbar extends Component<any, NavbarState> {
this.state.siteName = data.site.name;
this.state.version = data.version;
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() {
if (this.state.isLoggedIn) {
let repliesForm: GetRepliesForm = {
sort: SortType[SortType.New],
unread_only: true,
page: 1,
limit: fetchLimit,
};
console.log('Fetching unreads...');
let repliesForm: GetRepliesForm = {
sort: SortType[SortType.New],
unread_only: true,
page: 1,
limit: fetchLimit,
};
let userMentionsForm: GetUserMentionsForm = {
sort: SortType[SortType.New],
unread_only: true,
page: 1,
limit: fetchLimit,
};
let userMentionsForm: GetUserMentionsForm = {
sort: SortType[SortType.New],
unread_only: true,
page: 1,
limit: fetchLimit,
};
let privateMessagesForm: GetPrivateMessagesForm = {
unread_only: true,
page: 1,
limit: fetchLimit,
};
let privateMessagesForm: GetPrivateMessagesForm = {
unread_only: true,
page: 1,
limit: fetchLimit,
};
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
WebSocketService.Instance.getUserMentions(userMentionsForm);
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
}
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
WebSocketService.Instance.getUserMentions(userMentionsForm);
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
}
}
@ -438,10 +480,7 @@ export class Navbar extends Component<any, NavbarState> {
}
sendUnreadCount() {
UserService.Instance.user.unreadCount = this.state.unreadCount;
UserService.Instance.sub.next({
user: UserService.Instance.user,
});
UserService.Instance.unreadCountSub.next(this.state.unreadCount);
}
calculateUnreadCount(): number {

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -7,6 +8,7 @@ import {
PasswordChangeForm,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
@ -15,6 +17,7 @@ import { i18n } from '../i18next';
interface State {
passwordChangeForm: PasswordChangeForm;
loading: boolean;
site: Site;
}
export class PasswordChange extends Component<any, State> {
@ -27,6 +30,7 @@ export class PasswordChange extends Component<any, State> {
password_verify: undefined,
},
loading: false,
site: undefined,
};
constructor(props: any, context: any) {
@ -48,9 +52,18 @@ export class PasswordChange extends Component<any, State> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site) {
return `${i18n.t('password_change')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('password_change')}</h5>
@ -142,7 +155,8 @@ export class PasswordChange extends Component<any, State> {
this.props.history.push('/');
} else if (res.op == UserOperation.GetSite) {
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,
auth: null,
community_id: null,
creator_id: UserService.Instance.user
? UserService.Instance.user.id
: null,
},
communities: [],
loading: false,
@ -99,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: this.props.post.name,
community_id: this.props.post.community_id,
edit_id: this.props.post.id,
creator_id: this.props.post.creator_id,
url: this.props.post.url,
nsfw: this.props.post.nsfw,
auth: null,

View File

@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services';
import {
Post,
CreatePostLikeForm,
PostForm as PostFormI,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm,
CommunityUser,
UserView,
@ -33,7 +36,6 @@ import {
setupTippy,
hostname,
previewLines,
toast,
} from '../utils';
import { i18n } from '../i18next';
@ -238,9 +240,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
title={post.url}
rel="noopener"
>
<svg class="icon thumbnail">
<use xlinkHref="#icon-external-link"></use>
</svg>
<div class="thumbnail rounded bg-light d-flex justify-content-center">
<svg class="icon d-flex align-items-center">
<use xlinkHref="#icon-external-link"></use>
</svg>
</div>
</a>
);
}
@ -251,9 +255,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
to={`/post/${post.id}`}
title={i18n.t('comments')}
>
<svg class="icon thumbnail">
<use xlinkHref="#icon-message-square"></use>
</svg>
<div class="thumbnail rounded bg-light d-flex justify-content-center">
<svg class="icon d-flex align-items-center">
<use xlinkHref="#icon-message-square"></use>
</svg>
</div>
</Link>
);
}
@ -296,7 +302,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)}
</div>
{!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>
)}
@ -560,7 +566,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleSavePostClick)}
data-tippy-content={
post.saved ? i18n.t('unsave') : i18n.t('save')
@ -577,7 +583,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<Link
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
to={`/create_post${this.crossPostParams}`}
title={i18n.t('cross_post')}
>
@ -592,7 +598,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
>
@ -603,7 +609,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={
!post.deleted
@ -626,7 +632,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{!this.state.showAdvanced && this.props.showBody ? (
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleShowAdvanced)}
data-tippy-content={i18n.t('more')}
>
@ -640,7 +646,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.props.showBody && post.body && (
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
>
@ -658,7 +664,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModLock)}
data-tippy-content={
post.locked
@ -677,7 +683,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModSticky)}
data-tippy-content={
post.stickied
@ -1114,18 +1120,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
handleDeleteClick(i: PostListing) {
let deleteForm: PostFormI = {
body: i.props.post.body,
community_id: i.props.post.community_id,
name: i.props.post.name,
url: i.props.post.url,
let deleteForm: DeletePostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
auth: null,
};
WebSocketService.Instance.editPost(deleteForm);
WebSocketService.Instance.deletePost(deleteForm);
}
handleSavePostClick(i: PostListing) {
@ -1163,46 +1163,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: RemovePostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.removePost(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handleModLock(i: PostListing) {
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: LockPostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
locked: !i.props.post.locked,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.lockPost(form);
}
handleModSticky(i: PostListing) {
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: StickyPostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
stickied: !i.props.post.stickied,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.stickyPost(form);
}
handleModBanFromCommunityShow(i: PostListing) {

View File

@ -32,7 +32,7 @@ export class PostListings extends Component<PostListingsProps, any> {
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
<hr class="my-2" />
<hr class="my-3" />
</>
))
) : (

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -8,7 +9,7 @@ import {
GetPostResponse,
PostResponse,
Comment,
CommentForm as CommentFormI,
MarkCommentAsReadForm,
CommentResponse,
CommentSortType,
CommentViewType,
@ -168,26 +169,30 @@ export class Post extends Component<any, PostState> {
UserService.Instance.user &&
UserService.Instance.user.id == parent_user_id
) {
let form: CommentFormI = {
content: found.content,
let form: MarkCommentAsReadForm = {
edit_id: found.id,
creator_id: found.creator_id,
post_id: found.post_id,
parent_id: found.parent_id,
read: true,
auth: null,
};
WebSocketService.Instance.editComment(form);
UserService.Instance.user.unreadCount--;
UserService.Instance.sub.next({
user: UserService.Instance.user,
});
WebSocketService.Instance.markCommentAsRead(form);
UserService.Instance.unreadCountSub.next(
UserService.Instance.unreadCountSub.value - 1
);
}
}
get documentTitle(): string {
if (this.state.post) {
return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5>
<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">
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Hot && 'active'
}`}
>
@ -242,7 +247,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Top && 'active'
}`}
>
@ -255,7 +260,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.New && 'active'
}`}
>
@ -268,7 +273,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Old && 'active'
}`}
>
@ -283,7 +288,7 @@ export class Post extends Component<any, PostState> {
</div>
<div class="btn-group btn-group-toggle mb-2">
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentViewType === CommentViewType.Chat && 'active'
}`}
>
@ -409,10 +414,8 @@ export class Post extends Component<any, PostState> {
this.state.comments = data.comments;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.state.online = data.online;
this.state.loading = false;
document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
// Get cross-posts
if (this.state.post.url) {
@ -436,7 +439,11 @@ export class Post extends Component<any, PostState> {
this.state.comments.unshift(data.comment);
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;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -453,7 +460,13 @@ export class Post extends Component<any, PostState> {
let data = res.data as PostResponse;
createPostLikeRes(data, this.state.post);
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;
this.state.post = data.post;
this.setState(this.state);
@ -463,7 +476,11 @@ export class Post extends Component<any, PostState> {
this.state.post = data.post;
this.setState(this.state);
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;
this.state.community = data.community;
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;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.setState(this.state);
}
}

View File

@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component<
this.state.loading = false;
this.setState(this.state);
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;
this.state.loading = false;
this.props.onEdit(data.message);

View File

@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import {
PrivateMessage as PrivateMessageI,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
@ -130,7 +131,7 @@ export class PrivateMessage extends Component<
<>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={
message.read
@ -149,7 +150,7 @@ export class PrivateMessage extends Component<
</li>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t('reply')}
>
@ -164,7 +165,7 @@ export class PrivateMessage extends Component<
<>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
>
@ -175,7 +176,7 @@ export class PrivateMessage extends Component<
</li>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={
!message.deleted
@ -196,7 +197,7 @@ export class PrivateMessage extends Component<
)}
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
>
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
}
handleDeleteClick(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: DeletePrivateMessageForm = {
edit_id: i.props.privateMessage.id,
deleted: !i.props.privateMessage.deleted,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.deletePrivateMessage(form);
}
handleReplyCancel() {
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
}
handleMarkRead(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: MarkPrivateMessageAsReadForm = {
edit_id: i.props.privateMessage.id,
read: !i.props.privateMessage.read,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.markPrivateMessageAsRead(form);
}
handleMessageCollapse(i: PrivateMessage) {

View File

@ -1,5 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
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() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<h5>{i18n.t('search')}</h5>
{this.selects()}
{this.searchForm()}
@ -207,7 +222,7 @@ export class Search extends Component<any, SearchState> {
<select
value={this.state.type_}
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 value={SearchType.All}>{i18n.t('all')}</option>
@ -402,7 +417,7 @@ export class Search extends Component<any, SearchState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -411,7 +426,7 @@ export class Search extends Component<any, SearchState> {
{this.resultsCount() > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -500,9 +515,6 @@ export class Search extends Component<any, SearchState> {
let data = res.data as SearchResponse;
this.state.searchResponse = data;
this.state.loading = false;
document.title = `${i18n.t('search')} - ${this.state.q} - ${
this.state.site.name
}`;
window.scrollTo(0, 0);
this.setState(this.state);
} else if (res.op == UserOperation.CreateCommentLike) {
@ -517,7 +529,6 @@ export class Search extends Component<any, SearchState> {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('search')} - ${data.site.name}`;
}
}
}

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -51,13 +52,14 @@ export class Setup extends Component<any, State> {
this.subscription.unsubscribe();
}
componentDidMount() {
document.title = `${i18n.t('setup')} - Lemmy`;
get documentTitle(): string {
return `${i18n.t('setup')} - Lemmy`;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 offset-lg-3 col-lg-6">
<h3>{i18n.t('lemmy_instance_setup')}</h3>

View File

@ -4,7 +4,8 @@ import {
Community,
CommunityUser,
FollowCommunityForm,
CommunityForm as CommunityFormI,
DeleteCommunityForm,
RemoveCommunityForm,
UserView,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
@ -74,7 +75,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5 className="mb-0">
<span>{community.title}</span>
@ -176,33 +177,33 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)}
<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 })}
</li>
*/}
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_subscribers', {
count: community.number_of_subscribers,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_posts', {
count: community.number_of_posts,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_comments', {
count: community.number_of_comments,
})}
</li>
<li className="list-inline-item">
<Link className="badge badge-secondary" to="/communities">
<Link className="badge badge-light" to="/communities">
{community.category_name}
</Link>
</li>
<li className="list-inline-item">
<Link
className="badge badge-secondary"
className="badge badge-light"
to={`/modlog/community/${this.props.community.id}`}
>
{i18n.t('modlog')}
@ -227,7 +228,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</ul>
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
<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'
}`}
to={`/create_post?community=${community.name}`}
@ -237,14 +238,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div>
{community.subscribed ? (
<button
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
onClick={linkEvent(community.id, this.handleUnsubscribe)}
>
{i18n.t('unsubscribe')}
</button>
) : (
<button
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
onClick={linkEvent(community.id, this.handleSubscribe)}
>
{i18n.t('subscribe')}
@ -254,7 +255,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</div>
</div>
{community.description && (
<div class="card border-secondary">
<div class="card bg-transparent border-secondary">
<div class="card-body">
<div
className="md-div"
@ -284,16 +285,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleDeleteClick(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
let deleteForm: DeleteCommunityForm = {
edit_id: i.props.community.id,
deleted: !i.props.community.deleted,
nsfw: i.props.community.nsfw,
auth: null,
};
WebSocketService.Instance.editCommunity(deleteForm);
WebSocketService.Instance.deleteCommunity(deleteForm);
}
handleUnsubscribe(communityId: number) {
@ -350,18 +346,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleModRemoveSubmit(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
let removeForm: RemoveCommunityForm = {
edit_id: i.props.community.id,
removed: !i.props.community.removed,
reason: i.state.removeReason,
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.setState(i.state);

View File

@ -35,7 +35,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<select
value={this.state.sort}
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>
{!this.props.hideHot && (

View File

@ -1,9 +1,11 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { WebSocketService } from '../services';
import {
GetSiteResponse,
Site,
WebSocketJsonResponse,
UserOperation,
} from '../interfaces';
@ -17,6 +19,7 @@ interface SilverUser {
}
let general = [
'William Moore',
'Rachel Schmitz',
'comradeda',
'ybaumy',
@ -31,7 +34,7 @@ let general = [
'Andre Vallestero',
'NotTooHighToHack',
];
let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
let highlighted = ['DQW', 'DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
let silver: Array<SilverUser> = [
{
name: 'Redjoker',
@ -41,10 +44,18 @@ let silver: Array<SilverUser> = [
// let gold = [];
// let latinum = [];
export class Sponsors extends Component<any, any> {
interface SponsorsState {
site: Site;
}
export class Sponsors extends Component<any, SponsorsState> {
private subscription: Subscription;
private emptyState: SponsorsState = {
site: undefined,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
@ -64,9 +75,18 @@ export class Sponsors extends Component<any, any> {
this.subscription.unsubscribe();
}
get documentTitle(): string {
if (this.state.site) {
return `${i18n.t('sponsors')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container text-center">
<Helmet title={this.documentTitle} />
{this.topMessage()}
<hr />
{this.sponsors()}
@ -108,7 +128,7 @@ export class Sponsors extends Component<any, any> {
<div class="container">
<h5>{i18n.t('sponsors')}</h5>
<p>{i18n.t('silver_sponsors')}</p>
<div class="row card-columns">
<div class="row justify-content-md-center card-columns">
{silver.map(s => (
<div class="card col-12 col-md-2">
<div>
@ -124,7 +144,7 @@ export class Sponsors extends Component<any, any> {
))}
</div>
<p>{i18n.t('general_sponsors')}</p>
<div class="row card-columns">
<div class="row justify-content-md-center card-columns">
{highlighted.map(s => (
<div class="card bg-primary col-12 col-md-2 font-weight-bold">
<div>{s}</div>
@ -182,7 +202,8 @@ export class Sponsors extends Component<any, any> {
return;
} else if (res.op == UserOperation.GetSite) {
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 { WebSocketService, UserService } from '../services';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take, last } from 'rxjs/operators';
import { retryWhen, delay, take } from 'rxjs/operators';
import { i18n } from '../i18next';
import {
UserOperation,
@ -16,7 +16,6 @@ import {
CommentResponse,
BanUserResponse,
PostResponse,
AddAdminResponse,
} from '../interfaces';
import {
wsJsonToRes,
@ -41,6 +40,7 @@ interface UserDetailsProps {
enableNsfw: boolean;
view: UserDetailsView;
onPageChange(page: number): number | any;
admins: Array<UserView>;
}
interface UserDetailsState {
@ -49,7 +49,6 @@ interface UserDetailsState {
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
admins: Array<UserView>;
}
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
@ -63,7 +62,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
comments: [],
posts: [],
saved: [],
admins: [],
};
this.subscription = WebSocketService.Instance.subject
@ -148,25 +146,30 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
return (
<div>
{combined.map(i => (
<div>
{i.type === 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
/>
)}
</div>
<>
<div>
{i.type === 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.props.admins}
noBorder
noIndent
showCommunity
showContext
enableDownvotes={this.props.enableDownvotes}
/>
)}
</div>
<hr class="my-3" />
</>
))}
</div>
);
@ -177,8 +180,9 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins}
admins={this.props.admins}
noIndent
showCommunity
showContext
enableDownvotes={this.props.enableDownvotes}
/>
@ -190,13 +194,16 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
return (
<div>
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
<>
<PostListing
post={post}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
<hr class="my-3" />
</>
))}
</div>
);
@ -207,7 +214,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div class="my-2">
{this.props.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -215,7 +222,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -252,7 +259,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
follows: data.follows,
moderates: data.moderates,
posts: data.posts,
admins: data.admins,
});
} else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse;
@ -260,7 +266,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
this.setState({
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;
editCommentRes(data, this.state.comments);
this.setState({
@ -298,11 +308,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
posts: this.state.posts,
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 (
<>
<Link className="text-body font-weight-bold" to={link}>
<Link className="text-info" to={link}>
{user.avatar && showAvatars() && (
<img
height="32"
width="32"
style="width: 2rem; height: 2rem;"
src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>

View File

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
@ -13,9 +14,9 @@ import {
DeleteAccountForm,
WebSocketJsonResponse,
GetSiteResponse,
Site,
UserDetailsView,
UserDetailsResponse,
AddAdminResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@ -54,7 +55,7 @@ interface UserState {
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm;
site: Site;
siteRes: GetSiteResponse;
}
interface UserProps {
@ -114,19 +115,25 @@ export class User extends Component<any, UserState> {
deleteAccountForm: {
password: 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,
siteRes: {
admins: [],
banned: [],
online: 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,
},
version: undefined,
},
};
@ -201,13 +208,21 @@ export class User extends Component<any, UserState> {
// Couldnt get a refresh working. This does for now.
location.reload();
}
document.title = `/u/${this.state.username} - ${this.state.site.name}`;
setupTippy();
}
get documentTitle(): string {
if (this.state.siteRes.site.name) {
return `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<div class="row">
<div class="col-12 col-md-8">
<h5>
@ -236,8 +251,9 @@ export class User extends Component<any, UserState> {
sort={SortType[this.state.sort]}
page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
admins={this.state.siteRes.admins}
view={this.state.view}
onPageChange={this.handlePageChange}
/>
@ -260,7 +276,7 @@ export class User extends Component<any, UserState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Overview && 'active'}
`}
>
@ -273,7 +289,7 @@ export class User extends Component<any, UserState> {
{i18n.t('overview')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Comments && 'active'}
`}
>
@ -286,7 +302,7 @@ export class User extends Component<any, UserState> {
{i18n.t('comments')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Posts && 'active'}
`}
>
@ -299,7 +315,7 @@ export class User extends Component<any, UserState> {
{i18n.t('posts')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Saved && 'active'}
`}
>
@ -344,7 +360,7 @@ export class User extends Component<any, UserState> {
let user = this.state.user;
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>
<ul class="list-inline mb-0">
@ -441,7 +457,7 @@ export class User extends Component<any, UserState> {
userSettings() {
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('settings')}</h5>
<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"
>
{!this.checkSettingsAvatar ? (
<span class="btn btn-sm btn-secondary">
<span class="btn btn-secondary">
{i18n.t('upload_avatar')}
</span>
) : (
@ -493,7 +509,7 @@ export class User extends Component<any, UserState> {
<select
value={this.state.userSettingsForm.lang}
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 value="browser">{i18n.t('browser_default')}</option>
@ -508,7 +524,7 @@ export class User extends Component<any, UserState> {
<select
value={this.state.userSettingsForm.theme}
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>
{themes.map(theme => (
@ -637,7 +653,7 @@ export class User extends Component<any, UserState> {
/>
</div>
</div>
{this.state.site.enable_nsfw && (
{this.state.siteRes.site.enable_nsfw && (
<div class="form-group">
<div class="form-check">
<input
@ -769,7 +785,7 @@ export class User extends Component<any, UserState> {
return (
<div>
{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">
<h5>{i18n.t('moderates')}</h5>
<ul class="list-unstyled mb-0">
@ -792,7 +808,7 @@ export class User extends Component<any, UserState> {
return (
<div>
{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">
<h5>{i18n.t('subscribed')}</h5>
<ul class="list-unstyled mb-0">
@ -1063,9 +1079,12 @@ export class User extends Component<any, UserState> {
this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) {
const data = res.data as GetSiteResponse;
this.setState({
site: data.site,
});
this.state.siteRes = data;
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;
}
i18next.init({
debug: false,
// load: 'languageOnly',
// initImmediate: false,
lng: getLanguage(),
fallbackLng: 'en',
resources,
interpolation: { format },
});
export function i18nextSetup() {
i18next.init({
debug: false,
// load: 'languageOnly',
// initImmediate: false,
lng: getLanguage(),
fallbackLng: 'en',
resources,
interpolation: { format },
});
}
export { i18next as i18n, resources };

154
ui/src/interfaces.ts vendored
View File

@ -9,19 +9,28 @@ export enum UserOperation {
GetCommunity,
CreateComment,
EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost,
EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
MarkUserMentionAsRead,
GetModlog,
BanFromCommunity,
AddModToCommunity,
@ -40,6 +49,8 @@ export enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,
@ -89,18 +100,33 @@ export enum SearchType {
Url,
}
export interface User {
export interface Claims {
id: number;
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;
theme: string;
default_sort_type: SortType;
default_listing_type: ListingType;
lang: string;
avatar?: string;
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 {
@ -355,9 +381,9 @@ export interface GetUserMentionsResponse {
mentions: Array<Comment>;
}
export interface EditUserMentionForm {
export interface MarkUserMentionAsReadForm {
user_mention_id: number;
read?: boolean;
read: boolean;
auth?: string;
}
@ -571,13 +597,23 @@ export interface UserSettingsForm {
export interface CommunityForm {
name: string;
edit_id?: number;
title: string;
description?: string;
category_id: number;
edit_id?: number;
removed?: boolean;
deleted?: 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;
expires?: number;
auth?: string;
@ -592,7 +628,6 @@ export interface GetCommunityForm {
export interface GetCommunityResponse {
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number;
}
@ -619,19 +654,37 @@ export interface PostForm {
name: string;
url?: string;
body?: string;
community_id: number;
updated?: number;
community_id?: number;
edit_id?: number;
creator_id: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean;
locked?: boolean;
stickied?: boolean;
auth: string;
}
export interface DeletePostForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemovePostForm {
edit_id: number;
removed: boolean;
reason?: 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 {
name: string;
url?: string;
@ -649,7 +702,6 @@ export interface GetPostResponse {
comments: Array<Comment>;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number;
}
@ -665,14 +717,30 @@ export interface PostResponse {
export interface CommentForm {
content: string;
post_id: number;
post_id?: number;
parent_id?: number;
edit_id?: number;
creator_id?: number;
removed?: boolean;
deleted?: boolean;
form_id?: string;
auth: string;
}
export interface DeleteCommentForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemoveCommentForm {
edit_id: number;
removed: boolean;
reason?: string;
read?: boolean;
auth: string;
}
export interface MarkCommentAsReadForm {
edit_id: number;
read: boolean;
auth: string;
}
@ -685,11 +753,11 @@ export interface SaveCommentForm {
export interface CommentResponse {
comment: Comment;
recipient_ids: Array<number>;
form_id?: string;
}
export interface CommentLikeForm {
comment_id: number;
post_id: number;
score: number;
auth?: string;
}
@ -744,6 +812,10 @@ export interface GetSiteConfig {
auth?: string;
}
export interface GetSiteForm {
auth?: string;
}
export interface GetSiteConfigResponse {
config_hjson: string;
}
@ -759,6 +831,7 @@ export interface GetSiteResponse {
banned: Array<UserView>;
online: number;
version: string;
my_user?: User;
}
export interface SiteResponse {
@ -835,9 +908,19 @@ export interface PrivateMessageFormParams {
export interface EditPrivateMessageForm {
edit_id: number;
content?: string;
deleted?: boolean;
read?: boolean;
content: string;
auth?: string;
}
export interface DeletePrivateMessageForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface MarkPrivateMessageAsReadForm {
edit_id: number;
read: boolean;
auth?: string;
}
@ -865,18 +948,26 @@ export interface UserJoinResponse {
}
export type MessageType =
| EditPrivateMessageForm
| LoginForm
| RegisterForm
| CommunityForm
| DeleteCommunityForm
| RemoveCommunityForm
| FollowCommunityForm
| ListCommunitiesForm
| GetFollowedCommunitiesForm
| PostForm
| DeletePostForm
| RemovePostForm
| LockPostForm
| StickyPostForm
| GetPostForm
| GetPostsForm
| GetCommunityForm
| CommentForm
| DeleteCommentForm
| RemoveCommentForm
| MarkCommentAsReadForm
| CommentLikeForm
| SaveCommentForm
| CreatePostLikeForm
@ -891,7 +982,7 @@ export type MessageType =
| GetUserDetailsForm
| GetRepliesForm
| GetUserMentionsForm
| EditUserMentionForm
| MarkUserMentionAsReadForm
| GetModlogForm
| SiteForm
| SearchForm
@ -901,6 +992,8 @@ export type MessageType =
| PasswordChangeForm
| PrivateMessageForm
| EditPrivateMessageForm
| DeletePrivateMessageForm
| MarkPrivateMessageAsReadForm
| GetPrivateMessagesForm
| SiteConfigForm;
@ -925,7 +1018,8 @@ type ResponseType =
| AddAdminResponse
| PrivateMessageResponse
| PrivateMessagesResponse
| GetSiteConfigResponse;
| GetSiteConfigResponse
| GetSiteResponse;
export interface WebSocketResponse {
op: UserOperation;

View File

@ -1,20 +1,22 @@
import Cookies from 'js-cookie';
import { User, LoginResponse } from '../interfaces';
import { User, Claims, LoginResponse } from '../interfaces';
import { setTheme } from '../utils';
import jwt_decode from 'jwt-decode';
import { Subject } from 'rxjs';
import { Subject, BehaviorSubject } from 'rxjs';
export class UserService {
private static _instance: UserService;
public user: User;
public sub: Subject<{ user: User }> = new Subject<{
user: User;
}>();
public claims: Claims;
public jwtSub: Subject<string> = new Subject<string>();
public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
0
);
private constructor() {
let jwt = Cookies.get('jwt');
if (jwt) {
this.setUser(jwt);
this.setClaims(jwt);
} else {
setTheme();
console.log('No JWT cookie found.');
@ -22,16 +24,17 @@ export class UserService {
}
public login(res: LoginResponse) {
this.setUser(res.jwt);
this.setClaims(res.jwt);
Cookies.set('jwt', res.jwt, { expires: 365 });
console.log('jwt cookie set');
}
public logout() {
this.claims = undefined;
this.user = undefined;
Cookies.remove('jwt');
setTheme();
this.sub.next({ user: undefined });
this.jwtSub.next();
console.log('Logged out.');
}
@ -39,11 +42,9 @@ export class UserService {
return Cookies.get('jwt');
}
private setUser(jwt: string) {
this.user = jwt_decode(jwt);
setTheme(this.user.theme, true);
this.sub.next({ user: this.user });
console.log(this.user);
private setClaims(jwt: string) {
this.claims = jwt_decode(jwt);
this.jwtSub.next(jwt);
}
public static get Instance() {

View File

@ -4,9 +4,18 @@ import {
RegisterForm,
UserOperation,
CommunityForm,
DeleteCommunityForm,
RemoveCommunityForm,
PostForm,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm,
CommentForm,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentAsReadForm,
SaveCommentForm,
CommentLikeForm,
GetPostForm,
@ -28,7 +37,7 @@ import {
UserView,
GetRepliesForm,
GetUserMentionsForm,
EditUserMentionForm,
MarkUserMentionAsReadForm,
SearchForm,
UserSettingsForm,
DeleteAccountForm,
@ -36,10 +45,13 @@ import {
PasswordChangeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
GetPrivateMessagesForm,
GetCommentsForm,
UserJoinForm,
GetSiteConfig,
GetSiteForm,
SiteConfigForm,
MessageType,
WebSocketJsonResponse,
@ -103,18 +115,24 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
}
public createCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm);
this.ws.send(
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
);
public createCommunity(form: CommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
}
public editCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm);
this.ws.send(
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
);
public editCommunity(form: CommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form));
}
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) {
@ -140,9 +158,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
}
public createPost(postForm: PostForm) {
this.setAuth(postForm);
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm));
public createPost(form: PostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form));
}
public getPost(form: GetPostForm) {
@ -155,14 +173,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
}
public createComment(commentForm: CommentForm) {
this.setAuth(commentForm);
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
public createComment(form: CommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form));
}
public editComment(commentForm: CommentForm) {
this.setAuth(commentForm);
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm));
public editComment(form: CommentForm) {
this.setAuth(form);
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) {
@ -190,9 +223,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
}
public editPost(postForm: PostForm) {
this.setAuth(postForm);
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm));
public editPost(form: PostForm) {
this.setAuth(form);
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) {
@ -245,9 +298,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
}
public editUserMention(form: EditUserMentionForm) {
public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form));
this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form));
}
public getModlog(form: GetModlogForm) {
@ -264,8 +317,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
}
public getSite() {
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
public getSite(form: GetSiteForm = {}) {
this.setAuth(form, false);
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, form));
}
public getSiteConfig() {
@ -315,6 +369,18 @@ export class WebSocketService {
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) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));

View File

@ -256,6 +256,7 @@
"couldnt_save_post": "Couldn't save post.",
"no_slurs": "No slurs.",
"not_an_admin": "Not an admin.",
"not_a_moderator": "Not a moderator.",
"site_already_exists": "Site already exists.",
"couldnt_update_site": "Couldn't update site.",
"couldnt_find_that_username_or_email":

View File

@ -46,7 +46,7 @@
"url": "URL",
"chat": "Txata",
"your_site": "zure gunea",
"nsfw": "NSFW (eduki hunkigarria)",
"nsfw": "NSFW (eduki hunkigarriak)",
"block_leaving": "Ziur al zaude atera nahi duzula?",
"bitcoin": "Bitcoin",
"ethereum": "Ethereum",
@ -117,14 +117,14 @@
"remove_community": "Ezabatu komunitatea",
"subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
"trending_communities": "<1>Komunitateen</1> joerak",
"list_of_communities": "Komunitate-zerrenda",
"list_of_communities": "Komunitateen zerrenda",
"community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
"create_private_message": "Sortu mezu pribatua",
"cancel": "Ezeztatu",
"stickied": "finkatuta",
"reason": "Arrazoia",
"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.",
"restore": "leheneratu",
"unban_from_site": "kendu debekua gunean",
@ -166,7 +166,7 @@
"reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
"no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
"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",
"optional": "Hautazkoa",
"browser_default": "Nabigatzaileko lehenetsia",
@ -180,7 +180,7 @@
"number_of_upvotes_plural": "{{count}} aldeko bozka",
"open_registration": "Izen-ematea irekia",
"registration_closed": "Izen-ematea itxira",
"enable_nsfw": "Gaitu NSFW (eduki hunkigarria)",
"enable_nsfw": "Gaitu NSFW (eduki hunkigarriak)",
"body": "Gorputza",
"copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
"community": "Komunitatea",
@ -193,7 +193,7 @@
"lemmy_instance_setup": "Lemmy instantziaren ezarpena",
"setup_admin": "Ezarri gunearen administratzailea",
"modified": "aldatuta",
"show_nsfw": "Erakutsi eduki hunkigarria (NSFW)",
"show_nsfw": "Erakutsi eduki hunkigarriak (NSFW)",
"expires": "Noiz iraungitzen da:",
"theme": "Itxura",
"sponsors": "Babesleak",
@ -204,19 +204,19 @@
"support_on_open_collective": "OpenCollective bitartez lagundu",
"donate_to_lemmy": "Egin dohaintza bat Lemmyri",
"donate": "Dohaintza egin",
"general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman zizkiotenak dira.",
"silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman zizkiotenak dira.",
"general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman dizkiotenak dira.",
"silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman dizkiotenak dira.",
"crypto": "Kriptomonetak",
"code": "Kodea",
"joined": "Batuta",
"by": "egilea",
"to": "nori",
"by": "egilea:",
"to": "non:",
"from": "nork",
"transfer_community": "transferentzia-komunitatea",
"transfer_site": "transferentzia-gunea",
"are_you_sure": "ziur al zaude?",
"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.",
"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. <14></14> <15>Eskerrak ematen dizkiegu gure laguntzaileei: </15> dessalines, Nutomic, asonix, zacanger eta iav.",
"logged_in": "Saioa hasi duzu.",
"not_logged_in": "Ez duzu saiorik hasi.",
"site_saved": "Gunea gorde da.",
@ -257,5 +257,21 @@
"couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
"emoji_picker": "Emoji hautagailua",
"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",
"top": "Top",
"api": "API",
"docs": "Docs",
"docs": "Documentação",
"inbox": "Caixa de entrada",
"inbox_for": "Caixa de entrada de <1>{{user}}</1>",
"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.",
"invalid_post_title": "Título de publicação inválido",
"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": "Сборщик эмодзи",
"select_a_community": "Выбрать сообщество",
"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.",
"matrix_user_id": "Matrix-användare",
"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",
"language": "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.",
"what_is": "Vad är",
"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