mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 11:21:32 +00:00
Merge branch 'main' into inbox-refactoring
This commit is contained in:
commit
cee72065e9
39 changed files with 1474 additions and 489 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -16,3 +16,5 @@ ui/src/translations
|
||||||
|
|
||||||
# ide config
|
# ide config
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
target
|
||||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
||||||
v0.7.30
|
v0.7.33
|
||||||
|
|
3
docker/dev/Dockerfile
vendored
3
docker/dev/Dockerfile
vendored
|
@ -41,6 +41,9 @@ FROM alpine:3.12
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
|
# Install Espeak for captchas
|
||||||
|
RUN apk add espeak
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
|
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
|
||||||
|
|
4
docker/prod/Dockerfile
vendored
4
docker/prod/Dockerfile
vendored
|
@ -50,6 +50,10 @@ FROM alpine:3.12 as lemmy
|
||||||
|
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
|
# Install Espeak for captchas
|
||||||
|
RUN apk add espeak
|
||||||
|
|
||||||
RUN addgroup -g 1000 lemmy
|
RUN addgroup -g 1000 lemmy
|
||||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
|
|
||||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.7.30
|
image: dessalines/lemmy:v0.7.33
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
4
docker/travis/docker_push.sh
vendored
4
docker/travis/docker_push.sh
vendored
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||||
docker tag dessalines/lemmy:travis \
|
docker tag dessalines/lemmy:travis \
|
||||||
dessalines/lemmy:v0.7.30
|
dessalines/lemmy:v0.7.33
|
||||||
docker push dessalines/lemmy:v0.7.30
|
docker push dessalines/lemmy:v0.7.33
|
||||||
|
|
32
docs/src/contributing_websocket_http_api.md
vendored
32
docs/src/contributing_websocket_http_api.md
vendored
|
@ -390,7 +390,9 @@ Only the first user will be able to be the admin.
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
password: String,
|
password: String,
|
||||||
password_verify: String,
|
password_verify: String,
|
||||||
admin: bool
|
admin: bool,
|
||||||
|
captcha_uuid: Option<String>, // Only checked if these are enabled in the server
|
||||||
|
captcha_answer: Option<String>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -408,6 +410,34 @@ Only the first user will be able to be the admin.
|
||||||
|
|
||||||
`POST /user/register`
|
`POST /user/register`
|
||||||
|
|
||||||
|
#### Get Captcha
|
||||||
|
|
||||||
|
These expire after 10 minutes.
|
||||||
|
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "GetCaptcha",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "GetCaptcha",
|
||||||
|
data: {
|
||||||
|
ok?: { // Will be undefined if captchas are disabled
|
||||||
|
png: String, // A Base64 encoded png
|
||||||
|
wav: Option<String>, // A Base64 encoded wav audio file
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /user/get_captcha`
|
||||||
|
|
||||||
#### Get User Details
|
#### Get User Details
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
|
295
server/Cargo.lock
generated
vendored
295
server/Cargo.lock
generated
vendored
|
@ -31,7 +31,7 @@ checksum = "a9028932f36d45df020c92317ccb879ab77d8f066f57ff143dd5bee93ba3de0d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix_derive",
|
"actix_derive",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
@ -54,7 +54,7 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
@ -94,7 +94,7 @@ dependencies = [
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -120,7 +120,7 @@ dependencies = [
|
||||||
"actix-tls",
|
"actix-tls",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"brotli2",
|
"brotli2",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
|
@ -280,7 +280,7 @@ dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -382,6 +382,12 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler32"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.13"
|
version = "0.7.13"
|
||||||
|
@ -496,6 +502,15 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
|
checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -539,6 +554,12 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
@ -642,6 +663,12 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db7a1029718df60331e557c9e83a55523c955e5dd2a7bfeffad6bbd50b538ae9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.3.4"
|
version = "1.3.4"
|
||||||
|
@ -663,6 +690,26 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "c_vec"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8a318911dce53b5f1ca6539c44f5342c632269f0fa7ea3e35f32458c27a7c30"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "captcha"
|
||||||
|
version = "0.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d060a3be43adb2fe89d3448e9a193149806139b1ce99281865fcab7aeaf04ed"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.5.2",
|
||||||
|
"image",
|
||||||
|
"lodepng",
|
||||||
|
"rand 0.3.23",
|
||||||
|
"serde_json",
|
||||||
|
"time 0.1.43",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.58"
|
version = "1.0.58"
|
||||||
|
@ -695,7 +742,7 @@ checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"strsim 0.8.0",
|
"strsim 0.8.0",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
@ -708,7 +755,7 @@ version = "0.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -717,9 +764,15 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
|
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_quant"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "comrak"
|
name = "comrak"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -806,6 +859,43 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"maybe-uninit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg 1.0.0",
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"lazy_static",
|
||||||
|
"maybe-uninit",
|
||||||
|
"memoffset",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-queue"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"maybe-uninit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -852,6 +942,16 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate"
|
||||||
|
version = "0.7.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
|
||||||
|
dependencies = [
|
||||||
|
"adler32",
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_builder"
|
name = "derive_builder"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -894,7 +994,7 @@ version = "1.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
|
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
|
@ -1072,6 +1172,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum_primitive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits 0.1.43",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1167,7 +1276,7 @@ version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"fuchsia-zircon-sys",
|
"fuchsia-zircon-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1311,6 +1420,16 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gif"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f"
|
||||||
|
dependencies = [
|
||||||
|
"color_quant",
|
||||||
|
"lzw",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
|
@ -1455,6 +1574,23 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c3f4f5ea213ed9899eca760a8a14091d4b82d33e27cf8ced336ff730e9f6da8"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"enum_primitive",
|
||||||
|
"gif",
|
||||||
|
"jpeg-decoder",
|
||||||
|
"num-iter",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits 0.1.43",
|
||||||
|
"png",
|
||||||
|
"scoped_threadpool",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -1465,6 +1601,12 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inflate"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1238524675af3938a7c74980899535854b88ba07907bb1c944abe5b8fc437e5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -1507,6 +1649,16 @@ version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jpeg-decoder"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.42"
|
version = "0.3.42"
|
||||||
|
@ -1585,6 +1737,7 @@ dependencies = [
|
||||||
"awc",
|
"awc",
|
||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
"captcha",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
@ -1673,7 +1826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
|
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"ryu",
|
"ryu",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
|
@ -1719,6 +1872,18 @@ dependencies = [
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lodepng"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ac1dfdf85b7d5dea61a620e12c051a72078189366a0b3c0ab331e30847def2f"
|
||||||
|
dependencies = [
|
||||||
|
"c_vec",
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"rgb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
|
@ -1737,6 +1902,12 @@ dependencies = [
|
||||||
"linked-hash-map 0.5.3",
|
"linked-hash-map 0.5.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzw"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maplit"
|
name = "maplit"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -1755,12 +1926,27 @@ version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maybe-uninit"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg 1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "migrations_internals"
|
name = "migrations_internals"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
@ -1920,6 +2106,27 @@ dependencies = [
|
||||||
"num-traits 0.2.12",
|
"num-traits 0.2.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg 1.0.0",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits 0.2.12",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.1.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits 0.2.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.1.43"
|
version = "0.1.43"
|
||||||
|
@ -1978,7 +2185,7 @@ version = "0.10.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
|
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
@ -2153,6 +2360,18 @@ version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
|
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48f397b84083c2753ba53c7b56ad023edb94512b2885ffe227c66ff7edb61868"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 0.7.0",
|
||||||
|
"deflate",
|
||||||
|
"inflate",
|
||||||
|
"num-iter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
@ -2225,6 +2444,16 @@ dependencies = [
|
||||||
"scheduled-thread-pool",
|
"scheduled-thread-pool",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand 0.4.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
@ -2385,6 +2614,31 @@ dependencies = [
|
||||||
"rand_core 0.3.1",
|
"rand_core 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg 1.0.0",
|
||||||
|
"crossbeam-deque",
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-queue",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"lazy_static",
|
||||||
|
"num_cpus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rdrand"
|
name = "rdrand"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -2437,6 +2691,15 @@ dependencies = [
|
||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90ef54b45ae131327a88597e2463fee4098ad6c88ba7b6af4b3987db8aad4098"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.15"
|
version = "0.16.15"
|
||||||
|
@ -2521,6 +2784,12 @@ dependencies = [
|
||||||
"parking_lot 0.11.0",
|
"parking_lot 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped_threadpool"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -2543,7 +2812,7 @@ version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
|
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.2.1",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
1
server/Cargo.toml
vendored
1
server/Cargo.toml
vendored
|
@ -51,3 +51,4 @@ itertools = "0.9.0"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
async-trait = "0.1.36"
|
async-trait = "0.1.36"
|
||||||
|
captcha = "0.0.7"
|
||||||
|
|
4
server/config/defaults.hjson
vendored
4
server/config/defaults.hjson
vendored
|
@ -59,6 +59,10 @@
|
||||||
# comma seperated list of instances with which federation is allowed
|
# comma seperated list of instances with which federation is allowed
|
||||||
allowed_instances: ""
|
allowed_instances: ""
|
||||||
}
|
}
|
||||||
|
captcha: {
|
||||||
|
enabled: true
|
||||||
|
difficulty: medium # Can be easy, medium, or hard
|
||||||
|
}
|
||||||
# # email sending configuration
|
# # email sending configuration
|
||||||
# email: {
|
# email: {
|
||||||
# # hostname and port of the smtp server
|
# # hostname and port of the smtp server
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub struct Settings {
|
||||||
pub rate_limit: RateLimitConfig,
|
pub rate_limit: RateLimitConfig,
|
||||||
pub email: Option<EmailConfig>,
|
pub email: Option<EmailConfig>,
|
||||||
pub federation: Federation,
|
pub federation: Federation,
|
||||||
|
pub captcha: CaptchaConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
@ -46,6 +47,12 @@ pub struct EmailConfig {
|
||||||
pub use_tls: bool,
|
pub use_tls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct CaptchaConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub difficulty: String, // easy, medium, or hard
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub user: String,
|
pub user: String,
|
||||||
|
|
|
@ -370,6 +370,8 @@ impl Perform for Oper<GetSite> {
|
||||||
password_verify: setup.admin_password.to_owned(),
|
password_verify: setup.admin_password.to_owned(),
|
||||||
admin: true,
|
admin: true,
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
|
captcha_uuid: None,
|
||||||
|
captcha_answer: None,
|
||||||
};
|
};
|
||||||
let login_response = Oper::new(register, self.client.clone())
|
let login_response = Oper::new(register, self.client.clone())
|
||||||
.perform(pool, websocket_info.clone())
|
.perform(pool, websocket_info.clone())
|
||||||
|
|
|
@ -2,8 +2,9 @@ use crate::{
|
||||||
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||||
apub::ApubObjectType,
|
apub::ApubObjectType,
|
||||||
blocking,
|
blocking,
|
||||||
|
captcha_espeak_wav_base64,
|
||||||
websocket::{
|
websocket::{
|
||||||
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
||||||
UserOperation,
|
UserOperation,
|
||||||
WebsocketInfo,
|
WebsocketInfo,
|
||||||
},
|
},
|
||||||
|
@ -11,6 +12,8 @@ use crate::{
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
|
use captcha::{gen, Difficulty};
|
||||||
|
use chrono::Duration;
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment::*,
|
comment::*,
|
||||||
comment_view::*,
|
comment_view::*,
|
||||||
|
@ -66,6 +69,23 @@ pub struct Register {
|
||||||
pub password_verify: String,
|
pub password_verify: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
|
pub captcha_uuid: Option<String>,
|
||||||
|
pub captcha_answer: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetCaptcha {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetCaptchaResponse {
|
||||||
|
ok: Option<CaptchaResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CaptchaResponse {
|
||||||
|
png: String, // A Base64 encoded png
|
||||||
|
wav: Option<String>, // A Base64 encoded wav audio
|
||||||
|
uuid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -303,7 +323,7 @@ impl Perform for Oper<Register> {
|
||||||
async fn perform(
|
async fn perform(
|
||||||
&self,
|
&self,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
websocket_info: Option<WebsocketInfo>,
|
||||||
) -> Result<LoginResponse, LemmyError> {
|
) -> Result<LoginResponse, LemmyError> {
|
||||||
let data: &Register = &self.data;
|
let data: &Register = &self.data;
|
||||||
|
|
||||||
|
@ -320,6 +340,31 @@ impl Perform for Oper<Register> {
|
||||||
return Err(APIError::err("passwords_dont_match").into());
|
return Err(APIError::err("passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If its not the admin, check the captcha
|
||||||
|
if !data.admin && Settings::get().captcha.enabled {
|
||||||
|
match websocket_info {
|
||||||
|
Some(ws) => {
|
||||||
|
let check = ws
|
||||||
|
.chatserver
|
||||||
|
.send(CheckCaptcha {
|
||||||
|
uuid: data
|
||||||
|
.captcha_uuid
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| "".to_string()),
|
||||||
|
answer: data
|
||||||
|
.captcha_answer
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| "".to_string()),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if !check {
|
||||||
|
return Err(APIError::err("captcha_incorrect").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(APIError::err("captcha_incorrect").into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(slurs) = slur_check(&data.username) {
|
if let Err(slurs) = slur_check(&data.username) {
|
||||||
return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
|
return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
|
||||||
}
|
}
|
||||||
|
@ -439,6 +484,54 @@ impl Perform for Oper<Register> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Perform for Oper<GetCaptcha> {
|
||||||
|
type Response = GetCaptchaResponse;
|
||||||
|
|
||||||
|
async fn perform(
|
||||||
|
&self,
|
||||||
|
_pool: &DbPool,
|
||||||
|
websocket_info: Option<WebsocketInfo>,
|
||||||
|
) -> Result<Self::Response, LemmyError> {
|
||||||
|
let captcha_settings = Settings::get().captcha;
|
||||||
|
|
||||||
|
if !captcha_settings.enabled {
|
||||||
|
return Ok(GetCaptchaResponse { ok: None });
|
||||||
|
}
|
||||||
|
|
||||||
|
let captcha = match captcha_settings.difficulty.as_str() {
|
||||||
|
"easy" => gen(Difficulty::Easy),
|
||||||
|
"medium" => gen(Difficulty::Medium),
|
||||||
|
"hard" => gen(Difficulty::Hard),
|
||||||
|
_ => gen(Difficulty::Medium),
|
||||||
|
};
|
||||||
|
|
||||||
|
let answer = captcha.chars_as_string();
|
||||||
|
|
||||||
|
let png_byte_array = captcha.as_png().expect("failed to generate captcha");
|
||||||
|
|
||||||
|
let png = base64::encode(png_byte_array);
|
||||||
|
|
||||||
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let wav = captcha_espeak_wav_base64(&answer).ok();
|
||||||
|
|
||||||
|
let captcha_item = CaptchaItem {
|
||||||
|
answer,
|
||||||
|
uuid: uuid.to_owned(),
|
||||||
|
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ws) = websocket_info {
|
||||||
|
ws.chatserver.do_send(captcha_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(GetCaptchaResponse {
|
||||||
|
ok: Some(CaptchaResponse { png, uuid, wav }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for Oper<SaveUserSettings> {
|
impl Perform for Oper<SaveUserSettings> {
|
||||||
type Response = LoginResponse;
|
type Response = LoginResponse;
|
||||||
|
|
|
@ -7,7 +7,9 @@ pub extern crate lazy_static;
|
||||||
pub extern crate failure;
|
pub extern crate failure;
|
||||||
pub extern crate actix;
|
pub extern crate actix;
|
||||||
pub extern crate actix_web;
|
pub extern crate actix_web;
|
||||||
|
pub extern crate base64;
|
||||||
pub extern crate bcrypt;
|
pub extern crate bcrypt;
|
||||||
|
pub extern crate captcha;
|
||||||
pub extern crate chrono;
|
pub extern crate chrono;
|
||||||
pub extern crate diesel;
|
pub extern crate diesel;
|
||||||
pub extern crate dotenv;
|
pub extern crate dotenv;
|
||||||
|
@ -35,6 +37,7 @@ use lemmy_utils::{get_apub_protocol_string, settings::Settings};
|
||||||
use log::error;
|
use log::error;
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
||||||
pub type ConnectionId = usize;
|
pub type ConnectionId = usize;
|
||||||
|
@ -224,9 +227,56 @@ where
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
|
||||||
|
let mut built_text = String::new();
|
||||||
|
|
||||||
|
// Building proper speech text for espeak
|
||||||
|
for mut c in captcha.chars() {
|
||||||
|
let new_str = if c.is_alphabetic() {
|
||||||
|
if c.is_lowercase() {
|
||||||
|
c.make_ascii_uppercase();
|
||||||
|
format!("lower case {} ... ", c)
|
||||||
|
} else {
|
||||||
|
c.make_ascii_uppercase();
|
||||||
|
format!("capital {} ... ", c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("{} ...", c)
|
||||||
|
};
|
||||||
|
|
||||||
|
built_text.push_str(&new_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
espeak_wav_base64(&built_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
|
||||||
|
// Make a temp file path
|
||||||
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
|
||||||
|
|
||||||
|
// Write the wav file
|
||||||
|
Command::new("espeak")
|
||||||
|
.arg("-w")
|
||||||
|
.arg(&file_path)
|
||||||
|
.arg(text)
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
// Read the wav file bytes
|
||||||
|
let bytes = std::fs::read(&file_path)?;
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
std::fs::remove_file(file_path)?;
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
let base64 = base64::encode(bytes);
|
||||||
|
|
||||||
|
Ok(base64)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::is_image_content_type;
|
use crate::{captcha_espeak_wav_base64, is_image_content_type};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_image() {
|
fn test_image() {
|
||||||
|
@ -241,6 +291,11 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_espeak() {
|
||||||
|
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
// These helped with testing
|
// These helped with testing
|
||||||
// #[test]
|
// #[test]
|
||||||
// fn test_iframely() {
|
// fn test_iframely() {
|
||||||
|
|
|
@ -140,6 +140,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.route("/ban", web::post().to(route_post::<BanUser>))
|
.route("/ban", web::post().to(route_post::<BanUser>))
|
||||||
// Account actions. I don't like that they're in /user maybe /accounts
|
// Account actions. I don't like that they're in /user maybe /accounts
|
||||||
.route("/login", web::post().to(route_post::<Login>))
|
.route("/login", web::post().to(route_post::<Login>))
|
||||||
|
.route("/get_captcha", web::get().to(route_post::<GetCaptcha>))
|
||||||
.route(
|
.route(
|
||||||
"/delete_account",
|
"/delete_account",
|
||||||
web::post().to(route_post::<DeleteAccount>),
|
web::post().to(route_post::<DeleteAccount>),
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
pub const VERSION: &str = "v0.7.30";
|
pub const VERSION: &str = "v0.7.33";
|
||||||
|
|
|
@ -20,6 +20,7 @@ use std::{
|
||||||
pub enum UserOperation {
|
pub enum UserOperation {
|
||||||
Login,
|
Login,
|
||||||
Register,
|
Register,
|
||||||
|
GetCaptcha,
|
||||||
CreateCommunity,
|
CreateCommunity,
|
||||||
CreatePost,
|
CreatePost,
|
||||||
ListCommunities,
|
ListCommunities,
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
UserId,
|
UserId,
|
||||||
};
|
};
|
||||||
use actix_web::client::Client;
|
use actix_web::client::Client;
|
||||||
|
use lemmy_db::naive_now;
|
||||||
|
|
||||||
/// Chat server sends this messages to session
|
/// Chat server sends this messages to session
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
|
@ -134,6 +135,21 @@ pub struct SessionInfo {
|
||||||
pub ip: IPAddr,
|
pub ip: IPAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Message, Debug)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct CaptchaItem {
|
||||||
|
pub uuid: String,
|
||||||
|
pub answer: String,
|
||||||
|
pub expires: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(bool)]
|
||||||
|
pub struct CheckCaptcha {
|
||||||
|
pub uuid: String,
|
||||||
|
pub answer: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||||
/// session.
|
/// session.
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
|
@ -158,6 +174,9 @@ pub struct ChatServer {
|
||||||
/// Rate limiting based on rate type and IP addr
|
/// Rate limiting based on rate type and IP addr
|
||||||
rate_limiter: RateLimit,
|
rate_limiter: RateLimit,
|
||||||
|
|
||||||
|
/// A list of the current captchas
|
||||||
|
captchas: Vec<CaptchaItem>,
|
||||||
|
|
||||||
/// An HTTP Client
|
/// An HTTP Client
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
@ -176,6 +195,7 @@ impl ChatServer {
|
||||||
rng: rand::thread_rng(),
|
rng: rand::thread_rng(),
|
||||||
pool,
|
pool,
|
||||||
rate_limiter,
|
rate_limiter,
|
||||||
|
captchas: Vec::new(),
|
||||||
client,
|
client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,6 +461,7 @@ impl ChatServer {
|
||||||
// User ops
|
// User ops
|
||||||
UserOperation::Login => do_user_operation::<Login>(args).await,
|
UserOperation::Login => do_user_operation::<Login>(args).await,
|
||||||
UserOperation::Register => do_user_operation::<Register>(args).await,
|
UserOperation::Register => do_user_operation::<Register>(args).await,
|
||||||
|
UserOperation::GetCaptcha => do_user_operation::<GetCaptcha>(args).await,
|
||||||
UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
|
UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
|
||||||
UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
|
UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
|
||||||
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
||||||
|
@ -635,7 +656,7 @@ impl Handler<StandardMessage> for ChatServer {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match fut.await {
|
match fut.await {
|
||||||
Ok(m) => {
|
Ok(m) => {
|
||||||
info!("Message Sent: {}", m);
|
// info!("Message Sent: {}", m);
|
||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -774,3 +795,30 @@ where
|
||||||
};
|
};
|
||||||
Ok(serde_json::to_string(&response)?)
|
Ok(serde_json::to_string(&response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Handler<CaptchaItem> for ChatServer {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: CaptchaItem, _: &mut Context<Self>) {
|
||||||
|
self.captchas.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<CheckCaptcha> for ChatServer {
|
||||||
|
type Result = bool;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: CheckCaptcha, _: &mut Context<Self>) -> Self::Result {
|
||||||
|
// Remove all the ones that are past the expire time
|
||||||
|
self.captchas.retain(|x| x.expires.gt(&naive_now()));
|
||||||
|
|
||||||
|
let check = self
|
||||||
|
.captchas
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.uuid == msg.uuid && r.answer == msg.answer);
|
||||||
|
|
||||||
|
// Remove this uuid so it can't be re-checked (Checks only work once)
|
||||||
|
self.captchas.retain(|x| x.uuid != msg.uuid);
|
||||||
|
|
||||||
|
check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
19
ui/src/components/community.tsx
vendored
19
ui/src/components/community.tsx
vendored
|
@ -176,8 +176,8 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
if (this.state.community.name) {
|
if (this.state.community.title) {
|
||||||
return `/c/${this.state.community.name} - ${this.state.site.name}`;
|
return `${this.state.community.title} - ${this.state.site.name}`;
|
||||||
} else {
|
} else {
|
||||||
return 'Lemmy';
|
return 'Lemmy';
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,6 @@ export class Community extends Component<any, State> {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Helmet title={this.documentTitle} />
|
<Helmet title={this.documentTitle} />
|
||||||
{this.selects()}
|
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -197,19 +196,7 @@ export class Community extends Component<any, State> {
|
||||||
) : (
|
) : (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<h5>
|
{this.selects()}
|
||||||
{this.state.community.title}
|
|
||||||
{this.state.community.removed && (
|
|
||||||
<small className="ml-2 text-muted font-italic">
|
|
||||||
{i18n.t('removed')}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{this.state.community.nsfw && (
|
|
||||||
<small className="ml-2 text-muted font-italic">
|
|
||||||
{i18n.t('nsfw')}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</h5>
|
|
||||||
{this.listings()}
|
{this.listings()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
2
ui/src/components/data-type-select.tsx
vendored
2
ui/src/components/data-type-select.tsx
vendored
|
@ -33,7 +33,7 @@ export class DataTypeSelect extends Component<
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`pointer btn btn-outline-secondary
|
className={`pointer btn btn-outline-secondary
|
||||||
${this.state.type_ == DataType.Post && 'active'}
|
${this.state.type_ == DataType.Post && 'active'}
|
||||||
|
|
6
ui/src/components/inbox.tsx
vendored
6
ui/src/components/inbox.tsx
vendored
|
@ -112,7 +112,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
if (this.state.site.name) {
|
if (this.state.site.name) {
|
||||||
return `/u/${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
|
return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
|
||||||
this.state.site.name
|
this.state.site.name
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -171,7 +171,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
|
|
||||||
unreadOrAllRadios() {
|
unreadOrAllRadios() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary pointer
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
||||||
|
@ -204,7 +204,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
|
|
||||||
messageTypeRadios() {
|
messageTypeRadios() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary pointer
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.messageType == MessageType.All && 'active'}
|
${this.state.messageType == MessageType.All && 'active'}
|
||||||
|
|
2
ui/src/components/listing-type-select.tsx
vendored
2
ui/src/components/listing-type-select.tsx
vendored
|
@ -34,7 +34,7 @@ export class ListingTypeSelect extends Component<
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary
|
className={`btn btn-outline-secondary
|
||||||
${this.state.type_ == ListingType.Subscribed && 'active'}
|
${this.state.type_ == ListingType.Subscribed && 'active'}
|
||||||
|
|
131
ui/src/components/login.tsx
vendored
131
ui/src/components/login.tsx
vendored
|
@ -9,6 +9,7 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
PasswordResetForm,
|
PasswordResetForm,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
GetCaptchaResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
Site,
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
@ -21,11 +22,8 @@ interface State {
|
||||||
registerForm: RegisterForm;
|
registerForm: RegisterForm;
|
||||||
loginLoading: boolean;
|
loginLoading: boolean;
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
mathQuestion: {
|
captcha: GetCaptchaResponse;
|
||||||
a: number;
|
captchaPlaying: boolean;
|
||||||
b: number;
|
|
||||||
answer: number;
|
|
||||||
};
|
|
||||||
site: Site;
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,14 +41,13 @@ export class Login extends Component<any, State> {
|
||||||
password_verify: undefined,
|
password_verify: undefined,
|
||||||
admin: false,
|
admin: false,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
|
captcha_uuid: undefined,
|
||||||
|
captcha_answer: undefined,
|
||||||
},
|
},
|
||||||
loginLoading: false,
|
loginLoading: false,
|
||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
mathQuestion: {
|
captcha: undefined,
|
||||||
a: Math.floor(Math.random() * 10) + 1,
|
captchaPlaying: false,
|
||||||
b: Math.floor(Math.random() * 10) + 1,
|
|
||||||
answer: undefined,
|
|
||||||
},
|
|
||||||
site: {
|
site: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
@ -81,6 +78,7 @@ export class Login extends Component<any, State> {
|
||||||
);
|
);
|
||||||
|
|
||||||
WebSocketService.Instance.getSite();
|
WebSocketService.Instance.getSite();
|
||||||
|
WebSocketService.Instance.getCaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -172,6 +170,7 @@ export class Login extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerForm() {
|
registerForm() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
||||||
|
@ -258,23 +257,37 @@ export class Login extends Component<any, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
|
||||||
<label class="col-sm-10 col-form-label" htmlFor="register-math">
|
|
||||||
{i18n.t('what_is')}{' '}
|
|
||||||
{`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="col-sm-2">
|
{this.state.captcha && (
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2" htmlFor="register-captcha">
|
||||||
|
<span class="mr-2">{i18n.t('enter_code')}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleRegenCaptcha)}
|
||||||
|
>
|
||||||
|
<svg class="icon icon-refresh-cw">
|
||||||
|
<use xlinkHref="#icon-refresh-cw"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
{this.showCaptcha()}
|
||||||
|
<div class="col-sm-6">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
id="register-math"
|
|
||||||
class="form-control"
|
class="form-control"
|
||||||
value={this.state.mathQuestion.answer}
|
id="register-captcha"
|
||||||
onInput={linkEvent(this, this.handleMathAnswerChange)}
|
value={this.state.registerForm.captcha_answer}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleRegisterCaptchaAnswerChange
|
||||||
|
)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{this.state.site.enable_nsfw && (
|
{this.state.site.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
@ -295,11 +308,7 @@ export class Login extends Component<any, State> {
|
||||||
)}
|
)}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button
|
<button type="submit" class="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
disabled={this.mathCheck}
|
|
||||||
>
|
|
||||||
{this.state.registerLoading ? (
|
{this.state.registerLoading ? (
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
@ -314,6 +323,36 @@ export class Login extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCaptcha() {
|
||||||
|
return (
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{this.state.captcha.ok && (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
class="rounded-top img-fluid"
|
||||||
|
src={this.captchaPngSrc()}
|
||||||
|
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
|
||||||
|
/>
|
||||||
|
{this.state.captcha.ok.wav && (
|
||||||
|
<button
|
||||||
|
class="rounded-bottom btn btn-sm btn-secondary btn-block"
|
||||||
|
style="border-top-right-radius: 0; border-top-left-radius: 0;"
|
||||||
|
title={i18n.t('play_captcha_audio')}
|
||||||
|
onClick={linkEvent(this, this.handleCaptchaPlay)}
|
||||||
|
type="button"
|
||||||
|
disabled={this.state.captchaPlaying}
|
||||||
|
>
|
||||||
|
<svg class="icon icon-play">
|
||||||
|
<use xlinkHref="#icon-play"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleLoginSubmit(i: Login, event: any) {
|
handleLoginSubmit(i: Login, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.loginLoading = true;
|
i.state.loginLoading = true;
|
||||||
|
@ -335,11 +374,8 @@ export class Login extends Component<any, State> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.registerLoading = true;
|
i.state.registerLoading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
|
||||||
if (!i.mathCheck) {
|
|
||||||
WebSocketService.Instance.register(i.state.registerForm);
|
WebSocketService.Instance.register(i.state.registerForm);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleRegisterUsernameChange(i: Login, event: any) {
|
handleRegisterUsernameChange(i: Login, event: any) {
|
||||||
i.state.registerForm.username = event.target.value;
|
i.state.registerForm.username = event.target.value;
|
||||||
|
@ -369,11 +405,16 @@ export class Login extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMathAnswerChange(i: Login, event: any) {
|
handleRegisterCaptchaAnswerChange(i: Login, event: any) {
|
||||||
i.state.mathQuestion.answer = event.target.value;
|
i.state.registerForm.captcha_answer = event.target.value;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRegenCaptcha(_i: Login, _event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
WebSocketService.Instance.getCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
handlePasswordReset(i: Login) {
|
handlePasswordReset(i: Login) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let resetForm: PasswordResetForm = {
|
let resetForm: PasswordResetForm = {
|
||||||
|
@ -382,11 +423,21 @@ export class Login extends Component<any, State> {
|
||||||
WebSocketService.Instance.passwordReset(resetForm);
|
WebSocketService.Instance.passwordReset(resetForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
get mathCheck(): boolean {
|
handleCaptchaPlay(i: Login) {
|
||||||
return (
|
event.preventDefault();
|
||||||
this.state.mathQuestion.answer !=
|
let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
|
||||||
this.state.mathQuestion.a + this.state.mathQuestion.b
|
snd.play();
|
||||||
);
|
i.state.captchaPlaying = true;
|
||||||
|
i.setState(i.state);
|
||||||
|
snd.addEventListener('ended', () => {
|
||||||
|
snd.currentTime = 0;
|
||||||
|
i.state.captchaPlaying = false;
|
||||||
|
i.setState(this.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaPngSrc() {
|
||||||
|
return `data:image/png;base64,${this.state.captcha.ok.png}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: WebSocketJsonResponse) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
|
@ -394,6 +445,9 @@ export class Login extends Component<any, State> {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
toast(i18n.t(msg.error), 'danger');
|
toast(i18n.t(msg.error), 'danger');
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
this.state.registerForm.captcha_answer = undefined;
|
||||||
|
// Refetch another captcha
|
||||||
|
WebSocketService.Instance.getCaptcha();
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -412,6 +466,13 @@ export class Login extends Component<any, State> {
|
||||||
UserService.Instance.login(data);
|
UserService.Instance.login(data);
|
||||||
WebSocketService.Instance.userJoin();
|
WebSocketService.Instance.userJoin();
|
||||||
this.props.history.push('/communities');
|
this.props.history.push('/communities');
|
||||||
|
} else if (res.op == UserOperation.GetCaptcha) {
|
||||||
|
let data = res.data as GetCaptchaResponse;
|
||||||
|
if (data.ok) {
|
||||||
|
this.state.captcha = data;
|
||||||
|
this.state.registerForm.captcha_uuid = data.ok.uuid;
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.PasswordReset) {
|
} else if (res.op == UserOperation.PasswordReset) {
|
||||||
toast(i18n.t('reset_password_mail_sent'));
|
toast(i18n.t('reset_password_mail_sent'));
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
|
|
205
ui/src/components/main.tsx
vendored
205
ui/src/components/main.tsx
vendored
|
@ -194,21 +194,77 @@ export class Main extends Component<any, MainState> {
|
||||||
<main role="main" class="col-12 col-md-8">
|
<main role="main" class="col-12 col-md-8">
|
||||||
{this.posts()}
|
{this.posts()}
|
||||||
</main>
|
</main>
|
||||||
<aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
|
<aside class="col-12 col-md-4">{this.mySidebar()}</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
my_sidebar() {
|
mySidebar() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!this.state.loading && (
|
{!this.state.loading && (
|
||||||
<div>
|
<div>
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
|
<div class="card-header bg-transparent border-secondary">
|
||||||
|
{this.siteName()}
|
||||||
|
{this.adminButtons()}
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{this.trendingCommunities()}
|
{this.trendingCommunities()}
|
||||||
{UserService.Instance.user &&
|
{this.createCommunityButton()}
|
||||||
|
{/*
|
||||||
|
{this.subscribedCommunities()}
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
|
<div class="card-body">{this.sidebar()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-transparent border-secondary">
|
||||||
|
<div class="card-body">{this.landing()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCommunityButton() {
|
||||||
|
return (
|
||||||
|
<Link class="btn btn-secondary btn-block" to="/create_community">
|
||||||
|
{i18n.t('create_a_community')}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
trendingCommunities() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h5>
|
||||||
|
<T i18nKey="trending_communities">
|
||||||
|
#
|
||||||
|
<Link class="text-body" to="/communities">
|
||||||
|
#
|
||||||
|
</Link>
|
||||||
|
</T>
|
||||||
|
</h5>
|
||||||
|
<ul class="list-inline">
|
||||||
|
{this.state.trendingCommunities.map(community => (
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<CommunityLink community={community} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribedCommunities() {
|
||||||
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
this.state.subscribedCommunities.length > 0 && (
|
this.state.subscribedCommunities.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h5>
|
<h5>
|
||||||
|
@ -234,42 +290,7 @@ export class Main extends Component<any, MainState> {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
<Link
|
|
||||||
class="btn btn-secondary btn-block"
|
|
||||||
to="/create_community"
|
|
||||||
>
|
|
||||||
{i18n.t('create_a_community')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.sidebar()}
|
|
||||||
{this.landing()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
trendingCommunities() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h5>
|
|
||||||
<T i18nKey="trending_communities">
|
|
||||||
#
|
|
||||||
<Link class="text-body" to="/communities">
|
|
||||||
#
|
|
||||||
</Link>
|
|
||||||
</T>
|
|
||||||
</h5>
|
|
||||||
<ul class="list-inline">
|
|
||||||
{this.state.trendingCommunities.map(community => (
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<CommunityLink community={community} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,24 +326,40 @@ export class Main extends Component<any, MainState> {
|
||||||
siteInfo() {
|
siteInfo() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
{this.state.siteRes.site.description && this.siteDescription()}
|
||||||
<div class="card-body">
|
{this.badges()}
|
||||||
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
|
{this.admins()}
|
||||||
{this.canAdmin && (
|
</div>
|
||||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
);
|
||||||
<li className="list-inline-item-action">
|
}
|
||||||
<span
|
|
||||||
class="pointer"
|
siteName() {
|
||||||
onClick={linkEvent(this, this.handleEditClick)}
|
return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
|
||||||
data-tippy-content={i18n.t('edit')}
|
}
|
||||||
>
|
|
||||||
<svg class="icon icon-inline">
|
admins() {
|
||||||
<use xlinkHref="#icon-edit"></use>
|
return (
|
||||||
</svg>
|
<ul class="mt-1 list-inline small mb-0">
|
||||||
</span>
|
<li class="list-inline-item">{i18n.t('admins')}:</li>
|
||||||
|
{this.state.siteRes.admins.map(admin => (
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<UserListing
|
||||||
|
user={{
|
||||||
|
name: admin.name,
|
||||||
|
avatar: admin.avatar,
|
||||||
|
local: admin.local,
|
||||||
|
actor_id: admin.actor_id,
|
||||||
|
id: admin.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
badges() {
|
||||||
|
return (
|
||||||
<ul class="my-2 list-inline">
|
<ul class="my-2 list-inline">
|
||||||
{/*
|
{/*
|
||||||
<li className="list-inline-item badge badge-light">
|
<li className="list-inline-item badge badge-light">
|
||||||
|
@ -355,44 +392,41 @@ export class Main extends Component<any, MainState> {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="mt-1 list-inline small mb-0">
|
);
|
||||||
<li class="list-inline-item">{i18n.t('admins')}:</li>
|
}
|
||||||
{this.state.siteRes.admins.map(admin => (
|
|
||||||
<li class="list-inline-item">
|
adminButtons() {
|
||||||
<UserListing
|
return (
|
||||||
user={{
|
this.canAdmin && (
|
||||||
name: admin.name,
|
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||||
avatar: admin.avatar,
|
<li className="list-inline-item-action">
|
||||||
local: admin.local,
|
<span
|
||||||
actor_id: admin.actor_id,
|
class="pointer"
|
||||||
id: admin.id,
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
}}
|
data-tippy-content={i18n.t('edit')}
|
||||||
/>
|
>
|
||||||
|
<svg class="icon icon-inline">
|
||||||
|
<use xlinkHref="#icon-edit"></use>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
)
|
||||||
</div>
|
);
|
||||||
{this.state.siteRes.site.description && (
|
}
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
|
||||||
<div class="card-body">
|
siteDescription() {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className="md-div"
|
className="md-div"
|
||||||
dangerouslySetInnerHTML={mdToHtml(
|
dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
|
||||||
this.state.siteRes.site.description
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
landing() {
|
landing() {
|
||||||
return (
|
return (
|
||||||
<div class="card bg-transparent border-secondary">
|
<>
|
||||||
<div class="card-body">
|
|
||||||
<h5>
|
<h5>
|
||||||
{i18n.t('powered_by')}
|
{i18n.t('powered_by')}
|
||||||
<svg class="icon mx-2">
|
<svg class="icon mx-2">
|
||||||
|
@ -426,15 +460,13 @@ export class Main extends Component<any, MainState> {
|
||||||
</a>
|
</a>
|
||||||
</T>
|
</T>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
posts() {
|
posts() {
|
||||||
return (
|
return (
|
||||||
<div class="main-content-wrapper">
|
<div class="main-content-wrapper">
|
||||||
{this.selects()}
|
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -443,6 +475,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
|
{this.selects()}
|
||||||
{this.listings()}
|
{this.listings()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
5
ui/src/components/navbar.tsx
vendored
5
ui/src/components/navbar.tsx
vendored
|
@ -31,7 +31,7 @@ import {
|
||||||
md,
|
md,
|
||||||
setTheme,
|
setTheme,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n, i18nextSetup } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface NavbarState {
|
interface NavbarState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -435,12 +435,11 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
this.requestNotificationPermission();
|
this.requestNotificationPermission();
|
||||||
this.fetchUnreads();
|
this.fetchUnreads();
|
||||||
setTheme(data.my_user.theme, true);
|
setTheme(data.my_user.theme, true);
|
||||||
|
i18n.changeLanguage(data.my_user.lang);
|
||||||
}
|
}
|
||||||
this.state.isLoggedIn = true;
|
this.state.isLoggedIn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
i18nextSetup();
|
|
||||||
|
|
||||||
this.state.siteLoading = false;
|
this.state.siteLoading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
39
ui/src/components/post-listing.tsx
vendored
39
ui/src/components/post-listing.tsx
vendored
|
@ -312,7 +312,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="post-title">
|
<div className="post-title">
|
||||||
<h5 className="mb-0 d-inline">
|
<h5 className="mb-1 d-inline-block">
|
||||||
{this.props.showBody && post.url ? (
|
{this.props.showBody && post.url ? (
|
||||||
<a
|
<a
|
||||||
className="text-body"
|
className="text-body"
|
||||||
|
@ -434,7 +434,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div className="details col-12">
|
<div className="details col-12">
|
||||||
<ul class="list-inline mb-0 text-muted small">
|
<ul class="list-inline mb-1 text-muted small">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>{i18n.t('by')} </span>
|
<span>{i18n.t('by')} </span>
|
||||||
<UserListing
|
<UserListing
|
||||||
|
@ -501,9 +501,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">•</li>
|
</ul>
|
||||||
|
<ul class="list-inline mb-1 text-muted small">
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
title={i18n.t('number_of_comments', {
|
||||||
|
count: post.number_of_comments,
|
||||||
|
})}
|
||||||
|
to={`/post/${post.id}`}
|
||||||
|
>
|
||||||
|
<svg class="mr-1 icon icon-inline">
|
||||||
|
<use xlinkHref="#icon-message-square"></use>
|
||||||
|
</svg>
|
||||||
|
{i18n.t('number_of_comments', {
|
||||||
|
count: post.number_of_comments,
|
||||||
|
})}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
{this.state.upvotes !== this.state.score && (
|
{this.state.upvotes !== this.state.score && (
|
||||||
<>
|
<>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
<span
|
<span
|
||||||
class="unselectable pointer mr-2"
|
class="unselectable pointer mr-2"
|
||||||
data-tippy-content={this.pointsTippy}
|
data-tippy-content={this.pointsTippy}
|
||||||
|
@ -525,23 +543,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</span>
|
</span>
|
||||||
<li className="list-inline-item">•</li>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">
|
|
||||||
<Link
|
|
||||||
className="text-muted"
|
|
||||||
title={i18n.t('number_of_comments', {
|
|
||||||
count: post.number_of_comments,
|
|
||||||
})}
|
|
||||||
to={`/post/${post.id}`}
|
|
||||||
>
|
|
||||||
<svg class="mr-1 icon icon-inline">
|
|
||||||
<use xlinkHref="#icon-message-square"></use>
|
|
||||||
</svg>
|
|
||||||
{post.number_of_comments}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
{this.props.post.duplicates && (
|
{this.props.post.duplicates && (
|
||||||
<ul class="list-inline mb-1 small text-muted">
|
<ul class="list-inline mb-1 small text-muted">
|
||||||
|
|
4
ui/src/components/post.tsx
vendored
4
ui/src/components/post.tsx
vendored
|
@ -232,7 +232,7 @@ export class Post extends Component<any, PostState> {
|
||||||
sortRadios() {
|
sortRadios() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="btn-group btn-group-toggle mr-3 mb-2">
|
<div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||||
|
@ -286,7 +286,7 @@ export class Post extends Component<any, PostState> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-toggle mb-2">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary pointer ${
|
className={`btn btn-outline-secondary pointer ${
|
||||||
this.state.commentViewType === CommentViewType.Chat && 'active'
|
this.state.commentViewType === CommentViewType.Chat && 'active'
|
||||||
|
|
6
ui/src/components/search.tsx
vendored
6
ui/src/components/search.tsx
vendored
|
@ -196,14 +196,14 @@ export class Search extends Component<any, SearchState> {
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control mr-2"
|
class="form-control mr-2 mb-2"
|
||||||
value={this.state.searchText}
|
value={this.state.searchText}
|
||||||
placeholder={`${i18n.t('search')}...`}
|
placeholder={`${i18n.t('search')}...`}
|
||||||
onInput={linkEvent(this, this.handleQChange)}
|
onInput={linkEvent(this, this.handleQChange)}
|
||||||
required
|
required
|
||||||
minLength={3}
|
minLength={3}
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="btn btn-secondary mr-2">
|
<button type="submit" class="btn btn-secondary mr-2 mb-2">
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
@ -222,7 +222,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.type_}
|
value={this.state.type_}
|
||||||
onChange={linkEvent(this, this.handleTypeChange)}
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
class="custom-select w-auto"
|
class="custom-select w-auto mb-2"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('type')}</option>
|
<option disabled>{i18n.t('type')}</option>
|
||||||
<option value={SearchType.All}>{i18n.t('all')}</option>
|
<option value={SearchType.All}>{i18n.t('all')}</option>
|
||||||
|
|
3
ui/src/components/setup.tsx
vendored
3
ui/src/components/setup.tsx
vendored
|
@ -29,6 +29,9 @@ export class Setup extends Component<any, State> {
|
||||||
password_verify: undefined,
|
password_verify: undefined,
|
||||||
admin: true,
|
admin: true,
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
|
// The first admin signup doesn't need a captcha
|
||||||
|
captcha_uuid: '',
|
||||||
|
captcha_answer: '',
|
||||||
},
|
},
|
||||||
doneRegisteringUser: false,
|
doneRegisteringUser: false,
|
||||||
userLoading: false,
|
userLoading: false,
|
||||||
|
|
251
ui/src/components/sidebar.tsx
vendored
251
ui/src/components/sidebar.tsx
vendored
|
@ -9,7 +9,7 @@ import {
|
||||||
UserView,
|
UserView,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime, hostname } from '../utils';
|
import { mdToHtml, getUnixTime } from '../utils';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
import { UserListing } from './user-listing';
|
import { UserListing } from './user-listing';
|
||||||
import { CommunityLink } from './community-link';
|
import { CommunityLink } from './community-link';
|
||||||
|
@ -63,21 +63,30 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebar() {
|
sidebar() {
|
||||||
let community = this.props.community;
|
|
||||||
let name_: string, link: string;
|
|
||||||
|
|
||||||
if (community.local) {
|
|
||||||
name_ = community.name;
|
|
||||||
link = `/c/${community.name}`;
|
|
||||||
} else {
|
|
||||||
name_ = `${community.name}@${hostname(community.actor_id)}`;
|
|
||||||
link = community.actor_id;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
|
<div class="card-header bg-transparent border-secondary">
|
||||||
|
{this.communityTitle()}
|
||||||
|
{this.adminButtons()}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">{this.subscribes()}</div>
|
||||||
|
</div>
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 className="mb-0">
|
{this.description()}
|
||||||
|
{this.badges()}
|
||||||
|
{this.mods()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
communityTitle() {
|
||||||
|
let community = this.props.community;
|
||||||
|
return (
|
||||||
|
<h5 className="mb-2">
|
||||||
<span>{community.title}</span>
|
<span>{community.title}</span>
|
||||||
{community.removed && (
|
{community.removed && (
|
||||||
<small className="ml-2 text-muted font-italic">
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
@ -89,8 +98,129 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
{i18n.t('deleted')}
|
{i18n.t('deleted')}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
{community.nsfw && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
{i18n.t('nsfw')}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
badges() {
|
||||||
|
let community = this.props.community;
|
||||||
|
return (
|
||||||
|
<ul class="my-1 list-inline">
|
||||||
|
{/*
|
||||||
|
<li className="list-inline-item badge badge-light">
|
||||||
|
{i18n.t('number_online', { count: this.props.online })}
|
||||||
|
</li>
|
||||||
|
*/}
|
||||||
|
<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-light">
|
||||||
|
{i18n.t('number_of_posts', {
|
||||||
|
count: community.number_of_posts,
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
<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-light" to="/communities">
|
||||||
|
{community.category_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<Link
|
||||||
|
className="badge badge-light"
|
||||||
|
to={`/modlog/community/${this.props.community.id}`}
|
||||||
|
>
|
||||||
|
{i18n.t('modlog')}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item badge badge-light">
|
||||||
<CommunityLink community={community} realLink />
|
<CommunityLink community={community} realLink />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mods() {
|
||||||
|
return (
|
||||||
|
<ul class="list-inline small">
|
||||||
|
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
||||||
|
{this.props.moderators.map(mod => (
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<UserListing
|
||||||
|
user={{
|
||||||
|
name: mod.user_name,
|
||||||
|
avatar: mod.avatar,
|
||||||
|
id: mod.user_id,
|
||||||
|
local: mod.user_local,
|
||||||
|
actor_id: mod.user_actor_id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribes() {
|
||||||
|
let community = this.props.community;
|
||||||
|
return (
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
<Link
|
||||||
|
class={`btn btn-secondary flex-fill mr-2 mb-2 ${
|
||||||
|
community.deleted || community.removed ? 'no-click' : ''
|
||||||
|
}`}
|
||||||
|
to={`/create_post?community=${community.name}`}
|
||||||
|
>
|
||||||
|
{i18n.t('create_a_post')}
|
||||||
|
</Link>
|
||||||
|
{community.subscribed ? (
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary flex-fill mb-2"
|
||||||
|
href="#"
|
||||||
|
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
||||||
|
>
|
||||||
|
{i18n.t('unsubscribe')}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary flex-fill mb-2"
|
||||||
|
href="#"
|
||||||
|
onClick={linkEvent(community.id, this.handleSubscribe)}
|
||||||
|
>
|
||||||
|
{i18n.t('subscribe')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
description() {
|
||||||
|
let community = this.props.community;
|
||||||
|
return (
|
||||||
|
community.description && (
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(community.description)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
adminButtons() {
|
||||||
|
let community = this.props.community;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||||
{this.canMod && (
|
{this.canMod && (
|
||||||
<>
|
<>
|
||||||
|
@ -111,9 +241,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
!community.deleted
|
!community.deleted ? i18n.t('delete') : i18n.t('restore')
|
||||||
? i18n.t('delete')
|
|
||||||
: i18n.t('restore')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -175,96 +303,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
<ul class="my-1 list-inline">
|
</>
|
||||||
{/*
|
|
||||||
<li className="list-inline-item badge badge-light">
|
|
||||||
{i18n.t('number_online', { count: this.props.online })}
|
|
||||||
</li>
|
|
||||||
*/}
|
|
||||||
<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-light">
|
|
||||||
{i18n.t('number_of_posts', {
|
|
||||||
count: community.number_of_posts,
|
|
||||||
})}
|
|
||||||
</li>
|
|
||||||
<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-light" to="/communities">
|
|
||||||
{community.category_name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<Link
|
|
||||||
className="badge badge-light"
|
|
||||||
to={`/modlog/community/${this.props.community.id}`}
|
|
||||||
>
|
|
||||||
{i18n.t('modlog')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="list-inline small">
|
|
||||||
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
|
||||||
{this.props.moderators.map(mod => (
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<UserListing
|
|
||||||
user={{
|
|
||||||
name: mod.user_name,
|
|
||||||
avatar: mod.avatar,
|
|
||||||
id: mod.user_id,
|
|
||||||
local: mod.user_local,
|
|
||||||
actor_id: mod.user_actor_id,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
|
|
||||||
<Link
|
|
||||||
class={`btn btn-secondary btn-block mb-3 ${
|
|
||||||
(community.deleted || community.removed) && 'no-click'
|
|
||||||
}`}
|
|
||||||
to={`/create_post?community=${community.name}`}
|
|
||||||
>
|
|
||||||
{i18n.t('create_a_post')}
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
{community.subscribed ? (
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary btn-block"
|
|
||||||
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
|
||||||
>
|
|
||||||
{i18n.t('unsubscribe')}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary btn-block"
|
|
||||||
onClick={linkEvent(community.id, this.handleSubscribe)}
|
|
||||||
>
|
|
||||||
{i18n.t('subscribe')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{community.description && (
|
|
||||||
<div class="card bg-transparent border-secondary">
|
|
||||||
<div class="card-body">
|
|
||||||
<div
|
|
||||||
className="md-div"
|
|
||||||
dangerouslySetInnerHTML={mdToHtml(community.description)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,6 +332,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
|
event.preventDefault();
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: false,
|
follow: false,
|
||||||
|
@ -301,6 +341,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubscribe(communityId: number) {
|
handleSubscribe(communityId: number) {
|
||||||
|
event.preventDefault();
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|
2
ui/src/components/sort-select.tsx
vendored
2
ui/src/components/sort-select.tsx
vendored
|
@ -35,7 +35,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
||||||
<select
|
<select
|
||||||
value={this.state.sort}
|
value={this.state.sort}
|
||||||
onChange={linkEvent(this, this.handleSortChange)}
|
onChange={linkEvent(this, this.handleSortChange)}
|
||||||
class="custom-select w-auto mr-2"
|
class="custom-select w-auto mr-2 mb-2"
|
||||||
>
|
>
|
||||||
<option disabled>{i18n.t('sort_type')}</option>
|
<option disabled>{i18n.t('sort_type')}</option>
|
||||||
{!this.props.hideHot && (
|
{!this.props.hideHot && (
|
||||||
|
|
10
ui/src/components/symbols.tsx
vendored
10
ui/src/components/symbols.tsx
vendored
File diff suppressed because one or more lines are too long
6
ui/src/components/user.tsx
vendored
6
ui/src/components/user.tsx
vendored
|
@ -213,7 +213,7 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
if (this.state.siteRes.site.name) {
|
if (this.state.siteRes.site.name) {
|
||||||
return `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
|
return `@${this.state.username} - ${this.state.siteRes.site.name}`;
|
||||||
} else {
|
} else {
|
||||||
return 'Lemmy';
|
return 'Lemmy';
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@ export class User extends Component<any, UserState> {
|
||||||
class="rounded-circle mr-2"
|
class="rounded-circle mr-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>/u/{this.state.username}</span>
|
<span>@{this.state.username}</span>
|
||||||
</h5>
|
</h5>
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
|
@ -274,7 +274,7 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
viewRadios() {
|
viewRadios() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
<label
|
||||||
className={`btn btn-outline-secondary pointer
|
className={`btn btn-outline-secondary pointer
|
||||||
${this.state.view == UserDetailsView.Overview && 'active'}
|
${this.state.view == UserDetailsView.Overview && 'active'}
|
||||||
|
|
7
ui/src/i18next.ts
vendored
7
ui/src/i18next.ts
vendored
|
@ -65,8 +65,7 @@ function format(value: any, format: any, lng: any): any {
|
||||||
return format === 'uppercase' ? value.toUpperCase() : value;
|
return format === 'uppercase' ? value.toUpperCase() : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function i18nextSetup() {
|
i18next.init({
|
||||||
i18next.init({
|
|
||||||
debug: false,
|
debug: false,
|
||||||
// load: 'languageOnly',
|
// load: 'languageOnly',
|
||||||
|
|
||||||
|
@ -75,6 +74,6 @@ export function i18nextSetup() {
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
resources,
|
resources,
|
||||||
interpolation: { format },
|
interpolation: { format },
|
||||||
});
|
});
|
||||||
}
|
|
||||||
export { i18next as i18n, resources };
|
export { i18next as i18n, resources };
|
||||||
|
|
12
ui/src/interfaces.ts
vendored
12
ui/src/interfaces.ts
vendored
|
@ -1,6 +1,7 @@
|
||||||
export enum UserOperation {
|
export enum UserOperation {
|
||||||
Login,
|
Login,
|
||||||
Register,
|
Register,
|
||||||
|
GetCaptcha,
|
||||||
CreateCommunity,
|
CreateCommunity,
|
||||||
CreatePost,
|
CreatePost,
|
||||||
ListCommunities,
|
ListCommunities,
|
||||||
|
@ -572,6 +573,16 @@ export interface RegisterForm {
|
||||||
password_verify: string;
|
password_verify: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
show_nsfw: boolean;
|
show_nsfw: boolean;
|
||||||
|
captcha_uuid?: string;
|
||||||
|
captcha_answer?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCaptchaResponse {
|
||||||
|
ok?: {
|
||||||
|
png: string;
|
||||||
|
wav?: string;
|
||||||
|
uuid: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
@ -1010,6 +1021,7 @@ type ResponseType =
|
||||||
| CommentResponse
|
| CommentResponse
|
||||||
| UserMentionResponse
|
| UserMentionResponse
|
||||||
| LoginResponse
|
| LoginResponse
|
||||||
|
| GetCaptchaResponse
|
||||||
| GetModlogResponse
|
| GetModlogResponse
|
||||||
| SearchResponse
|
| SearchResponse
|
||||||
| BanFromCommunityResponse
|
| BanFromCommunityResponse
|
||||||
|
|
4
ui/src/services/WebSocketService.ts
vendored
4
ui/src/services/WebSocketService.ts
vendored
|
@ -115,6 +115,10 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCaptcha() {
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.GetCaptcha, {}));
|
||||||
|
}
|
||||||
|
|
||||||
public createCommunity(form: CommunityForm) {
|
public createCommunity(form: CommunityForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
|
||||||
|
|
5
ui/translations/en.json
vendored
5
ui/translations/en.json
vendored
|
@ -264,6 +264,8 @@
|
||||||
"password_incorrect": "Password incorrect.",
|
"password_incorrect": "Password incorrect.",
|
||||||
"passwords_dont_match": "Passwords do not match.",
|
"passwords_dont_match": "Passwords do not match.",
|
||||||
"no_password_reset": "You will not be able to reset your password without an email.",
|
"no_password_reset": "You will not be able to reset your password without an email.",
|
||||||
|
"captcha_incorrect": "Captcha incorrect.",
|
||||||
|
"enter_code": "Enter Code",
|
||||||
"invalid_username": "Invalid username.",
|
"invalid_username": "Invalid username.",
|
||||||
"admin_already_created": "Sorry, there's already an admin.",
|
"admin_already_created": "Sorry, there's already an admin.",
|
||||||
"user_already_exists": "User already exists.",
|
"user_already_exists": "User already exists.",
|
||||||
|
@ -281,5 +283,6 @@
|
||||||
"cake_day_title": "Cake day:",
|
"cake_day_title": "Cake day:",
|
||||||
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
|
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
|
||||||
"invalid_post_title": "Invalid post title",
|
"invalid_post_title": "Invalid post title",
|
||||||
"invalid_url": "Invalid URL."
|
"invalid_url": "Invalid URL.",
|
||||||
|
"play_captcha_audio": "Play Captcha Audio"
|
||||||
}
|
}
|
||||||
|
|
303
ui/translations/ga.json
vendored
303
ui/translations/ga.json
vendored
|
@ -1 +1,302 @@
|
||||||
{}
|
{
|
||||||
|
"bold": "trom",
|
||||||
|
"italic": "iodálach",
|
||||||
|
"header": "ceanntásc",
|
||||||
|
"strikethrough": "stailc tríd",
|
||||||
|
"quote": "ceanglófar",
|
||||||
|
"spoiler": "milleadh scéil",
|
||||||
|
"list": "liosta",
|
||||||
|
"admin_settings": "Socruithe Riaracháin",
|
||||||
|
"site_config": "Cumraíocht Suímh",
|
||||||
|
"remove_as_mod": "bhaint mar modhnóir",
|
||||||
|
"appoint_as_mod": "ceapachán mar mhodhnóir",
|
||||||
|
"locked": "glasáilte",
|
||||||
|
"stickied": "bioráin",
|
||||||
|
"reason": "Cúis",
|
||||||
|
"mark_as_read": "marc mar a léitear",
|
||||||
|
"mark_as_unread": "marc mar neamhléite",
|
||||||
|
"delete": "scriosadh",
|
||||||
|
"deleted": "scriosta ag cruthaitheoir",
|
||||||
|
"delete_account": "Scrios Cuntas",
|
||||||
|
"click_to_delete_picture": "Cliceáil chun pictiúr a scriosadh.",
|
||||||
|
"picture_deleted": "Scriosadh an pictiúr.",
|
||||||
|
"restore": "athchóirigh",
|
||||||
|
"ban": "cosc",
|
||||||
|
"ban_from_site": "cosc ón suíomh",
|
||||||
|
"unban": "Cealaigh cosc",
|
||||||
|
"unban_from_site": "Cealaigh cosc ón suíomh",
|
||||||
|
"banned": "coisceadh",
|
||||||
|
"banned_users": "Úsáideoirí Coisceadh",
|
||||||
|
"save": "sábháil",
|
||||||
|
"unsave": "cealaigh sábháil",
|
||||||
|
"create": "cruthaigh",
|
||||||
|
"creator": "cruthaitheoir",
|
||||||
|
"email_or_username": "Ríomhphost nó Ainm Úsáideora",
|
||||||
|
"number_online_0": "{{count}} Úsáideoir Ar Líne",
|
||||||
|
"number_online_1": "{{count}} Úsáideoirí Ar Líne",
|
||||||
|
"number_online_2": "{{count}} Úsáideoirí Ar Líne",
|
||||||
|
"number_online_3": "{{count}} Úsáideoirí Ar Líne",
|
||||||
|
"number_online_4": "{{count}} Úsáideoirí Ar Líne",
|
||||||
|
"name": "Ainm",
|
||||||
|
"title": "Teideal",
|
||||||
|
"subscribers": "Síntiúsóirí",
|
||||||
|
"both": "Araon",
|
||||||
|
"saved": "Coinníodh",
|
||||||
|
"unsubscribe": "Díliostáil",
|
||||||
|
"subscribe": "Liostáil",
|
||||||
|
"subscribed": "Suibscríofa",
|
||||||
|
"prev": "Roimhe",
|
||||||
|
"next": "Chéad Eile",
|
||||||
|
"hot": "Te",
|
||||||
|
"new": "Nua",
|
||||||
|
"old": "Sean",
|
||||||
|
"top_day": "Lá barr",
|
||||||
|
"week": "Seachtain",
|
||||||
|
"month": "Mí",
|
||||||
|
"year": "Bliain",
|
||||||
|
"all": "Gach",
|
||||||
|
"top": "Barr",
|
||||||
|
"api": "API",
|
||||||
|
"docs": "Doic",
|
||||||
|
"inbox": "Bosca Isteach",
|
||||||
|
"inbox_for": "Bosca Isteach do <1>{{user}}</1>",
|
||||||
|
"mark_all_as_read": "marcáil gach rud mar atá léite",
|
||||||
|
"type": "Cineál",
|
||||||
|
"unread": "Gan léamh",
|
||||||
|
"replies": "Freagraí",
|
||||||
|
"mentions": "Luann",
|
||||||
|
"reply_sent": "Freagra seolta",
|
||||||
|
"message_sent": "Teachtaireacht seolta",
|
||||||
|
"search": "Cuardaigh",
|
||||||
|
"overview": "Forbhreathnú",
|
||||||
|
"view": "Amharc",
|
||||||
|
"logout": "Logáil Amach",
|
||||||
|
"login_sign_up": "Logáil Isteach / Cláraigh",
|
||||||
|
"login": "Logáil Isteach",
|
||||||
|
"unread_messages": "Teachtaireachtaí Neamhléite",
|
||||||
|
"messages": "Teachtaireachtaí",
|
||||||
|
"password": "Pasfhocal",
|
||||||
|
"verify_password": "Deimhnigh Pasfhocal",
|
||||||
|
"old_password": "Sean Pasfhocal",
|
||||||
|
"forgot_password": "Dearmad ar pasfhocal",
|
||||||
|
"reset_password_mail_sent": "Seol Ríomhphost chun do phasfhocal a athshocrú.",
|
||||||
|
"password_change": "Athrú Pasfhocal",
|
||||||
|
"new_password": "Focal Faire Nua",
|
||||||
|
"no_email_setup": "Níl ríomhphost curtha ar bun i gceart ag an bhfreastalaí seo.",
|
||||||
|
"email": "Ríomhphost",
|
||||||
|
"send_notifications_to_email": "Seol fógraí chuig Ríomhphost",
|
||||||
|
"expires": "In éag",
|
||||||
|
"joined": "Teanga",
|
||||||
|
"by": "le",
|
||||||
|
"to": "chun",
|
||||||
|
"from": "ó",
|
||||||
|
"transfer_community": "aistrithe pobal",
|
||||||
|
"transfer_site": "aistrithe suíomh",
|
||||||
|
"number_of_comments_0": "{{count}} Trácht",
|
||||||
|
"number_of_comments_1": "{{count}} Tráchtanna",
|
||||||
|
"number_of_comments_2": "{{count}} Tráchtanna",
|
||||||
|
"number_of_comments_3": "{{count}} Tráchtanna",
|
||||||
|
"number_of_comments_4": "{{count}} Tráchtanna",
|
||||||
|
"send_secure_message": "Seol Teachtaireacht Sábháilte",
|
||||||
|
"delete_account_confirm": "Rabhadh: scriosfaidh sé seo do chuid sonraí go buan. Iontráil do phasfhocal le deimhniú.",
|
||||||
|
"username": "Ainm Úsáideora",
|
||||||
|
"number_of_subscribers_0": "{{count}} Suibscríobhaí",
|
||||||
|
"number_of_subscribers_1": "{{count}} Síntiúsóirí",
|
||||||
|
"number_of_subscribers_2": "{{count}} Síntiúsóirí",
|
||||||
|
"number_of_subscribers_3": "{{count}} Síntiúsóirí",
|
||||||
|
"number_of_subscribers_4": "{{count}} Síntiúsóirí",
|
||||||
|
"sign_up": "Cláraigh",
|
||||||
|
"notifications_error": "Níl faisnéisí deisce ar fáil i do bhrabhsálaí. Bain triail as Firefox nó Chrome.",
|
||||||
|
"private_message_disclaimer": "Rabhadh: Níl teachtaireachtaí príobháideacha i Lemmy slán. Cruthaigh cuntas ar <1>Element.io</1> le haghaidh teachtaireachtaí slán.",
|
||||||
|
"landing": "Is <1>comhiomlánóir nasc</1> / reddit malartach é Lemmy atá beartaithe le bheith ag obair sa <2>fediverse </2>.<3> </3> Tá sé féin-hostable, tá snáitheanna tráchta ann a nuashonraíonn beo, agus beag bídeach (<4> ~ 80kB </4>). Cónaidhm isteach sa líonra ActivityPub tá sé ar an treochlár. <5> </5> Seo <6> leagan béite an-luath </6>, agus tá a lán gnéithe briste nó in easnamh faoi láthair. <7> </7> Mol gnéithe nua nó tuairiscigh fabhtanna <8> áit. </8> <9> </9> Déanta le <10> Meirge </10>, <11> Actix </11>, < 12> Inferno </12>, <13> Clóscríbhneoireacht </13>. <14> </14> <15> Go raibh maith agat dár rannpháirtithe: </15> dessalines, Nutomic, asonix, zacanger, agus iav.",
|
||||||
|
"post": "postáil",
|
||||||
|
"remove_post": "Postáil a Bhaint",
|
||||||
|
"no_posts": "Gan aon Postáil.",
|
||||||
|
"create_a_post": "Cruthaigh Postáil",
|
||||||
|
"create_post": "Cruthaigh Postáil",
|
||||||
|
"number_of_posts_0": "{{count}} Postáil",
|
||||||
|
"number_of_posts_1": "{{count}} Postálacha",
|
||||||
|
"number_of_posts_2": "{{count}} Postálacha",
|
||||||
|
"number_of_posts_3": "{{count}} Postálacha",
|
||||||
|
"number_of_posts_4": "{{count}} Postálacha",
|
||||||
|
"posts": "Postálacha",
|
||||||
|
"related_posts": "D’fhéadfadh baint a bheith ag na Poist seo",
|
||||||
|
"cross_posts": "Cuireadh an nasc seo sa phost freisin chuig:",
|
||||||
|
"cross_post": "tras-phost",
|
||||||
|
"cross_posted_to": "tras-phostáilte chuig: ",
|
||||||
|
"comments": "Tráchtanna",
|
||||||
|
"remove_comment": "Bain Trácht",
|
||||||
|
"communities": "Pobail",
|
||||||
|
"users": "Úsáideoirí",
|
||||||
|
"create_a_community": "Cruthaigh Pobal",
|
||||||
|
"select_a_community": "Roghnaigh Pobal",
|
||||||
|
"create_community": "Cruthaigh Pobal",
|
||||||
|
"remove_community": "Bain Pobal",
|
||||||
|
"subscribed_to_communities": "Suibscríofa le <1>pobail</1>",
|
||||||
|
"trending_communities": "Treocht <1>pobail</1>",
|
||||||
|
"list_of_communities": "Liosta na bPobal",
|
||||||
|
"community_reqs": "cás íochtair, fostríoc, agus gan aon spásanna.",
|
||||||
|
"number_of_communities_0": "{{count}} Pobal",
|
||||||
|
"number_of_communities_1": "{{count}} Pobail",
|
||||||
|
"number_of_communities_2": "{{count}} Pobail",
|
||||||
|
"number_of_communities_3": "{{count}} Pobail",
|
||||||
|
"number_of_communities_4": "{{count}} Pobail",
|
||||||
|
"invalid_community_name": "Ainm neamhbhailí.",
|
||||||
|
"create_private_message": "Cruthaigh Teachtaireacht Phríobháideach",
|
||||||
|
"send_message": "Seol Teachtaireacht",
|
||||||
|
"message": "Teachtaireacht",
|
||||||
|
"edit": "cuir in eagar",
|
||||||
|
"reply": "freagra",
|
||||||
|
"more": "tuilleadh",
|
||||||
|
"cancel": "Cealú",
|
||||||
|
"preview": "Réamhléiriú",
|
||||||
|
"upload_image": "íomhá a uaslódáil",
|
||||||
|
"avatar": "Abhatár",
|
||||||
|
"upload_avatar": "Uaslódáil Abhatár",
|
||||||
|
"show_avatars": "Taispeáin Abhatáranna",
|
||||||
|
"show_context": "Taispeáin comhthéacs",
|
||||||
|
"formatting_help": "formáidiú cabhrú",
|
||||||
|
"sorting_help": "cúnamh a shórtáil",
|
||||||
|
"view_source": "féachaint foinse",
|
||||||
|
"unlock": "dhíghlasáil",
|
||||||
|
"lock": "glas",
|
||||||
|
"sticky": "bioráin",
|
||||||
|
"unsticky": "cealaigh bioráin",
|
||||||
|
"link": "nasc",
|
||||||
|
"archive_link": "nasc cartlainne",
|
||||||
|
"mod": "modhnóir",
|
||||||
|
"mods": "modhnóirí",
|
||||||
|
"moderates": "Modhnóireacht",
|
||||||
|
"settings": "Socruithe",
|
||||||
|
"modlog": "Logamod",
|
||||||
|
"admin": "riarthóir",
|
||||||
|
"admins": "riarthóirí",
|
||||||
|
"remove_as_admin": "bhaint mar riarthóir",
|
||||||
|
"appoint_as_admin": "ceapachá mar riarthóir",
|
||||||
|
"remove": "bain",
|
||||||
|
"removed": "bainte ag an modhnóir",
|
||||||
|
"category": "Catagóir",
|
||||||
|
"sidebar": "Barrataobh",
|
||||||
|
"sort_type": "Cineál sórtála",
|
||||||
|
"matrix_user_id": "Úsáideoir Matrix",
|
||||||
|
"optional": "Roghnach",
|
||||||
|
"are_you_sure": "An bhfuil tú cinnte?",
|
||||||
|
"yes": "tá",
|
||||||
|
"no": "níl",
|
||||||
|
"powered_by": "Cumhachtaithe ag",
|
||||||
|
"not_logged_in": "Ní logáilte isteach.",
|
||||||
|
"logged_in": "Logáilte isteach.",
|
||||||
|
"number_of_users_0": "{{count}} Úsáideoir",
|
||||||
|
"number_of_users_1": "{{count}} Úsáideoirí",
|
||||||
|
"number_of_users_2": "{{count}} Úsáideoirí",
|
||||||
|
"number_of_users_3": "{{count}} Úsáideoirí",
|
||||||
|
"number_of_users_4": "{{count}} Úsáideoirí",
|
||||||
|
"number_of_points_0": "{{count}} Pointe",
|
||||||
|
"number_of_points_1": "{{count}} Pointí",
|
||||||
|
"number_of_points_2": "{{count}} Pointí",
|
||||||
|
"number_of_points_3": "{{count}} Pointí",
|
||||||
|
"number_of_points_4": "{{count}} Pointí",
|
||||||
|
"subscribe_to_communities": "Liostáil le roinnt <1>pobail</1>.",
|
||||||
|
"chat": "Comhrá",
|
||||||
|
"recent_comments": "Tráchtanna le Déanaí",
|
||||||
|
"no_results": "Gan torthaí.",
|
||||||
|
"setup": "Cumraigh",
|
||||||
|
"lemmy_instance_setup": "Lemmy Ásc Cumraigh",
|
||||||
|
"setup_admin": "Riarthóir Suímh a Bhunú",
|
||||||
|
"your_site": "do suíomh",
|
||||||
|
"modified": "modhnaithe",
|
||||||
|
"sponsors": "Urraitheoirí",
|
||||||
|
"sponsors_of_lemmy": "Urraitheoirí Lemmy",
|
||||||
|
"sponsor_message": "Tá Lemmy saor in aisce, <1>bogearraí foinse oscailte</1>, gan aon fhógraíocht, monetizing, ná caipiteal fiontair, riamh. Tacaíonn do shíntiúis go díreach le forbairt lánaimseartha an tionscadail. Go raibh maith agat do na daoine seo a leanas:",
|
||||||
|
"support_on_patreon": "Tacaíocht ar Patreon",
|
||||||
|
"support_on_liberapay": "Tacaíocht ar Liberapay",
|
||||||
|
"support_on_open_collective": "Tacaíocht ar OpenCollective",
|
||||||
|
"donate_to_lemmy": "Bronn do Lemmy",
|
||||||
|
"donate": "Bronn",
|
||||||
|
"general_sponsors": "Is iad Urraitheoirí Ginearálta iad siúd a gheall $10 go $39 chun Lemmy.",
|
||||||
|
"silver_sponsors": "Is iad Urraitheoirí Airgid iad siúd a gheall $ 40 chun Lemmy.",
|
||||||
|
"crypto": "Criptea",
|
||||||
|
"bitcoin": "Bonn Giotáin",
|
||||||
|
"ethereum": "Ethereum",
|
||||||
|
"monero": "Monero",
|
||||||
|
"code": "Cód",
|
||||||
|
"language": "Teanga",
|
||||||
|
"body": "Corp",
|
||||||
|
"copy_suggested_title": "cóip teideal molta: {{title}}",
|
||||||
|
"community": "Pobal",
|
||||||
|
"expand_here": "Leathnaigh anseo",
|
||||||
|
"browser_default": "Réamhshocrú Brabhsálaí",
|
||||||
|
"downvotes_disabled": "Síosvótaí faoi mhíchumas",
|
||||||
|
"enable_downvotes": "Cumasaigh Síosvótaí",
|
||||||
|
"open_registration": "Clárú Oscailte",
|
||||||
|
"registration_closed": "Clárú dúnta",
|
||||||
|
"enable_nsfw": "Cumasaigh NSFW",
|
||||||
|
"must_login": "Caithfidh tú <1>logáil isteach nó clárú</1> chun trácht a dhéanamh.",
|
||||||
|
"community_ban": "Cuireadh cosc ort ón bpobal seo.",
|
||||||
|
"site_ban": "Cuireadh cosc ort ón suíomh",
|
||||||
|
"couldnt_create_comment": "Níorbh fhéidir a chruthú trácht.",
|
||||||
|
"couldnt_like_comment": "Níorbh fhéidir a is maith trácht.",
|
||||||
|
"couldnt_update_comment": "Níorbh fhéidir trácht a nuashonrú.",
|
||||||
|
"couldnt_save_comment": "Níorbh fhéidir trácht a shábháil.",
|
||||||
|
"couldnt_get_comments": "Níorbh fhéidir tuairimí a fháil.",
|
||||||
|
"no_community_edit_allowed": "Ní cheadaítear an pobal a chur in eagar.",
|
||||||
|
"couldnt_find_community": "Níorbh fhéidir Pobal a aimsiú.",
|
||||||
|
"couldnt_update_community": "Níorbh fhéidir an Pobal a nuashonrú.",
|
||||||
|
"community_already_exists": "Pobal ann cheana féin.",
|
||||||
|
"community_moderator_already_exists": "Tá modhnóir pobail ann cheana féin.",
|
||||||
|
"community_follower_already_exists": "Tá leantóir pobail ann cheana féin.",
|
||||||
|
"community_user_already_banned": "Toirmisctear úsáideoir pobail cheana féin.",
|
||||||
|
"couldnt_create_post": "Níorbh fhéidir postáil a chruthú.",
|
||||||
|
"post_title_too_long": "Tá teideal an postáil ró-fhada.",
|
||||||
|
"couldnt_like_post": "Níorbh fhéidir a is maith post.",
|
||||||
|
"couldnt_find_post": "Níorbh fhéidir an post a aimsiú.",
|
||||||
|
"couldnt_get_posts": "Níorbh fhéidir an post a fháil",
|
||||||
|
"couldnt_update_post": "Níorbh fhéidir an post a nuashonrú",
|
||||||
|
"not_a_moderator": "Ní modhnóir.",
|
||||||
|
"system_err_login": "Earráid chórais. Bain triail as logáil amach agus ar ais isteach.",
|
||||||
|
"couldnt_create_private_message": "Níorbh fhéidir teachtaireacht phríobháideach a chruthú.",
|
||||||
|
"couldnt_update_private_message": "Níorbh fhéidir teachtaireacht phríobháideach a nuashonrú.",
|
||||||
|
"action": "Gníomh",
|
||||||
|
"emoji_picker": "Piocálaí Emoji",
|
||||||
|
"block_leaving": "An bhfuil tú cinnte gur mhaith leat imeacht?",
|
||||||
|
"what_is": "Cád é",
|
||||||
|
"cake_day_title": "Lá císte:",
|
||||||
|
"cake_day_info": "Lá císte {{ creator_name }} é atá ann inniu!",
|
||||||
|
"invalid_post_title": "Teideal poist neamhbhailí",
|
||||||
|
"invalid_url": "URL neamhbhailí.",
|
||||||
|
"couldnt_find_that_username_or_email": "Níorbh fhéidir an t-ainm úsáideora nó an ríomhphost sin a fháil.",
|
||||||
|
"admin_already_created": "Tá brón orm, tá riarthóir ann cheana féin.",
|
||||||
|
"number_of_downvotes_0": "{{count}} Síosvótáil",
|
||||||
|
"number_of_downvotes_1": "{{count}} Síosvótaí",
|
||||||
|
"number_of_downvotes_2": "{{count}} Síosvótaí",
|
||||||
|
"number_of_downvotes_3": "{{count}} Síosvótaí",
|
||||||
|
"number_of_downvotes_4": "{{count}} Síosvótaí",
|
||||||
|
"upvote": "Suasvótáil",
|
||||||
|
"downvote": "Síosvótáil",
|
||||||
|
"url": "URL",
|
||||||
|
"number_of_upvotes_0": "{{count}} Suasvótáil",
|
||||||
|
"number_of_upvotes_1": "{{count}} Suasvótaí",
|
||||||
|
"number_of_upvotes_2": "{{count}} Suasvótaí",
|
||||||
|
"number_of_upvotes_3": "{{count}} Suasvótaí",
|
||||||
|
"number_of_upvotes_4": "{{count}} Suasvótaí",
|
||||||
|
"nsfw": "NSFW",
|
||||||
|
"show_nsfw": "Taispeáin ábhar NSFW",
|
||||||
|
"theme": "Téama",
|
||||||
|
"site_saved": "Sábháil Suíomh.",
|
||||||
|
"no_private_message_edit_allowed": "Ní cheadaítear teachtaireacht phríobháideach a chur in eagar.",
|
||||||
|
"no_comment_edit_allowed": "Ní cheadaítear trácht a chur in eagar.",
|
||||||
|
"no_post_edit_allowed": "Ní cheadaítear an post a chur in eagar.",
|
||||||
|
"couldnt_save_post": "Níorbh fhéidir an post a shábháil.",
|
||||||
|
"no_slurs": "Uimh masla.",
|
||||||
|
"not_an_admin": "Ní riarthóir é.",
|
||||||
|
"site_already_exists": "Suíomh ann cheana.",
|
||||||
|
"couldnt_update_site": "Níorbh fhéidir an suíomh a nuashonrú.",
|
||||||
|
"password_incorrect": "Pasfhocal mícheart.",
|
||||||
|
"passwords_dont_match": "Ní hionann pasfhocail.",
|
||||||
|
"no_password_reset": "Ní bheidh tú in ann do phasfhocal a athshocrú gan ríomhphost.",
|
||||||
|
"invalid_username": "Ainm Úsáideora neamhbhailí.",
|
||||||
|
"user_already_exists": "Úsáideoir ann cheana.",
|
||||||
|
"email_already_exists": "Tá ríomhphost ann cheana féin.",
|
||||||
|
"couldnt_update_user": "Níorbh fhéidir an t-úsáideoir a nuashonrú.",
|
||||||
|
"time": "Am"
|
||||||
|
}
|
||||||
|
|
23
ui/translations/ru.json
vendored
23
ui/translations/ru.json
vendored
|
@ -174,7 +174,7 @@
|
||||||
"send_message": "Послать сообщение",
|
"send_message": "Послать сообщение",
|
||||||
"message": "Сообщение",
|
"message": "Сообщение",
|
||||||
"avatar": "Аватар",
|
"avatar": "Аватар",
|
||||||
"show_avatars": "Показать Аватары",
|
"show_avatars": "Показывать аватары",
|
||||||
"formatting_help": "Помощь в верстке текста",
|
"formatting_help": "Помощь в верстке текста",
|
||||||
"sticky": "приклеить",
|
"sticky": "приклеить",
|
||||||
"stickied": "закрепленный пост",
|
"stickied": "закрепленный пост",
|
||||||
|
@ -187,7 +187,7 @@
|
||||||
"old_password": "Действующий пароль",
|
"old_password": "Действующий пароль",
|
||||||
"forgot_password": "я забыл(а) пароль",
|
"forgot_password": "я забыл(а) пароль",
|
||||||
"reset_password_mail_sent": "Письмо для восстановления пароля было выслано.",
|
"reset_password_mail_sent": "Письмо для восстановления пароля было выслано.",
|
||||||
"private_message_disclaimer": "Предупреждение: Приватные сообщения Lemmy на данный момент не зашифрованы. Для безопасной коммуникации создайте аккаунт на <1>Riot.im</1>.",
|
"private_message_disclaimer": "Предупреждение: Приватные сообщения Lemmy на данный момент не зашифрованы. Для безопасной коммуникации создайте аккаунт на <1>Element.io</1>.",
|
||||||
"send_notifications_to_email": "Посылать уведомления на e-mail адрес",
|
"send_notifications_to_email": "Посылать уведомления на e-mail адрес",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"browser_default": "Браузер по умолчанию",
|
"browser_default": "Браузер по умолчанию",
|
||||||
|
@ -208,7 +208,7 @@
|
||||||
"messages": "Сообщения",
|
"messages": "Сообщения",
|
||||||
"new_password": "Новый пароль",
|
"new_password": "Новый пароль",
|
||||||
"theme": "Визуальная тема",
|
"theme": "Визуальная тема",
|
||||||
"post_title_too_long": "Длина названия поста превышает допустимый лимит.",
|
"post_title_too_long": "Длина названия записи превышает допустимый лимит.",
|
||||||
"time": "Время",
|
"time": "Время",
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
"view_source": "исходный код сообщения",
|
"view_source": "исходный код сообщения",
|
||||||
|
@ -232,7 +232,7 @@
|
||||||
"banned": "забаненный",
|
"banned": "забаненный",
|
||||||
"password_change": "Смена пароля",
|
"password_change": "Смена пароля",
|
||||||
"no_email_setup": "Этот сервер неправильно настроил электронную почту.",
|
"no_email_setup": "Этот сервер неправильно настроил электронную почту.",
|
||||||
"matrix_user_id": "Матрица пользователя",
|
"matrix_user_id": "Адрес в Matrix",
|
||||||
"are_you_sure": "вы уверены?",
|
"are_you_sure": "вы уверены?",
|
||||||
"archive_link": "архивировать ссылку",
|
"archive_link": "архивировать ссылку",
|
||||||
"logged_in": "Вошли в систему.",
|
"logged_in": "Вошли в систему.",
|
||||||
|
@ -269,5 +269,18 @@
|
||||||
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
|
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
|
||||||
"no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
|
"no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
|
||||||
"cake_day_title": "День торта:",
|
"cake_day_title": "День торта:",
|
||||||
"what_is": "Что такое"
|
"what_is": "Что такое",
|
||||||
|
"superscript": "верхний индекс",
|
||||||
|
"cake_day_info": "Сегодня день торта у {{ creator_name }}!",
|
||||||
|
"invalid_post_title": "Недопустимый заголовок записи",
|
||||||
|
"bold": "жирный",
|
||||||
|
"italic": "курсив",
|
||||||
|
"subscript": "нижний индекс",
|
||||||
|
"header": "заголовок",
|
||||||
|
"strikethrough": "зачёркивание",
|
||||||
|
"quote": "цитата",
|
||||||
|
"spoiler": "спойлер",
|
||||||
|
"list": "список",
|
||||||
|
"not_a_moderator": "Не модератор.",
|
||||||
|
"invalid_url": "Недопустимый URL."
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue