mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-25 18:36:15 +00:00
Merge branch 'main' into inbox-refactoring
This commit is contained in:
commit
cee72065e9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -16,3 +16,5 @@ ui/src/translations
|
|||
|
||||
# ide config
|
||||
.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
|
||||
RUN apk add libpq
|
||||
|
||||
# Install Espeak for captchas
|
||||
RUN apk add espeak
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
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
|
||||
RUN apk add libpq
|
||||
|
||||
# Install Espeak for captchas
|
||||
RUN apk add espeak
|
||||
|
||||
RUN addgroup -g 1000 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
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.7.30
|
||||
image: dessalines/lemmy:v0.7.33
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
|
4
docker/travis/docker_push.sh
vendored
4
docker/travis/docker_push.sh
vendored
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
docker tag dessalines/lemmy:travis \
|
||||
dessalines/lemmy:v0.7.30
|
||||
docker push dessalines/lemmy:v0.7.30
|
||||
dessalines/lemmy:v0.7.33
|
||||
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>,
|
||||
password: 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`
|
||||
|
||||
#### 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
|
||||
##### Request
|
||||
```rust
|
||||
|
|
295
server/Cargo.lock
generated
vendored
295
server/Cargo.lock
generated
vendored
|
@ -31,7 +31,7 @@ checksum = "a9028932f36d45df020c92317ccb879ab77d8f066f57ff143dd5bee93ba3de0d"
|
|||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix_derive",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"derive_more",
|
||||
|
@ -54,7 +54,7 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -94,7 +94,7 @@ dependencies = [
|
|||
"actix-http",
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
|
@ -120,7 +120,7 @@ dependencies = [
|
|||
"actix-tls",
|
||||
"actix-utils",
|
||||
"base64 0.12.3",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"brotli2",
|
||||
"bytes",
|
||||
"cookie",
|
||||
|
@ -280,7 +280,7 @@ dependencies = [
|
|||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"bytes",
|
||||
"either",
|
||||
"futures",
|
||||
|
@ -382,6 +382,12 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.13"
|
||||
|
@ -496,6 +502,15 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.9.3"
|
||||
|
@ -539,6 +554,12 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
|
@ -642,6 +663,12 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db7a1029718df60331e557c9e83a55523c955e5dd2a7bfeffad6bbd50b538ae9"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.3.4"
|
||||
|
@ -663,6 +690,26 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cc"
|
||||
version = "1.0.58"
|
||||
|
@ -695,7 +742,7 @@ checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
|
|||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
|
@ -708,7 +755,7 @@ version = "0.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -717,9 +764,15 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
|
||||
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]]
|
||||
name = "comrak"
|
||||
version = "0.7.0"
|
||||
|
@ -806,6 +859,43 @@ dependencies = [
|
|||
"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]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.7.2"
|
||||
|
@ -852,6 +942,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.9.0"
|
||||
|
@ -894,7 +994,7 @@ version = "1.4.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"diesel_derives",
|
||||
|
@ -1072,6 +1172,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
|
@ -1167,7 +1276,7 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"fuchsia-zircon-sys",
|
||||
]
|
||||
|
||||
|
@ -1311,6 +1420,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gimli"
|
||||
version = "0.22.0"
|
||||
|
@ -1455,6 +1574,23 @@ dependencies = [
|
|||
"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]]
|
||||
name = "indexmap"
|
||||
version = "1.5.0"
|
||||
|
@ -1465,6 +1601,12 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inflate"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1238524675af3938a7c74980899535854b88ba07907bb1c944abe5b8fc437e5"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.6"
|
||||
|
@ -1507,6 +1649,16 @@ version = "0.4.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "js-sys"
|
||||
version = "0.3.42"
|
||||
|
@ -1585,6 +1737,7 @@ dependencies = [
|
|||
"awc",
|
||||
"base64 0.12.3",
|
||||
"bcrypt",
|
||||
"captcha",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
|
@ -1673,7 +1826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"cfg-if",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
|
@ -1719,6 +1872,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "log"
|
||||
version = "0.4.11"
|
||||
|
@ -1737,6 +1902,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
|
@ -1755,12 +1926,27 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "migrations_internals"
|
||||
version = "1.4.1"
|
||||
|
@ -1920,6 +2106,27 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
|
@ -1978,7 +2185,7 @@ version = "0.10.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"lazy_static",
|
||||
|
@ -2153,6 +2360,18 @@ version = "0.3.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.8"
|
||||
|
@ -2225,6 +2444,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
|
@ -2385,6 +2614,31 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
|
@ -2437,6 +2691,15 @@ dependencies = [
|
|||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ef54b45ae131327a88597e2463fee4098ad6c88ba7b6af4b3987db8aad4098"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.15"
|
||||
|
@ -2521,6 +2784,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -2543,7 +2812,7 @@ version = "0.4.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.2.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"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"] }
|
||||
sha2 = "0.9"
|
||||
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
|
||||
allowed_instances: ""
|
||||
}
|
||||
captcha: {
|
||||
enabled: true
|
||||
difficulty: medium # Can be easy, medium, or hard
|
||||
}
|
||||
# # email sending configuration
|
||||
# email: {
|
||||
# # hostname and port of the smtp server
|
||||
|
|
|
@ -17,6 +17,7 @@ pub struct Settings {
|
|||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation: Federation,
|
||||
pub captcha: CaptchaConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
@ -46,6 +47,12 @@ pub struct EmailConfig {
|
|||
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)]
|
||||
pub struct Database {
|
||||
pub user: String,
|
||||
|
|
|
@ -370,6 +370,8 @@ impl Perform for Oper<GetSite> {
|
|||
password_verify: setup.admin_password.to_owned(),
|
||||
admin: true,
|
||||
show_nsfw: true,
|
||||
captcha_uuid: None,
|
||||
captcha_answer: None,
|
||||
};
|
||||
let login_response = Oper::new(register, self.client.clone())
|
||||
.perform(pool, websocket_info.clone())
|
||||
|
|
|
@ -2,8 +2,9 @@ use crate::{
|
|||
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||
apub::ApubObjectType,
|
||||
blocking,
|
||||
captcha_espeak_wav_base64,
|
||||
websocket::{
|
||||
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
||||
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
||||
UserOperation,
|
||||
WebsocketInfo,
|
||||
},
|
||||
|
@ -11,6 +12,8 @@ use crate::{
|
|||
LemmyError,
|
||||
};
|
||||
use bcrypt::verify;
|
||||
use captcha::{gen, Difficulty};
|
||||
use chrono::Duration;
|
||||
use lemmy_db::{
|
||||
comment::*,
|
||||
comment_view::*,
|
||||
|
@ -66,6 +69,23 @@ pub struct Register {
|
|||
pub password_verify: String,
|
||||
pub admin: 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)]
|
||||
|
@ -303,7 +323,7 @@ impl Perform for Oper<Register> {
|
|||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, LemmyError> {
|
||||
let data: &Register = &self.data;
|
||||
|
||||
|
@ -320,6 +340,31 @@ impl Perform for Oper<Register> {
|
|||
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) {
|
||||
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)]
|
||||
impl Perform for Oper<SaveUserSettings> {
|
||||
type Response = LoginResponse;
|
||||
|
|
|
@ -7,7 +7,9 @@ pub extern crate lazy_static;
|
|||
pub extern crate failure;
|
||||
pub extern crate actix;
|
||||
pub extern crate actix_web;
|
||||
pub extern crate base64;
|
||||
pub extern crate bcrypt;
|
||||
pub extern crate captcha;
|
||||
pub extern crate chrono;
|
||||
pub extern crate diesel;
|
||||
pub extern crate dotenv;
|
||||
|
@ -35,6 +37,7 @@ use lemmy_utils::{get_apub_protocol_string, settings::Settings};
|
|||
use log::error;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
|
||||
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
||||
pub type ConnectionId = usize;
|
||||
|
@ -224,9 +227,56 @@ where
|
|||
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)]
|
||||
mod tests {
|
||||
use crate::is_image_content_type;
|
||||
use crate::{captcha_espeak_wav_base64, is_image_content_type};
|
||||
|
||||
#[test]
|
||||
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
|
||||
// #[test]
|
||||
// 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>))
|
||||
// Account actions. I don't like that they're in /user maybe /accounts
|
||||
.route("/login", web::post().to(route_post::<Login>))
|
||||
.route("/get_captcha", web::get().to(route_post::<GetCaptcha>))
|
||||
.route(
|
||||
"/delete_account",
|
||||
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 {
|
||||
Login,
|
||||
Register,
|
||||
GetCaptcha,
|
||||
CreateCommunity,
|
||||
CreatePost,
|
||||
ListCommunities,
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
|||
UserId,
|
||||
};
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::naive_now;
|
||||
|
||||
/// Chat server sends this messages to session
|
||||
#[derive(Message)]
|
||||
|
@ -134,6 +135,21 @@ pub struct SessionInfo {
|
|||
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
|
||||
/// session.
|
||||
pub struct ChatServer {
|
||||
|
@ -158,6 +174,9 @@ pub struct ChatServer {
|
|||
/// Rate limiting based on rate type and IP addr
|
||||
rate_limiter: RateLimit,
|
||||
|
||||
/// A list of the current captchas
|
||||
captchas: Vec<CaptchaItem>,
|
||||
|
||||
/// An HTTP Client
|
||||
client: Client,
|
||||
}
|
||||
|
@ -176,6 +195,7 @@ impl ChatServer {
|
|||
rng: rand::thread_rng(),
|
||||
pool,
|
||||
rate_limiter,
|
||||
captchas: Vec::new(),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
@ -441,6 +461,7 @@ impl ChatServer {
|
|||
// User ops
|
||||
UserOperation::Login => do_user_operation::<Login>(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::GetReplies => do_user_operation::<GetReplies>(args).await,
|
||||
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
||||
|
@ -635,7 +656,7 @@ impl Handler<StandardMessage> for ChatServer {
|
|||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(m) => {
|
||||
info!("Message Sent: {}", m);
|
||||
// info!("Message Sent: {}", m);
|
||||
Ok(m)
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -774,3 +795,30 @@ where
|
|||
};
|
||||
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 {
|
||||
if (this.state.community.name) {
|
||||
return `/c/${this.state.community.name} - ${this.state.site.name}`;
|
||||
if (this.state.community.title) {
|
||||
return `${this.state.community.title} - ${this.state.site.name}`;
|
||||
} else {
|
||||
return 'Lemmy';
|
||||
}
|
||||
|
@ -187,7 +187,6 @@ export class Community extends Component<any, State> {
|
|||
return (
|
||||
<div class="container">
|
||||
<Helmet title={this.documentTitle} />
|
||||
{this.selects()}
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -197,19 +196,7 @@ export class Community extends Component<any, State> {
|
|||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<h5>
|
||||
{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.selects()}
|
||||
{this.listings()}
|
||||
{this.paginator()}
|
||||
</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() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`pointer btn btn-outline-secondary
|
||||
${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 {
|
||||
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
|
||||
}`;
|
||||
} else {
|
||||
|
@ -171,7 +171,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
|
||||
unreadOrAllRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`btn btn-outline-secondary pointer
|
||||
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
||||
|
@ -204,7 +204,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
|
||||
messageTypeRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`btn btn-outline-secondary pointer
|
||||
${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() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`btn btn-outline-secondary
|
||||
${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,
|
||||
PasswordResetForm,
|
||||
GetSiteResponse,
|
||||
GetCaptchaResponse,
|
||||
WebSocketJsonResponse,
|
||||
Site,
|
||||
} from '../interfaces';
|
||||
|
@ -21,11 +22,8 @@ interface State {
|
|||
registerForm: RegisterForm;
|
||||
loginLoading: boolean;
|
||||
registerLoading: boolean;
|
||||
mathQuestion: {
|
||||
a: number;
|
||||
b: number;
|
||||
answer: number;
|
||||
};
|
||||
captcha: GetCaptchaResponse;
|
||||
captchaPlaying: boolean;
|
||||
site: Site;
|
||||
}
|
||||
|
||||
|
@ -43,14 +41,13 @@ export class Login extends Component<any, State> {
|
|||
password_verify: undefined,
|
||||
admin: false,
|
||||
show_nsfw: false,
|
||||
captcha_uuid: undefined,
|
||||
captcha_answer: undefined,
|
||||
},
|
||||
loginLoading: false,
|
||||
registerLoading: false,
|
||||
mathQuestion: {
|
||||
a: Math.floor(Math.random() * 10) + 1,
|
||||
b: Math.floor(Math.random() * 10) + 1,
|
||||
answer: undefined,
|
||||
},
|
||||
captcha: undefined,
|
||||
captchaPlaying: false,
|
||||
site: {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
|
@ -81,6 +78,7 @@ export class Login extends Component<any, State> {
|
|||
);
|
||||
|
||||
WebSocketService.Instance.getSite();
|
||||
WebSocketService.Instance.getCaptcha();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -172,6 +170,7 @@ export class Login extends Component<any, State> {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
registerForm() {
|
||||
return (
|
||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
||||
|
@ -258,23 +257,37 @@ export class Login extends Component<any, State> {
|
|||
/>
|
||||
</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
|
||||
type="number"
|
||||
id="register-math"
|
||||
type="text"
|
||||
class="form-control"
|
||||
value={this.state.mathQuestion.answer}
|
||||
onInput={linkEvent(this, this.handleMathAnswerChange)}
|
||||
id="register-captcha"
|
||||
value={this.state.registerForm.captcha_answer}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleRegisterCaptchaAnswerChange
|
||||
)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.site.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
|
@ -295,11 +308,7 @@ export class Login extends Component<any, State> {
|
|||
)}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
disabled={this.mathCheck}
|
||||
>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
{this.state.registerLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<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) {
|
||||
event.preventDefault();
|
||||
i.state.loginLoading = true;
|
||||
|
@ -335,11 +374,8 @@ export class Login extends Component<any, State> {
|
|||
event.preventDefault();
|
||||
i.state.registerLoading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
if (!i.mathCheck) {
|
||||
WebSocketService.Instance.register(i.state.registerForm);
|
||||
}
|
||||
}
|
||||
|
||||
handleRegisterUsernameChange(i: Login, event: any) {
|
||||
i.state.registerForm.username = event.target.value;
|
||||
|
@ -369,11 +405,16 @@ export class Login extends Component<any, State> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleMathAnswerChange(i: Login, event: any) {
|
||||
i.state.mathQuestion.answer = event.target.value;
|
||||
handleRegisterCaptchaAnswerChange(i: Login, event: any) {
|
||||
i.state.registerForm.captcha_answer = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleRegenCaptcha(_i: Login, _event: any) {
|
||||
event.preventDefault();
|
||||
WebSocketService.Instance.getCaptcha();
|
||||
}
|
||||
|
||||
handlePasswordReset(i: Login) {
|
||||
event.preventDefault();
|
||||
let resetForm: PasswordResetForm = {
|
||||
|
@ -382,11 +423,21 @@ export class Login extends Component<any, State> {
|
|||
WebSocketService.Instance.passwordReset(resetForm);
|
||||
}
|
||||
|
||||
get mathCheck(): boolean {
|
||||
return (
|
||||
this.state.mathQuestion.answer !=
|
||||
this.state.mathQuestion.a + this.state.mathQuestion.b
|
||||
);
|
||||
handleCaptchaPlay(i: Login) {
|
||||
event.preventDefault();
|
||||
let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
|
||||
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) {
|
||||
|
@ -394,6 +445,9 @@ export class Login extends Component<any, State> {
|
|||
if (msg.error) {
|
||||
toast(i18n.t(msg.error), 'danger');
|
||||
this.state = this.emptyState;
|
||||
this.state.registerForm.captcha_answer = undefined;
|
||||
// Refetch another captcha
|
||||
WebSocketService.Instance.getCaptcha();
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else {
|
||||
|
@ -412,6 +466,13 @@ export class Login extends Component<any, State> {
|
|||
UserService.Instance.login(data);
|
||||
WebSocketService.Instance.userJoin();
|
||||
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) {
|
||||
toast(i18n.t('reset_password_mail_sent'));
|
||||
} 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">
|
||||
{this.posts()}
|
||||
</main>
|
||||
<aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
|
||||
<aside class="col-12 col-md-4">{this.mySidebar()}</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
my_sidebar() {
|
||||
mySidebar() {
|
||||
return (
|
||||
<div>
|
||||
{!this.state.loading && (
|
||||
<div>
|
||||
<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">
|
||||
{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 && (
|
||||
<div>
|
||||
<h5>
|
||||
|
@ -234,42 +290,7 @@ export class Main extends Component<any, MainState> {
|
|||
))}
|
||||
</ul>
|
||||
</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() {
|
||||
return (
|
||||
<div>
|
||||
<div class="card bg-transparent border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
|
||||
{this.canAdmin && (
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
{this.state.siteRes.site.description && this.siteDescription()}
|
||||
{this.badges()}
|
||||
{this.admins()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
siteName() {
|
||||
return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
|
||||
}
|
||||
|
||||
admins() {
|
||||
return (
|
||||
<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">
|
||||
<UserListing
|
||||
user={{
|
||||
name: admin.name,
|
||||
avatar: admin.avatar,
|
||||
local: admin.local,
|
||||
actor_id: admin.actor_id,
|
||||
id: admin.id,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
badges() {
|
||||
return (
|
||||
<ul class="my-2 list-inline">
|
||||
{/*
|
||||
<li className="list-inline-item badge badge-light">
|
||||
|
@ -355,44 +392,41 @@ export class Main extends Component<any, MainState> {
|
|||
</Link>
|
||||
</li>
|
||||
</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">
|
||||
<UserListing
|
||||
user={{
|
||||
name: admin.name,
|
||||
avatar: admin.avatar,
|
||||
local: admin.local,
|
||||
actor_id: admin.actor_id,
|
||||
id: admin.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
adminButtons() {
|
||||
return (
|
||||
this.canAdmin && (
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.siteRes.site.description && (
|
||||
<div class="card bg-transparent border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
siteDescription() {
|
||||
return (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.siteRes.site.description
|
||||
)}
|
||||
dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
landing() {
|
||||
return (
|
||||
<div class="card bg-transparent border-secondary">
|
||||
<div class="card-body">
|
||||
<>
|
||||
<h5>
|
||||
{i18n.t('powered_by')}
|
||||
<svg class="icon mx-2">
|
||||
|
@ -426,15 +460,13 @@ export class Main extends Component<any, MainState> {
|
|||
</a>
|
||||
</T>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
posts() {
|
||||
return (
|
||||
<div class="main-content-wrapper">
|
||||
{this.selects()}
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -443,6 +475,7 @@ export class Main extends Component<any, MainState> {
|
|||
</h5>
|
||||
) : (
|
||||
<div>
|
||||
{this.selects()}
|
||||
{this.listings()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
|
|
5
ui/src/components/navbar.tsx
vendored
5
ui/src/components/navbar.tsx
vendored
|
@ -31,7 +31,7 @@ import {
|
|||
md,
|
||||
setTheme,
|
||||
} from '../utils';
|
||||
import { i18n, i18nextSetup } from '../i18next';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface NavbarState {
|
||||
isLoggedIn: boolean;
|
||||
|
@ -435,12 +435,11 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
this.requestNotificationPermission();
|
||||
this.fetchUnreads();
|
||||
setTheme(data.my_user.theme, true);
|
||||
i18n.changeLanguage(data.my_user.lang);
|
||||
}
|
||||
this.state.isLoggedIn = true;
|
||||
}
|
||||
|
||||
i18nextSetup();
|
||||
|
||||
this.state.siteLoading = false;
|
||||
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 className="col-12">
|
||||
<div className="post-title">
|
||||
<h5 className="mb-0 d-inline">
|
||||
<h5 className="mb-1 d-inline-block">
|
||||
{this.props.showBody && post.url ? (
|
||||
<a
|
||||
className="text-body"
|
||||
|
@ -434,7 +434,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</div>
|
||||
<div class="row">
|
||||
<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">
|
||||
<span>{i18n.t('by')} </span>
|
||||
<UserListing
|
||||
|
@ -501,9 +501,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</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 && (
|
||||
<>
|
||||
<li className="list-inline-item">•</li>
|
||||
<span
|
||||
class="unselectable pointer mr-2"
|
||||
data-tippy-content={this.pointsTippy}
|
||||
|
@ -525,23 +543,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</span>
|
||||
</li>
|
||||
</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>
|
||||
{this.props.post.duplicates && (
|
||||
<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() {
|
||||
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
|
||||
className={`btn btn-outline-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||
|
@ -286,7 +286,7 @@ export class Post extends Component<any, PostState> {
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-toggle mb-2">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`btn btn-outline-secondary pointer ${
|
||||
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
|
||||
type="text"
|
||||
class="form-control mr-2"
|
||||
class="form-control mr-2 mb-2"
|
||||
value={this.state.searchText}
|
||||
placeholder={`${i18n.t('search')}...`}
|
||||
onInput={linkEvent(this, this.handleQChange)}
|
||||
required
|
||||
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 ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
|
@ -222,7 +222,7 @@ export class Search extends Component<any, SearchState> {
|
|||
<select
|
||||
value={this.state.type_}
|
||||
onChange={linkEvent(this, this.handleTypeChange)}
|
||||
class="custom-select w-auto"
|
||||
class="custom-select w-auto mb-2"
|
||||
>
|
||||
<option disabled>{i18n.t('type')}</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,
|
||||
admin: true,
|
||||
show_nsfw: true,
|
||||
// The first admin signup doesn't need a captcha
|
||||
captcha_uuid: '',
|
||||
captcha_answer: '',
|
||||
},
|
||||
doneRegisteringUser: false,
|
||||
userLoading: false,
|
||||
|
|
251
ui/src/components/sidebar.tsx
vendored
251
ui/src/components/sidebar.tsx
vendored
|
@ -9,7 +9,7 @@ import {
|
|||
UserView,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, getUnixTime, hostname } from '../utils';
|
||||
import { mdToHtml, getUnixTime } from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
|
@ -63,21 +63,30 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
}
|
||||
|
||||
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 (
|
||||
<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-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>
|
||||
{community.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
|
@ -89,8 +98,129 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
{i18n.t('deleted')}
|
||||
</small>
|
||||
)}
|
||||
{community.nsfw && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('nsfw')}
|
||||
</small>
|
||||
)}
|
||||
</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 />
|
||||
</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">
|
||||
{this.canMod && (
|
||||
<>
|
||||
|
@ -111,9 +241,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
data-tippy-content={
|
||||
!community.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')
|
||||
!community.deleted ? i18n.t('delete') : i18n.t('restore')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
|
@ -175,96 +303,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
</div>
|
||||
</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) {
|
||||
event.preventDefault();
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
follow: false,
|
||||
|
@ -301,6 +341,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
}
|
||||
|
||||
handleSubscribe(communityId: number) {
|
||||
event.preventDefault();
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
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
|
||||
value={this.state.sort}
|
||||
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>
|
||||
{!this.props.hideHot && (
|
||||
|
|
6
ui/src/components/symbols.tsx
vendored
6
ui/src/components/symbols.tsx
vendored
|
@ -15,6 +15,12 @@ export class Symbols extends Component<any, any> {
|
|||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="icon-refresh-cw" viewBox="0 0 24 24">
|
||||
<path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-play" viewBox="0 0 24 24">
|
||||
<path d="M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-strikethrough" viewBox="0 0 28 28">
|
||||
<path d="M27.5 14c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-27c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h27zM7.547 13c-0.297-0.375-0.562-0.797-0.797-1.25-0.5-1.016-0.75-2-0.75-2.938 0-1.906 0.703-3.5 2.094-4.828s3.437-1.984 6.141-1.984c0.594 0 1.453 0.109 2.609 0.297 0.688 0.125 1.609 0.375 2.766 0.75 0.109 0.406 0.219 1.031 0.328 1.844 0.141 1.234 0.219 2.187 0.219 2.859 0 0.219-0.031 0.453-0.078 0.703l-0.187 0.047-1.313-0.094-0.219-0.031c-0.531-1.578-1.078-2.641-1.609-3.203-0.922-0.953-2.031-1.422-3.281-1.422-1.188 0-2.141 0.313-2.844 0.922s-1.047 1.375-1.047 2.281c0 0.766 0.344 1.484 1.031 2.188s2.141 1.375 4.359 2.016c0.75 0.219 1.641 0.562 2.703 1.031 0.562 0.266 1.062 0.531 1.484 0.812h-11.609zM15.469 17h6.422c0.078 0.438 0.109 0.922 0.109 1.437 0 1.125-0.203 2.234-0.641 3.313-0.234 0.578-0.594 1.109-1.109 1.625-0.375 0.359-0.938 0.781-1.703 1.266-0.781 0.469-1.563 0.828-2.391 1.031-0.828 0.219-1.875 0.328-3.172 0.328-0.859 0-1.891-0.031-3.047-0.359l-2.188-0.625c-0.609-0.172-0.969-0.313-1.125-0.438-0.063-0.063-0.125-0.172-0.125-0.344v-0.203c0-0.125 0.031-0.938-0.031-2.438-0.031-0.781 0.031-1.328 0.031-1.641v-0.688l1.594-0.031c0.578 1.328 0.844 2.125 1.016 2.406 0.375 0.609 0.797 1.094 1.25 1.469s1 0.672 1.641 0.891c0.625 0.234 1.328 0.344 2.063 0.344 0.656 0 1.391-0.141 2.172-0.422 0.797-0.266 1.437-0.719 1.906-1.344 0.484-0.625 0.734-1.297 0.734-2.016 0-0.875-0.422-1.687-1.266-2.453-0.344-0.297-1.062-0.672-2.141-1.109z"></path>
|
||||
</symbol>
|
||||
|
|
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 {
|
||||
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 {
|
||||
return 'Lemmy';
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ export class User extends Component<any, UserState> {
|
|||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>/u/{this.state.username}</span>
|
||||
<span>@{this.state.username}</span>
|
||||
</h5>
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
|
@ -274,7 +274,7 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
viewRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
||||
<label
|
||||
className={`btn btn-outline-secondary pointer
|
||||
${this.state.view == UserDetailsView.Overview && 'active'}
|
||||
|
|
3
ui/src/i18next.ts
vendored
3
ui/src/i18next.ts
vendored
|
@ -65,7 +65,6 @@ function format(value: any, format: any, lng: any): any {
|
|||
return format === 'uppercase' ? value.toUpperCase() : value;
|
||||
}
|
||||
|
||||
export function i18nextSetup() {
|
||||
i18next.init({
|
||||
debug: false,
|
||||
// load: 'languageOnly',
|
||||
|
@ -76,5 +75,5 @@ export function i18nextSetup() {
|
|||
resources,
|
||||
interpolation: { format },
|
||||
});
|
||||
}
|
||||
|
||||
export { i18next as i18n, resources };
|
||||
|
|
12
ui/src/interfaces.ts
vendored
12
ui/src/interfaces.ts
vendored
|
@ -1,6 +1,7 @@
|
|||
export enum UserOperation {
|
||||
Login,
|
||||
Register,
|
||||
GetCaptcha,
|
||||
CreateCommunity,
|
||||
CreatePost,
|
||||
ListCommunities,
|
||||
|
@ -572,6 +573,16 @@ export interface RegisterForm {
|
|||
password_verify: string;
|
||||
admin: boolean;
|
||||
show_nsfw: boolean;
|
||||
captcha_uuid?: string;
|
||||
captcha_answer?: string;
|
||||
}
|
||||
|
||||
export interface GetCaptchaResponse {
|
||||
ok?: {
|
||||
png: string;
|
||||
wav?: string;
|
||||
uuid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
|
@ -1010,6 +1021,7 @@ type ResponseType =
|
|||
| CommentResponse
|
||||
| UserMentionResponse
|
||||
| LoginResponse
|
||||
| GetCaptchaResponse
|
||||
| GetModlogResponse
|
||||
| SearchResponse
|
||||
| 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));
|
||||
}
|
||||
|
||||
public getCaptcha() {
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.GetCaptcha, {}));
|
||||
}
|
||||
|
||||
public createCommunity(form: CommunityForm) {
|
||||
this.setAuth(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.",
|
||||
"passwords_dont_match": "Passwords do not match.",
|
||||
"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.",
|
||||
"admin_already_created": "Sorry, there's already an admin.",
|
||||
"user_already_exists": "User already exists.",
|
||||
|
@ -281,5 +283,6 @@
|
|||
"cake_day_title": "Cake day:",
|
||||
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
|
||||
"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": "Послать сообщение",
|
||||
"message": "Сообщение",
|
||||
"avatar": "Аватар",
|
||||
"show_avatars": "Показать Аватары",
|
||||
"show_avatars": "Показывать аватары",
|
||||
"formatting_help": "Помощь в верстке текста",
|
||||
"sticky": "приклеить",
|
||||
"stickied": "закрепленный пост",
|
||||
|
@ -187,7 +187,7 @@
|
|||
"old_password": "Действующий пароль",
|
||||
"forgot_password": "я забыл(а) пароль",
|
||||
"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 адрес",
|
||||
"language": "Язык",
|
||||
"browser_default": "Браузер по умолчанию",
|
||||
|
@ -208,7 +208,7 @@
|
|||
"messages": "Сообщения",
|
||||
"new_password": "Новый пароль",
|
||||
"theme": "Визуальная тема",
|
||||
"post_title_too_long": "Длина названия поста превышает допустимый лимит.",
|
||||
"post_title_too_long": "Длина названия записи превышает допустимый лимит.",
|
||||
"time": "Время",
|
||||
"action": "Действие",
|
||||
"view_source": "исходный код сообщения",
|
||||
|
@ -232,7 +232,7 @@
|
|||
"banned": "забаненный",
|
||||
"password_change": "Смена пароля",
|
||||
"no_email_setup": "Этот сервер неправильно настроил электронную почту.",
|
||||
"matrix_user_id": "Матрица пользователя",
|
||||
"matrix_user_id": "Адрес в Matrix",
|
||||
"are_you_sure": "вы уверены?",
|
||||
"archive_link": "архивировать ссылку",
|
||||
"logged_in": "Вошли в систему.",
|
||||
|
@ -269,5 +269,18 @@
|
|||
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
|
||||
"no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
|
||||
"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