mirror of https://github.com/kitsune-soc/kitsune
Basic UI functionality (#365)
* begin implementing home timeline * up * yarn up * flake.lock: Update Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/f5892ddac112a1e9b3612c39af1b72987ee5783a' (2023-09-29) → 'github:nixos/nixpkgs/81e8f48ebdecf07aab321182011b067aafc78896' (2023-10-03) • Updated input 'rust-overlay': 'github:oxalica/rust-overlay/611ccdceed92b4d94ae75328148d84ee4a5b462d' (2023-10-03) → 'github:oxalica/rust-overlay/126829788e99c188be4eeb805f144d73d8a00f2c' (2023-10-07) * begin implementing post component * virtual scrolling * infinite scrolling * cleanup * nice effects, post progress * add attachment rendering * add post page * add changing titles * some i18n, new post modal * add move to thread view * add character limit calculation, fix bundle size * up * progress * flake.lock: Update Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/81e8f48ebdecf07aab321182011b067aafc78896' (2023-10-03) → 'github:nixos/nixpkgs/f99e5f03cc0aa231ab5950a15ed02afec45ed51a' (2023-10-09) • Updated input 'rust-overlay': 'github:oxalica/rust-overlay/126829788e99c188be4eeb805f144d73d8a00f2c' (2023-10-07) → 'github:oxalica/rust-overlay/aa7584f5bbf5947716ad8ec14eccc0334f0d28f0' (2023-10-12) * remove obsolete tokens * create glitchedelement wrapper that respects the reduced motion directive * remove vue logo * fix lints * remove templated html pages * remove conditional wrapper * remove unused css and templates * remove google fonts reference * update oauth forms * fix templates up * up * flake.lock: Update Flake lock file updates: • Updated input 'rust-overlay': 'github:oxalica/rust-overlay/dce60ca7fca201014868c08a612edb73a998310f' (2023-10-14) → 'github:oxalica/rust-overlay/e494404d36a41247987eeb1bfc2f1ca903e97764' (2023-10-15) * add local timeline, add global timeline, add infinite scrolling * fix infinite scroll * fix up styling * fix modal style * up
This commit is contained in:
parent
3cd82fc03d
commit
cc1aa20f9e
|
@ -12,4 +12,4 @@
|
|||
"rust-analyzer.server.extraEnv": {
|
||||
"CARGO_TARGET_DIR": "target-analyzer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,9 +276,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-graphql"
|
||||
version = "6.0.7"
|
||||
version = "6.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1addb0b551c59640e15de99e7566a4e3a1186cf42269e160c485ba6d8b43fe30"
|
||||
checksum = "117113a7ff4a98f2a864fa7a5274033b0907fce65dc8464993c75033f8074f90"
|
||||
dependencies = [
|
||||
"async-graphql-derive",
|
||||
"async-graphql-parser",
|
||||
|
@ -312,9 +312,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-graphql-axum"
|
||||
version = "6.0.7"
|
||||
version = "6.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c21af134ab9419aae6658298f819a28e4737ac81f96cde8008f9d49db1802662"
|
||||
checksum = "0ddd7767f83c3273a6d26a36cbdd562c6009aae87c6fa2c0f1eebb76f60c4151"
|
||||
dependencies = [
|
||||
"async-graphql",
|
||||
"async-trait",
|
||||
|
@ -330,9 +330,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-graphql-derive"
|
||||
version = "6.0.7"
|
||||
version = "6.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e1121ff0be2feea705c24f6940162c4f14a077e50a217b16e091e6534a8c08a"
|
||||
checksum = "6e4bb7b7b2344d24af860776b7fe4e4ee4a67cd965f076048d023f555703b854"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"async-graphql-parser",
|
||||
|
@ -347,9 +347,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-graphql-parser"
|
||||
version = "6.0.7"
|
||||
version = "6.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b6713fd4ffd610b8b6f6e911bf31277cbb84b7c2a9cdeeb39d1b3eed3b88e4"
|
||||
checksum = "c47e1c1ff6cb7cae62c9cd768d76475cc68f156d8234b024fd2499ad0e91da21"
|
||||
dependencies = [
|
||||
"async-graphql-value",
|
||||
"pest",
|
||||
|
@ -359,9 +359,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-graphql-value"
|
||||
version = "6.0.7"
|
||||
version = "6.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d74240f9daa8c1e8f73e9cfcc338d20a88d00bbeb83ded49ce8e5b4dcec0f5"
|
||||
checksum = "2270df3a642efce860ed06fbcf61fc6db10f83c2ecb5613127fb453c82e012a4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"indexmap 2.0.2",
|
||||
|
@ -1187,9 +1187,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.9"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
||||
checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -1338,7 +1338,7 @@ dependencies = [
|
|||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"socket2 0.4.9",
|
||||
"socket2 0.4.10",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -1463,7 +1463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.1",
|
||||
"hashbrown 0.14.2",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
|
@ -1625,9 +1625,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "diesel_full_text_search"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3975708a44577929dd7d921e5a4c1f95eef7c6a22d3e236d7d92fa108b535fdd"
|
||||
checksum = "8b44a057e441a6a5d802feea2646a6730cc6df99c75b7e02857a41ea90f2b30b"
|
||||
dependencies = [
|
||||
"diesel",
|
||||
]
|
||||
|
@ -2305,9 +2305,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
|
||||
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
|
@ -2486,7 +2486,7 @@ dependencies = [
|
|||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.9",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
@ -2593,7 +2593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.1",
|
||||
"hashbrown 0.14.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -3035,7 +3035,7 @@ dependencies = [
|
|||
"pem",
|
||||
"pkcs8",
|
||||
"rayon",
|
||||
"ring 0.17.4",
|
||||
"ring 0.17.5",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
|
@ -3192,7 +3192,7 @@ dependencies = [
|
|||
"quoted_printable",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"socket2 0.5.4",
|
||||
"socket2 0.5.5",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
|
@ -4609,7 +4609,7 @@ dependencies = [
|
|||
"rustls-native-certs",
|
||||
"ryu",
|
||||
"sha1_smol",
|
||||
"socket2 0.4.9",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tokio-retry",
|
||||
"tokio-rustls",
|
||||
|
@ -4734,9 +4734,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.4"
|
||||
version = "0.17.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fce3045ffa7c981a6ee93f640b538952e155f1ae3a1a02b84547fc7a56b7059a"
|
||||
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"getrandom",
|
||||
|
@ -4860,9 +4860,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.19"
|
||||
version = "0.38.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed"
|
||||
checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
|
@ -5399,9 +5399,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.9"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
|
||||
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
|
@ -5409,9 +5409,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
|
@ -5638,18 +5638,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5736,7 +5736,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.4",
|
||||
"socket2 0.5.5",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
@ -5772,7 +5772,7 @@ dependencies = [
|
|||
"postgres-protocol",
|
||||
"postgres-types",
|
||||
"rand",
|
||||
"socket2 0.5.4",
|
||||
"socket2 0.5.5",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"whoami",
|
||||
|
@ -5955,9 +5955,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
|||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
|
@ -6066,16 +6066,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.17.0"
|
||||
source = "git+https://github.com/idanarye/rust-typed-builder.git?rev=fd1bd8336aeb0dc334060635e2a6a1d8dc329e3c#fd1bd8336aeb0dc334060635e2a6a1d8dc329e3c"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e47c0496149861b7c95198088cbf36645016b1a0734cf350c50e2a38e070f38a"
|
||||
dependencies = [
|
||||
"typed-builder-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder-macro"
|
||||
version = "0.17.0"
|
||||
source = "git+https://github.com/idanarye/rust-typed-builder.git?rev=fd1bd8336aeb0dc334060635e2a6a1d8dc329e3c#fd1bd8336aeb0dc334060635e2a6a1d8dc329e3c"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "982ee4197351b5c9782847ef5ec1fdcaf50503fb19d68f9771adae314e72b492"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -42,4 +42,3 @@ version = "0.0.1-pre.3"
|
|||
|
||||
[patch.crates-io]
|
||||
redis = { git = "https://github.com/aumetra/redis-rs.git", rev = "3c4ee09d432a69e1d87d66dcba14c519467c9b81" }
|
||||
typed-builder = { git = "https://github.com/idanarye/rust-typed-builder.git", rev = "fd1bd8336aeb0dc334060635e2a6a1d8dc329e3c" }
|
||||
|
|
|
@ -11,9 +11,9 @@ moka = { version = "0.12.1", features = ["sync"] }
|
|||
redis = "0.23.3"
|
||||
serde = "1.0.189"
|
||||
simd-json = "0.12.0"
|
||||
thiserror = "1.0.49"
|
||||
tracing = "0.1.39"
|
||||
typed-builder = "0.17.0"
|
||||
thiserror = "1.0.50"
|
||||
tracing = "0.1.40"
|
||||
typed-builder = "0.18.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.33.0", features = ["macros", "rt"] }
|
||||
|
|
|
@ -12,5 +12,5 @@ serde = { version = "1.0.189", features = ["derive"] }
|
|||
serde_urlencoded = "0.7.1"
|
||||
simd-json = "0.12.0"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
thiserror = "1.0.49"
|
||||
typed-builder = "0.17.0"
|
||||
thiserror = "1.0.50"
|
||||
typed-builder = "0.18.0"
|
||||
|
|
|
@ -62,12 +62,12 @@ sha2 = { version = "0.10.8", features = ["asm"] }
|
|||
simd-json = "0.12.0"
|
||||
smol_str = "0.2.0"
|
||||
speedy-uuid = { path = "../../lib/speedy-uuid", features = ["diesel"] }
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
time = "0.3.30"
|
||||
tokio = { version = "1.33.0", features = ["macros", "rt"] }
|
||||
toml = { version = "0.8.2", default-features = false, features = ["parse"] }
|
||||
tracing = "0.1.39"
|
||||
typed-builder = "0.17.0"
|
||||
tracing = "0.1.40"
|
||||
typed-builder = "0.18.0"
|
||||
url = "2.4.1"
|
||||
zxcvbn = { version = "2.2.2", default-features = false }
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ diesel-async = { version = "0.4.1", features = [
|
|||
"postgres",
|
||||
"tokio",
|
||||
] }
|
||||
diesel_full_text_search = { version = "2.1.0", default-features = false }
|
||||
diesel_full_text_search = { version = "2.1.1", default-features = false }
|
||||
diesel_migrations = "2.1.0"
|
||||
iso8601-timestamp = { version = "0.2.12", features = ["diesel-pg"] }
|
||||
kitsune-language = { path = "../kitsune-language" }
|
||||
|
@ -22,7 +22,7 @@ num-traits = "0.2.17"
|
|||
serde = { version = "1.0.189", features = ["derive"] }
|
||||
simd-json = "0.12.0"
|
||||
speedy-uuid = { path = "../../lib/speedy-uuid", features = ["diesel"] }
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
tokio = { version = "1.33.0", features = ["rt"] }
|
||||
tracing-log = "0.1.3"
|
||||
typed-builder = "0.17.0"
|
||||
typed-builder = "0.18.0"
|
||||
|
|
|
@ -23,5 +23,5 @@ mrml = { version = "2.0.0-rc4", default-features = false, features = [
|
|||
"parse",
|
||||
"render",
|
||||
] }
|
||||
thiserror = "1.0.49"
|
||||
typed-builder = "0.17.0"
|
||||
thiserror = "1.0.50"
|
||||
typed-builder = "0.18.0"
|
||||
|
|
|
@ -14,5 +14,5 @@ kitsune-http-client = { path = "../kitsune-http-client" }
|
|||
once_cell = "1.18.0"
|
||||
scraper = { version = "0.17.1", default-features = false }
|
||||
smol_str = "0.2.0"
|
||||
thiserror = "1.0.49"
|
||||
typed-builder = "0.17.0"
|
||||
thiserror = "1.0.50"
|
||||
typed-builder = "0.18.0"
|
||||
|
|
|
@ -8,14 +8,14 @@ base64-simd = "0.8.0"
|
|||
derive_builder = "0.12.0"
|
||||
http = "0.2.9"
|
||||
rayon = "1.8.0"
|
||||
ring = { version = "0.17.4", features = ["std"] }
|
||||
ring = { version = "0.17.5", features = ["std"] }
|
||||
time = { version = "0.3.30", default-features = false, features = [
|
||||
"formatting",
|
||||
"parsing",
|
||||
] }
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
tokio = { version = "1.33.0", features = ["sync"] }
|
||||
typed-builder = "0.17.0"
|
||||
typed-builder = "0.18.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pem = "3.0.2"
|
||||
|
|
|
@ -18,4 +18,4 @@ serde = "1.0.189"
|
|||
simd-json = "0.12.0"
|
||||
tokio = { version = "1.33.0", features = ["macros", "rt", "sync"] }
|
||||
tokio-stream = { version = "0.1.14", features = ["sync"] }
|
||||
tracing = "0.1.39"
|
||||
tracing = "0.1.40"
|
||||
|
|
|
@ -7,7 +7,7 @@ version.workspace = true
|
|||
async-trait = "0.1.74"
|
||||
diesel = "2.1.3"
|
||||
diesel-async = "0.4.1"
|
||||
diesel_full_text_search = { version = "2.1.0", default-features = false }
|
||||
diesel_full_text_search = { version = "2.1.1", default-features = false }
|
||||
enum_dispatch = "0.3.12"
|
||||
futures-util = "0.3.28"
|
||||
kitsune-db = { path = "../kitsune-db" }
|
||||
|
@ -15,8 +15,8 @@ kitsune-language = { path = "../kitsune-language" }
|
|||
serde = { version = "1.0.189", features = ["derive"] }
|
||||
speedy-uuid = { path = "../../lib/speedy-uuid" }
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
thiserror = "1.0.49"
|
||||
tracing = "0.1.39"
|
||||
thiserror = "1.0.50"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# "meilisearch" feature
|
||||
meilisearch-sdk = { version = "0.24.2", optional = true }
|
||||
|
|
12
flake.lock
12
flake.lock
|
@ -140,11 +140,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1697059129,
|
||||
"narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=",
|
||||
"lastModified": 1697723726,
|
||||
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593",
|
||||
"rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -232,11 +232,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1697249410,
|
||||
"narHash": "sha256-OmsnxNsjBB1DJlUuJyzDJJ7psbm4/VzokNT+o0ajzFQ=",
|
||||
"lastModified": 1697854201,
|
||||
"narHash": "sha256-H+0Wb20PQx/8N7X/OfwwAVPeN9TbfjcyG0sXbdgsh50=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "dce60ca7fca201014868c08a612edb73a998310f",
|
||||
"rev": "6e8e3332433847cd56186b1f6fc8c47603cf5b46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
"importOrder": ["^\\w", "^[./|~/]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderParserPlugins": ["typescript"],
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"plugins": [
|
||||
"@trivago/prettier-plugin-sort-imports",
|
||||
"prettier-plugin-css-order"
|
||||
],
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"vueIndentScriptAndStyle": true
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<!--<link rel="icon" type="image/svg+xml" href="/vite.svg" />-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kitsune</title>
|
||||
</head>
|
||||
|
|
|
@ -19,17 +19,25 @@
|
|||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-3",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@tiptap/vue-3": "^2.1.12",
|
||||
"@urql/exchange-graphcache": "^6.3.3",
|
||||
"@urql/vue": "^1.1.2",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
"floating-vue": "^2.0.0-beta.24",
|
||||
"graphql": "^16.8.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.2.41",
|
||||
"tiptap-markdown": "^0.8.2",
|
||||
"unhead": "^1.7.4",
|
||||
"vue": "^3.3.6",
|
||||
"vue-i18n": "^9.5.0",
|
||||
"vue-powerglitch": "^1.0.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8"
|
||||
},
|
||||
|
@ -38,20 +46,21 @@
|
|||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@parcel/watcher": "^2.3.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.68.0",
|
||||
"prettier-plugin-css-order": "^2.0.1",
|
||||
"sass": "^1.69.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.10",
|
||||
"vue-tsc": "^1.8.15"
|
||||
"vite": "^4.5.0",
|
||||
"vue-tsc": "^1.8.19"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,9 +1,11 @@
|
|||
<template>
|
||||
<NavBar v-if="authStore.isAuthenticated()" />
|
||||
<router-view></router-view>
|
||||
<GenericFooter />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GenericFooter from './components/GenericFooter.vue';
|
||||
import NavBar from './components/NavBar.vue';
|
||||
import { useAuthStore } from './store/auth';
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
Before Width: | Height: | Size: 496 B |
|
@ -52,7 +52,7 @@
|
|||
/>
|
||||
</FormKit>
|
||||
|
||||
<BaseModal :closed="!modalData.show" :title="modalData.title">
|
||||
<BaseModal v-model="modalData.show" :title="modalData.title">
|
||||
<!-- This is returned from the backend and created from an error type, and only "enhanced" with HTML newlines by us -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="modalData.content" />
|
||||
|
@ -67,13 +67,16 @@
|
|||
<script setup lang="ts">
|
||||
import { useMutation } from '@urql/vue';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { defineAsyncComponent, reactive } from 'vue';
|
||||
|
||||
import { useInstanceInfo } from '../graphql/instance-info';
|
||||
import { graphql } from '../graphql/types';
|
||||
import { authorizationUrl } from '../lib/oauth2';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
import CaptchaComponent from './CaptchaComponent.vue';
|
||||
import BaseModal from './modal/BaseModal.vue';
|
||||
|
||||
const CaptchaComponent = defineAsyncComponent(
|
||||
() => import('./CaptchaComponent.vue'),
|
||||
);
|
||||
|
||||
const modalData = reactive({
|
||||
show: false,
|
||||
|
@ -158,15 +161,15 @@
|
|||
}
|
||||
|
||||
.formkit-form {
|
||||
background-color: $dark2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
padding: 3vh;
|
||||
border-radius: 5px;
|
||||
border: 0.2px solid $shade1dark;
|
||||
border-radius: 5px;
|
||||
background-color: $dark2;
|
||||
padding: 3vh;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.formkit-wrapper {
|
||||
|
@ -174,14 +177,14 @@
|
|||
}
|
||||
|
||||
.formkit-input[type='submit'] {
|
||||
border: 0;
|
||||
background-color: $shade1dark;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background-color: $shade1dark;
|
||||
padding: 10px;
|
||||
width: 100px;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: $shade2dark;
|
||||
|
@ -189,13 +192,13 @@
|
|||
}
|
||||
|
||||
.formkit-input:not([type='submit']) {
|
||||
width: 100%;
|
||||
border: 0.5px solid $shade1dark;
|
||||
background-color: $dark1;
|
||||
border-radius: 2px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
background-color: $dark1;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.forms {
|
||||
|
@ -203,9 +206,9 @@
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
width: 40%;
|
||||
padding: 1vw;
|
||||
gap: 20px;
|
||||
padding: 1vw;
|
||||
width: 40%;
|
||||
|
||||
@media only screen and (max-width: 1367px) {
|
||||
align-items: center;
|
||||
|
@ -218,13 +221,13 @@
|
|||
}
|
||||
|
||||
.formkit-messages {
|
||||
padding-left: 0;
|
||||
color: red;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.formkit-label {
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<div v-if="!closed" class="modal">
|
||||
<fieldset class="modal-content">
|
||||
<legend>
|
||||
{{ title }}
|
||||
</legend>
|
||||
<slot />
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
closed: boolean;
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../styles/colours' as *;
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
&-content {
|
||||
background-color: $dark1;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<fieldset ref="scroller" class="timeline">
|
||||
<legend class="timeline-legend">
|
||||
{{ $t('messages.timeline.title') }}
|
||||
</legend>
|
||||
<DynamicScroller class="scroller" :items="posts" :min-item-size="50">
|
||||
<template
|
||||
#default="{
|
||||
item,
|
||||
index,
|
||||
active,
|
||||
}: {
|
||||
item: PostType;
|
||||
index: number;
|
||||
active: boolean;
|
||||
}"
|
||||
>
|
||||
<DynamicScrollerItem
|
||||
class="post-container"
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[item.subject, item.content, item.attachments]"
|
||||
:data-index="index"
|
||||
>
|
||||
<Post
|
||||
:id="item.id"
|
||||
:account="item.account"
|
||||
:subject="item.subject"
|
||||
:content="item.content"
|
||||
:attachments="item.attachments"
|
||||
/>
|
||||
<!-- Load bearing little div -->
|
||||
<!-- Without this div, the height computation is all messed up and the margin of the post gets ignored -->
|
||||
<div style="height: 1px"></div>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInfiniteScroll } from '@vueuse/core';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
||||
|
||||
import Post, { Post as PostType } from './Post.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
posts: PostType[];
|
||||
loadMore: () => Promise<void>;
|
||||
}>();
|
||||
|
||||
const scroller = ref<HTMLElement>();
|
||||
useInfiniteScroll(scroller, async () => {
|
||||
if (props.posts.length !== 0) {
|
||||
await props.loadMore();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline {
|
||||
margin: auto;
|
||||
border-color: grey;
|
||||
max-width: 100ch;
|
||||
|
||||
max-height: 82vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
&-legend {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.post-container * {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
/>
|
||||
|
||||
<vue-hcaptcha
|
||||
v-if="backend == CaptchaBackend.HCaptcha"
|
||||
v-if="backend === CaptchaBackend.HCaptcha"
|
||||
:sitekey="sitekey"
|
||||
@verify="onVerify"
|
||||
@expired="onExpire"
|
||||
|
@ -58,7 +58,7 @@
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.backend == CaptchaBackend.MCaptcha) {
|
||||
if (props.backend === CaptchaBackend.MCaptcha) {
|
||||
const config = {
|
||||
widgetLink: new URL(props.sitekey),
|
||||
};
|
||||
|
@ -77,7 +77,7 @@
|
|||
@use '../styles/colours' as *;
|
||||
|
||||
#mcaptcha__widget-container {
|
||||
height: 80px;
|
||||
border: 1px solid $shade1dark;
|
||||
height: 80px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<footer>
|
||||
<div class="footer">
|
||||
<LanguageSelector />
|
||||
|
||||
<span>Kitsune v{{ instanceData?.version }}</span>
|
||||
|
||||
<a target="_blank" href="https://github.com/kitsune-soc/kitsune">
|
||||
Source code
|
||||
</a>
|
||||
|
@ -11,6 +14,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useInstanceInfo } from '../graphql/instance-info';
|
||||
import LanguageSelector from './LanguageSelector.vue';
|
||||
|
||||
const instanceData = useInstanceInfo();
|
||||
</script>
|
||||
|
@ -22,8 +26,8 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding: 10px 0;
|
||||
gap: 25px;
|
||||
margin: 25px 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<slot v-if="reduceMotion" />
|
||||
|
||||
<GlitchedElement v-else :options="options">
|
||||
<slot />
|
||||
</GlitchedElement>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePreferredReducedMotion } from '@vueuse/core';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { GlitchedElement } from 'vue-powerglitch';
|
||||
|
||||
defineProps<{ options?: object }>();
|
||||
|
||||
const preferredReducedMotion = usePreferredReducedMotion();
|
||||
const reduceMotion = computed(
|
||||
() => preferredReducedMotion.value === 'reduce',
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<select v-model="$i18n.locale">
|
||||
<option v-for="(lang, i) in $i18n.availableLocales" :key="i" :value="lang">
|
||||
{{ lang }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
|
@ -5,22 +5,29 @@
|
|||
<NavBarLink :to="route" :icon="details.icon" :detail="details.detail" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="nav-bar-profile">
|
||||
<div class="nav-bar-element profile-menu-button">
|
||||
<!-- Without this weird double quote stuff Vite would have tried to do some fucked up shit -->
|
||||
<img :src="'/public/assets/default-avatar.png'" />
|
||||
<img :src="DEFAULT_PROFILE_PICTURE_URL" />
|
||||
</div>
|
||||
|
||||
<div class="nav-bar-element">
|
||||
<font-awesome-icon
|
||||
class="icon create-status"
|
||||
icon="fa-pen-to-square fa-solid"
|
||||
@click="showPostModal = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<NewPostModal v-model="showPostModal" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
|
||||
import { DEFAULT_PROFILE_PICTURE_URL } from '../consts';
|
||||
import NavBarLink from './NavBarLink.vue';
|
||||
|
||||
type RouteInfo = {
|
||||
|
@ -50,6 +57,11 @@
|
|||
detail: 'Federated',
|
||||
},
|
||||
};
|
||||
|
||||
const NewPostModal = defineAsyncComponent(
|
||||
() => import('./modal/NewPostModal.vue'),
|
||||
);
|
||||
const showPostModal = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -57,17 +69,18 @@
|
|||
@use '../styles/mixins' as *;
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
margin-bottom: 100px;
|
||||
background-color: $dark2;
|
||||
padding: 0 25px;
|
||||
padding-top: 5px;
|
||||
margin-bottom: 100px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include only-on-mobile {
|
||||
padding: 0;
|
||||
|
@ -87,13 +100,14 @@
|
|||
gap: 10px;
|
||||
|
||||
.create-status {
|
||||
cursor: pointer;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.profile-menu-button {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
height: 30px;
|
||||
|
|
|
@ -4,12 +4,16 @@
|
|||
:to="to"
|
||||
draggable="false"
|
||||
>
|
||||
<font-awesome-icon class="icon" :icon="icon" />
|
||||
<span v-if="detail" class="detail">{{ detail }}</span>
|
||||
<GlitchedElement :options="{ playMode: 'hover' }">
|
||||
<font-awesome-icon class="icon" :icon="icon" />
|
||||
<span v-if="detail" class="detail">{{ detail }}</span>
|
||||
</GlitchedElement>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import GlitchedElement from './GlitchedElement.vue';
|
||||
|
||||
defineProps<{
|
||||
class?: string;
|
||||
to: string;
|
||||
|
@ -24,14 +28,14 @@
|
|||
|
||||
.nav-bar-link {
|
||||
display: inline-block;
|
||||
padding: 15px;
|
||||
color: $shade1dark;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
border-bottom: 4px solid;
|
||||
border-color: transparent;
|
||||
padding: 15px;
|
||||
color: $shade1dark;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
|
@ -52,9 +56,9 @@
|
|||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: $shade2light;
|
||||
border-image-slice: 1;
|
||||
border-image-source: linear-gradient(to left, $shade2light, $shade2dark);
|
||||
border-image-slice: 1;
|
||||
color: $shade2light;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<article class="post" @click="goToThreadView">
|
||||
<a class="account-info" :href="account.url">
|
||||
<img
|
||||
class="account-info-profile-picture"
|
||||
:src="profilePictureUrl"
|
||||
:alt="`${account.username}'s profile picture`"
|
||||
/>
|
||||
|
||||
<div class="account-info-names">
|
||||
<strong class="account-info-names-displayname">
|
||||
{{ account.displayName ?? account.username }}
|
||||
</strong>
|
||||
<span class="account-info-names-username">
|
||||
@{{ account.username }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<p v-if="subject">
|
||||
<strong>
|
||||
<!-- Cleaned on the backend -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="subject" />
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<!-- Cleaned on the backend -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="post-content" v-html="content" />
|
||||
|
||||
<div class="post-attachments">
|
||||
<div
|
||||
v-for="attachment in attachments"
|
||||
:key="attachment.url"
|
||||
:title="attachment.description!"
|
||||
>
|
||||
<audio
|
||||
v-if="attachment.contentType.startsWith('audio')"
|
||||
:src="attachment.url"
|
||||
controls
|
||||
/>
|
||||
|
||||
<video
|
||||
v-else-if="attachment.contentType.startsWith('video')"
|
||||
:src="attachment.url"
|
||||
controls
|
||||
/>
|
||||
|
||||
<img v-else :src="attachment.url" :alt="attachment.description!" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DEFAULT_PROFILE_PICTURE_URL } from '../consts';
|
||||
|
||||
export type PostAccount = {
|
||||
displayName?: string | null;
|
||||
username: string;
|
||||
avatar?: {
|
||||
url: string;
|
||||
} | null;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type PostAttachment = {
|
||||
contentType: string;
|
||||
description?: string | null;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
id: string;
|
||||
subject?: string | null;
|
||||
content: string;
|
||||
account: PostAccount;
|
||||
attachments: PostAttachment[];
|
||||
};
|
||||
|
||||
const props = defineProps<Post>();
|
||||
const profilePictureUrl = computed(
|
||||
() => props.account.avatar?.url ?? DEFAULT_PROFILE_PICTURE_URL,
|
||||
);
|
||||
const router = useRouter();
|
||||
|
||||
function goToThreadView() {
|
||||
router.push(`/posts/${props.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../styles/colours' as *;
|
||||
|
||||
.post {
|
||||
border: 1px solid white;
|
||||
border-radius: 3px;
|
||||
|
||||
background-color: $dark2;
|
||||
padding: 1em;
|
||||
|
||||
& .account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
width: fit-content;
|
||||
line-height: 100%;
|
||||
|
||||
&-profile-picture {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
&-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-displayname {
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&-attachments {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.25em;
|
||||
|
||||
& * {
|
||||
width: 100%;
|
||||
max-height: 30ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div v-if="modelValue" class="modal">
|
||||
<fieldset ref="modalContent" class="modal-content">
|
||||
<legend class="modal-title">
|
||||
{{ title }}
|
||||
</legend>
|
||||
<slot />
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', modelValue: boolean): void;
|
||||
}>();
|
||||
|
||||
const modalContent = ref();
|
||||
onClickOutside(modalContent, () => {
|
||||
emit('update:modelValue', false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/colours' as *;
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
z-index: 999;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
&-title {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: $dark1;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<BaseModal
|
||||
:model-value="modelValue"
|
||||
:title="$t('messages.newPost.title')"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<BubbleMenu v-if="editor" :editor="editor" :tippy-options="tippyOptions">
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
</BubbleMenu>
|
||||
|
||||
<FloatingMenu v-if="editor" :editor="editor" :tippy-options="tippyOptions">
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
>
|
||||
Toggle list
|
||||
</button>
|
||||
</FloatingMenu>
|
||||
|
||||
<EditorContent :editor="editor" />
|
||||
|
||||
<div class="post-modal-controls">
|
||||
<div class="post-modal-controls-modifiers">lmao</div>
|
||||
|
||||
<div class="post-modal-controls-post">
|
||||
{{ remainingCharacters }}
|
||||
<button class="post-modal-controls-post-button">Post!</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import {
|
||||
useEditor,
|
||||
BubbleMenu,
|
||||
EditorContent,
|
||||
FloatingMenu,
|
||||
} from '@tiptap/vue-3';
|
||||
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
import { useInstanceInfo } from '../../graphql/instance-info';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
|
||||
defineEmits<{
|
||||
(event: 'update:modelValue', modelValue: boolean): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [Markdown, StarterKit],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'post-modal-editor',
|
||||
},
|
||||
},
|
||||
});
|
||||
const tippyOptions = reactive({ duration: 200 });
|
||||
|
||||
const instanceData = useInstanceInfo();
|
||||
const remainingCharacters = computed(() => {
|
||||
if (instanceData.value) {
|
||||
const markdownText = editor.value?.storage.markdown.getMarkdown();
|
||||
return instanceData.value.characterLimit - markdownText.length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.post-modal {
|
||||
&-editor {
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid white;
|
||||
|
||||
padding: 0 1em;
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
height: fit-content;
|
||||
min-height: 250px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,2 +1,22 @@
|
|||
export const BACKEND_PREFIX =
|
||||
const BACKEND_PREFIX =
|
||||
import.meta.env.VITE_BACKEND_PREFIX ?? window.location.origin;
|
||||
|
||||
const DEFAULT_PROFILE_PICTURE_URL =
|
||||
BACKEND_PREFIX + '/public/assets/default-avatar.png';
|
||||
|
||||
const MAX_UUID = 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF';
|
||||
|
||||
const TEMPLATE_PARAMS = {
|
||||
separator: '-',
|
||||
siteName: 'Kitsune',
|
||||
};
|
||||
|
||||
const TITLE_TEMPLATE = '%s %separator %siteName';
|
||||
|
||||
export {
|
||||
BACKEND_PREFIX,
|
||||
DEFAULT_PROFILE_PICTURE_URL,
|
||||
MAX_UUID,
|
||||
TEMPLATE_PARAMS,
|
||||
TITLE_TEMPLATE,
|
||||
};
|
||||
|
|
|
@ -9,6 +9,11 @@ function useInstanceInfo() {
|
|||
query: graphql(`
|
||||
query getInstanceInfo {
|
||||
instance {
|
||||
captcha {
|
||||
backend
|
||||
key
|
||||
}
|
||||
characterLimit
|
||||
description
|
||||
domain
|
||||
localPostCount
|
||||
|
@ -16,10 +21,6 @@ function useInstanceInfo() {
|
|||
name
|
||||
userCount
|
||||
version
|
||||
captcha {
|
||||
backend
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { useQuery } from '@urql/vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { graphql } from './types';
|
||||
|
||||
function getPostById(id: string) {
|
||||
const { data } = useQuery({
|
||||
query: graphql(`
|
||||
query getPostById($id: UUID!) {
|
||||
getPostById(id: $id) {
|
||||
id
|
||||
subject
|
||||
content
|
||||
account {
|
||||
id
|
||||
displayName
|
||||
username
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
url
|
||||
}
|
||||
attachments {
|
||||
contentType
|
||||
description
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return computed(() => data.value?.getPostById);
|
||||
}
|
||||
|
||||
export { getPostById };
|
|
@ -0,0 +1,89 @@
|
|||
import { useQuery } from '@urql/vue';
|
||||
|
||||
import { Ref } from 'vue';
|
||||
|
||||
import { graphql } from './types';
|
||||
|
||||
function getHome(after: Ref<string>) {
|
||||
const { data } = useQuery({
|
||||
query: graphql(`
|
||||
query getHomeTimeline($after: String!) {
|
||||
homeTimeline(after: $after) @_relayPagination(mergeMode: "after") {
|
||||
nodes {
|
||||
id
|
||||
subject
|
||||
content
|
||||
url
|
||||
account {
|
||||
id
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
displayName
|
||||
username
|
||||
url
|
||||
}
|
||||
attachments {
|
||||
contentType
|
||||
description
|
||||
url
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
variables: {
|
||||
after: after as unknown as string, // Weird cast to allow reactivity
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getPublic(after: Ref<string>, onlyLocal: boolean) {
|
||||
const { data } = useQuery({
|
||||
query: graphql(`
|
||||
query getPublicTimeline($after: String!, $onlyLocal: Boolean!) {
|
||||
publicTimeline(after: $after, onlyLocal: $onlyLocal)
|
||||
@_relayPagination(mergeMode: "after") {
|
||||
nodes {
|
||||
id
|
||||
subject
|
||||
content
|
||||
url
|
||||
account {
|
||||
id
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
displayName
|
||||
username
|
||||
url
|
||||
}
|
||||
attachments {
|
||||
contentType
|
||||
description
|
||||
url
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
variables: {
|
||||
after: after as unknown as string,
|
||||
onlyLocal,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export { getHome, getPublic };
|
|
@ -16,8 +16,14 @@ import * as types from './graphql';
|
|||
const documents = {
|
||||
'\n mutation registerUser(\n $username: String!\n $email: String!\n $password: String!\n $captchaToken: String\n ) {\n registerUser(\n username: $username\n email: $email\n password: $password\n captchaToken: $captchaToken\n ) {\n id\n }\n }\n ':
|
||||
types.RegisterUserDocument,
|
||||
'\n query getInstanceInfo {\n instance {\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n captcha {\n backend\n key\n }\n }\n }\n ':
|
||||
'\n query getInstanceInfo {\n instance {\n captcha {\n backend\n key\n }\n characterLimit\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n }\n }\n ':
|
||||
types.GetInstanceInfoDocument,
|
||||
'\n query getPostById($id: UUID!) {\n getPostById(id: $id) {\n id\n subject\n content\n account {\n id\n displayName\n username\n avatar {\n url\n }\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n }\n ':
|
||||
types.GetPostByIdDocument,
|
||||
'\n query getHomeTimeline($after: String!) {\n homeTimeline(after: $after) @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n ':
|
||||
types.GetHomeTimelineDocument,
|
||||
'\n query getPublicTimeline($after: String!, $onlyLocal: Boolean!) {\n publicTimeline(after: $after, onlyLocal: $onlyLocal)\n @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n ':
|
||||
types.GetPublicTimelineDocument,
|
||||
'\n mutation registerOauthApplication(\n $name: String!\n $redirect_uri: String!\n ) {\n registerOauthApplication(name: $name, redirectUri: $redirect_uri) {\n id\n secret\n redirectUri\n }\n }\n ':
|
||||
types.RegisterOauthApplicationDocument,
|
||||
};
|
||||
|
@ -46,8 +52,26 @@ export function graphql(
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: '\n query getInstanceInfo {\n instance {\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n captcha {\n backend\n key\n }\n }\n }\n ',
|
||||
): (typeof documents)['\n query getInstanceInfo {\n instance {\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n captcha {\n backend\n key\n }\n }\n }\n '];
|
||||
source: '\n query getInstanceInfo {\n instance {\n captcha {\n backend\n key\n }\n characterLimit\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n }\n }\n ',
|
||||
): (typeof documents)['\n query getInstanceInfo {\n instance {\n captcha {\n backend\n key\n }\n characterLimit\n description\n domain\n localPostCount\n registrationsOpen\n name\n userCount\n version\n }\n }\n '];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: '\n query getPostById($id: UUID!) {\n getPostById(id: $id) {\n id\n subject\n content\n account {\n id\n displayName\n username\n avatar {\n url\n }\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n }\n ',
|
||||
): (typeof documents)['\n query getPostById($id: UUID!) {\n getPostById(id: $id) {\n id\n subject\n content\n account {\n id\n displayName\n username\n avatar {\n url\n }\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n }\n '];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: '\n query getHomeTimeline($after: String!) {\n homeTimeline(after: $after) @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n ',
|
||||
): (typeof documents)['\n query getHomeTimeline($after: String!) {\n homeTimeline(after: $after) @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n '];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: '\n query getPublicTimeline($after: String!, $onlyLocal: Boolean!) {\n publicTimeline(after: $after, onlyLocal: $onlyLocal)\n @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n ',
|
||||
): (typeof documents)['\n query getPublicTimeline($after: String!, $onlyLocal: Boolean!) {\n publicTimeline(after: $after, onlyLocal: $onlyLocal)\n @_relayPagination(mergeMode: "after") {\n nodes {\n id\n subject\n content\n url\n account {\n id\n avatar {\n url\n }\n displayName\n username\n url\n }\n attachments {\n contentType\n description\n url\n }\n }\n pageInfo {\n startCursor\n endCursor\n }\n }\n }\n '];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
|
@ -87,6 +87,7 @@ export type CaptchaInfo = {
|
|||
export type Instance = {
|
||||
__typename?: 'Instance';
|
||||
captcha?: Maybe<CaptchaInfo>;
|
||||
characterLimit: Scalars['Int']['output'];
|
||||
description: Scalars['String']['output'];
|
||||
domain: Scalars['String']['output'];
|
||||
localPostCount: Scalars['Int']['output'];
|
||||
|
@ -270,6 +271,7 @@ export type GetInstanceInfoQuery = {
|
|||
__typename?: 'RootQuery';
|
||||
instance: {
|
||||
__typename?: 'Instance';
|
||||
characterLimit: number;
|
||||
description: string;
|
||||
domain: string;
|
||||
localPostCount: number;
|
||||
|
@ -285,6 +287,109 @@ export type GetInstanceInfoQuery = {
|
|||
};
|
||||
};
|
||||
|
||||
export type GetPostByIdQueryVariables = Exact<{
|
||||
id: Scalars['UUID']['input'];
|
||||
}>;
|
||||
|
||||
export type GetPostByIdQuery = {
|
||||
__typename?: 'RootQuery';
|
||||
getPostById: {
|
||||
__typename?: 'Post';
|
||||
id: any;
|
||||
subject?: string | null;
|
||||
content: string;
|
||||
account: {
|
||||
__typename?: 'Account';
|
||||
id: any;
|
||||
displayName?: string | null;
|
||||
username: string;
|
||||
url: string;
|
||||
avatar?: { __typename?: 'MediaAttachment'; url: string } | null;
|
||||
};
|
||||
attachments: Array<{
|
||||
__typename?: 'MediaAttachment';
|
||||
contentType: string;
|
||||
description?: string | null;
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetHomeTimelineQueryVariables = Exact<{
|
||||
after: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type GetHomeTimelineQuery = {
|
||||
__typename?: 'RootQuery';
|
||||
homeTimeline: {
|
||||
__typename?: 'PostConnection';
|
||||
nodes: Array<{
|
||||
__typename?: 'Post';
|
||||
id: any;
|
||||
subject?: string | null;
|
||||
content: string;
|
||||
url: string;
|
||||
account: {
|
||||
__typename?: 'Account';
|
||||
id: any;
|
||||
displayName?: string | null;
|
||||
username: string;
|
||||
url: string;
|
||||
avatar?: { __typename?: 'MediaAttachment'; url: string } | null;
|
||||
};
|
||||
attachments: Array<{
|
||||
__typename?: 'MediaAttachment';
|
||||
contentType: string;
|
||||
description?: string | null;
|
||||
url: string;
|
||||
}>;
|
||||
}>;
|
||||
pageInfo: {
|
||||
__typename?: 'PageInfo';
|
||||
startCursor?: string | null;
|
||||
endCursor?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GetPublicTimelineQueryVariables = Exact<{
|
||||
after: Scalars['String']['input'];
|
||||
onlyLocal: Scalars['Boolean']['input'];
|
||||
}>;
|
||||
|
||||
export type GetPublicTimelineQuery = {
|
||||
__typename?: 'RootQuery';
|
||||
publicTimeline: {
|
||||
__typename?: 'PostConnection';
|
||||
nodes: Array<{
|
||||
__typename?: 'Post';
|
||||
id: any;
|
||||
subject?: string | null;
|
||||
content: string;
|
||||
url: string;
|
||||
account: {
|
||||
__typename?: 'Account';
|
||||
id: any;
|
||||
displayName?: string | null;
|
||||
username: string;
|
||||
url: string;
|
||||
avatar?: { __typename?: 'MediaAttachment'; url: string } | null;
|
||||
};
|
||||
attachments: Array<{
|
||||
__typename?: 'MediaAttachment';
|
||||
contentType: string;
|
||||
description?: string | null;
|
||||
url: string;
|
||||
}>;
|
||||
}>;
|
||||
pageInfo: {
|
||||
__typename?: 'PageInfo';
|
||||
startCursor?: string | null;
|
||||
endCursor?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type RegisterOauthApplicationMutationVariables = Exact<{
|
||||
name: Scalars['String']['input'];
|
||||
redirect_uri: Scalars['String']['input'];
|
||||
|
@ -430,19 +535,6 @@ export const GetInstanceInfoDocument = {
|
|||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'description' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'domain' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'localPostCount' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'registrationsOpen' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'userCount' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'captcha' },
|
||||
|
@ -457,6 +549,23 @@ export const GetInstanceInfoDocument = {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'characterLimit' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'description' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'domain' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'localPostCount' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'registrationsOpen' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'userCount' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -468,6 +577,455 @@ export const GetInstanceInfoDocument = {
|
|||
GetInstanceInfoQuery,
|
||||
GetInstanceInfoQueryVariables
|
||||
>;
|
||||
export const GetPostByIdDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'getPostById' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'UUID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'getPostById' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'id' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'id' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'subject' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'content' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'account' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'displayName' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'username' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'avatar' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'attachments' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'contentType' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'description' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<GetPostByIdQuery, GetPostByIdQueryVariables>;
|
||||
export const GetHomeTimelineDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'getHomeTimeline' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'String' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'homeTimeline' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
},
|
||||
},
|
||||
],
|
||||
directives: [
|
||||
{
|
||||
kind: 'Directive',
|
||||
name: { kind: 'Name', value: '_relayPagination' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'mergeMode' },
|
||||
value: {
|
||||
kind: 'StringValue',
|
||||
value: 'after',
|
||||
block: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'nodes' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'subject' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'content' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'account' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'id' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'avatar' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'displayName' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'username' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'attachments' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'contentType' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'description' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'pageInfo' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'startCursor' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'endCursor' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
GetHomeTimelineQuery,
|
||||
GetHomeTimelineQueryVariables
|
||||
>;
|
||||
export const GetPublicTimelineDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'getPublicTimeline' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'String' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'onlyLocal' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'publicTimeline' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'after' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'onlyLocal' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'onlyLocal' },
|
||||
},
|
||||
},
|
||||
],
|
||||
directives: [
|
||||
{
|
||||
kind: 'Directive',
|
||||
name: { kind: 'Name', value: '_relayPagination' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'mergeMode' },
|
||||
value: {
|
||||
kind: 'StringValue',
|
||||
value: 'after',
|
||||
block: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'nodes' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'subject' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'content' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'account' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'id' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'avatar' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'displayName' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'username' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'attachments' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'contentType' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'description' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'url' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'pageInfo' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'startCursor' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'endCursor' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
GetPublicTimelineQuery,
|
||||
GetPublicTimelineQueryVariables
|
||||
>;
|
||||
export const RegisterOauthApplicationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export default {
|
||||
messages: {
|
||||
mainPage: {
|
||||
aboutInstance: "Deets 'bout this megacorp",
|
||||
},
|
||||
newPost: {
|
||||
title: 'New Transmission',
|
||||
},
|
||||
notFoundPage: {
|
||||
main: 'Seems like ya wandered behind the blackwall, choom',
|
||||
link: 'Back to the main page',
|
||||
},
|
||||
timeline: {
|
||||
title: 'Incoming Transmissions',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
instance: 'megacorp | megacorps',
|
||||
post: 'transmission | transmissions',
|
||||
user: 'choom | chooms',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
export default {
|
||||
messages: {
|
||||
mainPage: {
|
||||
aboutInstance: 'About this instance',
|
||||
},
|
||||
newPost: {
|
||||
title: 'New Post',
|
||||
},
|
||||
notFoundPage: {
|
||||
main: 'Whoops! It seems that you are lost',
|
||||
link: 'Back to the main page',
|
||||
},
|
||||
timeline: {
|
||||
title: 'Posts',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
title: 'statistics',
|
||||
instance: 'instance | instances',
|
||||
post: 'post | posts',
|
||||
user: 'user | users',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import { usePreferredLanguages } from '@vueuse/core';
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
import { createI18n, useI18n } from 'vue-i18n';
|
||||
|
||||
import en from './en';
|
||||
import enCyberpunk from './en-cyberpunk';
|
||||
|
||||
const messages = {
|
||||
en,
|
||||
'en-cyberpunk': enCyberpunk,
|
||||
};
|
||||
|
||||
const preferredLanguages = usePreferredLanguages();
|
||||
const preferredLanguage = computed(
|
||||
() => (preferredLanguages.value[0] ?? 'en').split('-')[0],
|
||||
);
|
||||
|
||||
watch(preferredLanguage, (newPreferredLanguage) => {
|
||||
const i18n = useI18n();
|
||||
if (newPreferredLanguage in i18n.availableLocales) {
|
||||
i18n.locale.value = newPreferredLanguage;
|
||||
}
|
||||
});
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: preferredLanguage.value,
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
});
|
||||
|
||||
export { i18n };
|
|
@ -7,15 +7,20 @@ import urql from '@urql/vue';
|
|||
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedState from 'pinia-plugin-persistedstate';
|
||||
import { createHead } from 'unhead';
|
||||
import { createApp } from 'vue';
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||
|
||||
import App from './App.vue';
|
||||
import { i18n } from './i18n';
|
||||
import './icons';
|
||||
import { router } from './router';
|
||||
import './styles/root.scss';
|
||||
import { urqlClient } from './urql';
|
||||
import { zxcvbnRule, zxcvbnValidationMessage } from './zxcvbn';
|
||||
|
||||
createHead(); // We need to initialize `unhead` somewhere near the entry point, so yeah
|
||||
|
||||
const pinia = createPinia().use(piniaPluginPersistedState);
|
||||
|
||||
createApp(App)
|
||||
|
@ -35,6 +40,7 @@ createApp(App)
|
|||
},
|
||||
}),
|
||||
)
|
||||
.use(i18n)
|
||||
.use(pinia)
|
||||
.use(router)
|
||||
.use(urql, urqlClient)
|
||||
|
|
|
@ -1,29 +1,77 @@
|
|||
import { useHead } from 'unhead';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import { TEMPLATE_PARAMS, TITLE_TEMPLATE } from './consts';
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => import('./views/MainPage.vue') },
|
||||
{ path: '/about', component: () => import('./views/AboutPage.vue') },
|
||||
{ path: '/messages', component: () => import('./views/MainPage.vue') },
|
||||
{ path: '/notifications', component: () => import('./views/MainPage.vue') },
|
||||
{
|
||||
path: '/about',
|
||||
component: () => import('./views/AboutPage.vue'),
|
||||
meta: { title: 'About' },
|
||||
},
|
||||
{
|
||||
path: '/messages',
|
||||
component: () => import('./views/MessagePage.vue'),
|
||||
meta: { title: 'Messages' },
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
component: () => import('./views/NotificationPage.vue'),
|
||||
meta: { title: 'Notifications' },
|
||||
},
|
||||
{
|
||||
path: '/timeline',
|
||||
children: [
|
||||
{ path: 'home', component: () => import('./views/MainPage.vue') },
|
||||
{ path: 'local', component: () => import('./views/MainPage.vue') },
|
||||
{ path: 'federated', component: () => import('./views/MainPage.vue') },
|
||||
{
|
||||
path: 'home',
|
||||
component: () => import('./views/timeline/HomePage.vue'),
|
||||
meta: { title: 'Home' },
|
||||
},
|
||||
{
|
||||
path: 'local',
|
||||
component: () => import('./views/timeline/LocalPage.vue'),
|
||||
meta: { title: 'Local' },
|
||||
},
|
||||
{
|
||||
path: 'federated',
|
||||
component: () => import('./views/timeline/FederatedPage.vue'),
|
||||
meta: { title: 'Federated' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/oauth-callback',
|
||||
component: () => import('./views/OAuthCallback.vue'),
|
||||
},
|
||||
{
|
||||
path: '/posts/:id',
|
||||
component: () => import('./views/PostPage.vue'),
|
||||
meta: { title: 'Thread view' },
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)',
|
||||
component: () => import('./views/NotFound.vue'),
|
||||
meta: { title: '404 Not found' },
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
const title = to.meta.title;
|
||||
if (title) {
|
||||
useHead({
|
||||
title,
|
||||
titleTemplate: TITLE_TEMPLATE,
|
||||
templateParams: TEMPLATE_PARAMS,
|
||||
});
|
||||
} else {
|
||||
useHead({ title: TEMPLATE_PARAMS.siteName, titleTemplate: '%s' });
|
||||
}
|
||||
});
|
||||
|
||||
export { router };
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// Source: Tailwind <https://tailwindcss.com/docs/screens>
|
||||
|
||||
$breakpoints: (
|
||||
sm: 640px,
|
||||
md: 768px,
|
||||
lg: 1024px,
|
||||
xl: 1280px,
|
||||
xxl: 1536px,
|
||||
);
|
|
@ -1,6 +1,6 @@
|
|||
// COPYRIGHT INFO
|
||||
|
||||
@font-face {
|
||||
font-family: 'BlockZone';
|
||||
src: url('/BlockZone.ttf') format('truetype');
|
||||
font-family: 'BlockZone';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
@use './breakpoints' as *;
|
||||
@use 'sass:map';
|
||||
|
||||
@mixin only-on-mobile {
|
||||
@media only screen and (max-width: 850px) {
|
||||
@media only screen and (max-width: map.get($breakpoints, md)) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
@use 'fonts' as *;
|
||||
|
||||
* {
|
||||
font-family: BlockZone;
|
||||
// font-family: BlockZone;
|
||||
box-sizing: inherit;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background-color: $dark1;
|
||||
color: white;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: white;
|
||||
background-color: $dark1;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
||||
font-synthesis: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@ -24,8 +25,8 @@
|
|||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: $shade2light;
|
||||
font-weight: 500;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import {
|
||||
Client,
|
||||
Exchange,
|
||||
cacheExchange,
|
||||
fetchExchange,
|
||||
mapExchange,
|
||||
} from '@urql/vue';
|
||||
Directive,
|
||||
cacheExchange as createCacheExchange,
|
||||
} from '@urql/exchange-graphcache';
|
||||
import { relayPagination } from '@urql/exchange-graphcache/extras';
|
||||
import { Client, Exchange, fetchExchange, mapExchange } from '@urql/vue';
|
||||
|
||||
import { merge } from 'lodash';
|
||||
|
||||
|
@ -30,6 +29,12 @@ const authExchange: Exchange = mapExchange({
|
|||
},
|
||||
});
|
||||
|
||||
const cacheExchange: Exchange = createCacheExchange({
|
||||
directives: {
|
||||
relayPagination: <Directive>relayPagination,
|
||||
},
|
||||
});
|
||||
|
||||
export const urqlClient = new Client({
|
||||
url: `${BACKEND_PREFIX}/graphql`,
|
||||
exchanges: [authExchange, cacheExchange, fetchExchange],
|
||||
|
|
|
@ -14,12 +14,10 @@
|
|||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="instanceData?.description" />
|
||||
</fieldset>
|
||||
<GenericFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GenericFooter from '../components/GenericFooter.vue';
|
||||
import { useInstanceInfo } from '../graphql/instance-info';
|
||||
|
||||
const instanceData = useInstanceInfo();
|
||||
|
@ -32,8 +30,8 @@
|
|||
}
|
||||
|
||||
.about-page {
|
||||
max-width: 80ch;
|
||||
margin: auto;
|
||||
max-width: 80ch;
|
||||
|
||||
& .title {
|
||||
text-align: center;
|
||||
|
|
|
@ -9,39 +9,56 @@
|
|||
</svg>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<div>
|
||||
<span class="stat-highlight">
|
||||
{{ instanceInfo?.name }}
|
||||
</span>
|
||||
is home to
|
||||
<span class="stat-highlight">
|
||||
{{ instanceInfo?.userCount }}
|
||||
</span>
|
||||
users who authored
|
||||
<span class="stat-highlight">
|
||||
{{ instanceInfo?.localPostCount }}
|
||||
</span>
|
||||
posts!
|
||||
</p>
|
||||
{{ $t('stats.title') }}:
|
||||
<ul>
|
||||
<li>
|
||||
<span class="stat-highlight">
|
||||
{{ instanceInfo?.userCount }}
|
||||
</span>
|
||||
{{ $tc('stats.user', instanceInfo?.userCount ?? 0) }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="stat-highlight">
|
||||
{{ instanceInfo?.localPostCount }}
|
||||
</span>
|
||||
{{ $tc('stats.post', instanceInfo?.localPostCount ?? 0) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<strong class="about-link">
|
||||
<router-link to="/about">About this instance</router-link>
|
||||
<router-link to="/about">
|
||||
{{ $t('messages.mainPage.aboutInstance') }}
|
||||
</router-link>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<AuthForms />
|
||||
</div>
|
||||
|
||||
<GenericFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AuthForms from '../components/AuthForms.vue';
|
||||
import GenericFooter from '../components/GenericFooter.vue';
|
||||
import { useInstanceInfo } from '../graphql/instance-info';
|
||||
import { onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import AuthForms from '../components/AuthForms.vue';
|
||||
import { useInstanceInfo } from '../graphql/instance-info';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const instanceInfo = useInstanceInfo();
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.isAuthenticated()) {
|
||||
const router = useRouter();
|
||||
router.replace('/timeline/home');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -51,16 +68,16 @@
|
|||
&-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
width: 95vw;
|
||||
margin: 0 auto;
|
||||
padding: 0 4vw;
|
||||
width: 95vw;
|
||||
height: 80vh;
|
||||
|
||||
@media only screen and (max-width: 1023px) {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
padding: 3vh 4vw;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,42 +85,43 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 1vh 2vw;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
padding: 1vh 2vw;
|
||||
|
||||
@media only screen and (max-width: 1367px) {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1023px) {
|
||||
width: 75%;
|
||||
margin-bottom: 4vh;
|
||||
width: 75%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .stat-highlight {
|
||||
display: inline;
|
||||
color: $shade1dark;
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
color: $shade2light;
|
||||
font-weight: bold;
|
||||
font-size: 42px;
|
||||
|
||||
&-logo {
|
||||
color: $shade2light;
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
color: $shade2light;
|
||||
}
|
||||
}
|
||||
|
||||
&-description,
|
||||
&-more {
|
||||
margin: 10px 0;
|
||||
width: fit-content;
|
||||
font-size: 18px;
|
||||
line-height: 143%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<template></template>
|
||||
|
||||
<script lang="ts" setup></script>
|
|
@ -1,91 +1,33 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- ADD BACKGROUND IMAGE AS A <img> ELEMENT -->
|
||||
<div class="main-container">
|
||||
<div class="main-intro">
|
||||
<h2 class="main-intro-header">
|
||||
<svg class="main-intro-header-logo">
|
||||
<use xlink:href="/header.svg#logo" />
|
||||
</svg>
|
||||
</h2>
|
||||
<div class="container">
|
||||
<GlitchedElement>
|
||||
<h1>404</h1>
|
||||
</GlitchedElement>
|
||||
|
||||
<p>Whoops! It seems that you are lost</p>
|
||||
<p>
|
||||
{{ $t('messages.notFoundPage.main') }}
|
||||
</p>
|
||||
|
||||
<strong class="about-link">
|
||||
<router-link to="/">Back to the main page</router-link>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GenericFooter />
|
||||
<strong class="about-link">
|
||||
<router-link to="/">
|
||||
{{ $t('messages.notFoundPage.link') }}
|
||||
</router-link>
|
||||
</strong>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GenericFooter from '../components/GenericFooter.vue';
|
||||
<script lang="ts" setup>
|
||||
import GlitchedElement from '../components/GlitchedElement.vue';
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../styles/colours' as *;
|
||||
|
||||
.main {
|
||||
&-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
width: 95vw;
|
||||
margin: 0 auto;
|
||||
padding: 0 4vw;
|
||||
|
||||
@media only screen and (max-width: 1023px) {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
padding: 3vh 4vw;
|
||||
}
|
||||
}
|
||||
|
||||
&-intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
padding: 1vh 2vw;
|
||||
|
||||
@media only screen and (max-width: 1367px) {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1023px) {
|
||||
width: 75%;
|
||||
margin-bottom: 4vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .stat-highlight {
|
||||
color: $shade1dark;
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
color: $shade2light;
|
||||
|
||||
&-logo {
|
||||
color: $shade2light;
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-description,
|
||||
&-more {
|
||||
width: fit-content;
|
||||
font-size: 18px;
|
||||
line-height: 143%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 75vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<template></template>
|
||||
|
||||
<script lang="ts" setup></script>
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="post-view">
|
||||
<Post
|
||||
v-if="postData"
|
||||
:id="postData.id"
|
||||
:subject="postData.subject"
|
||||
:content="postData.content"
|
||||
:account="postData.account"
|
||||
:attachments="postData.attachments"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import Post from '../components/Post.vue';
|
||||
import { getPostById } from '../graphql/post';
|
||||
|
||||
const route = useRoute();
|
||||
const postData = getPostById(route.params.id as string);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-view {
|
||||
margin: auto;
|
||||
max-width: 100ch;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<BaseTimeline :posts="posts" :load-more="loadMore" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import BaseTimeline from '../../components/BaseTimeline.vue';
|
||||
import { Post } from '../../components/Post.vue';
|
||||
import { MAX_UUID } from '../../consts';
|
||||
import { getPublic } from '../../graphql/timeline';
|
||||
|
||||
const posts = ref<Post[]>([]);
|
||||
const lastPostId = ref<string>(MAX_UUID);
|
||||
|
||||
const localTimelineQuery = getPublic(lastPostId, false);
|
||||
watch(localTimelineQuery, (newTimelineQuery) => {
|
||||
posts.value = newTimelineQuery?.publicTimeline.nodes ?? [];
|
||||
});
|
||||
|
||||
async function loadMore(): Promise<void> {
|
||||
lastPostId.value = posts.value[posts.value.length - 1].id;
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<BaseTimeline :posts="posts" :load-more="loadMore" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import BaseTimeline from '../../components/BaseTimeline.vue';
|
||||
import { Post } from '../../components/Post.vue';
|
||||
import { MAX_UUID } from '../../consts';
|
||||
import { getHome } from '../../graphql/timeline';
|
||||
|
||||
const posts = ref<Post[]>([]);
|
||||
const lastPostId = ref<string>(MAX_UUID);
|
||||
|
||||
const homeTimelineQuery = getHome(lastPostId);
|
||||
watch(homeTimelineQuery, (newTimelineQuery) => {
|
||||
posts.value = newTimelineQuery?.homeTimeline.nodes ?? [];
|
||||
});
|
||||
|
||||
async function loadMore(): Promise<void> {
|
||||
lastPostId.value = posts.value[posts.value.length - 1].id;
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<BaseTimeline :posts="posts" :load-more="loadMore" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import BaseTimeline from '../../components/BaseTimeline.vue';
|
||||
import { Post } from '../../components/Post.vue';
|
||||
import { MAX_UUID } from '../../consts';
|
||||
import { getPublic } from '../../graphql/timeline';
|
||||
|
||||
const posts = ref<Post[]>([]);
|
||||
const lastPostId = ref<string>(MAX_UUID);
|
||||
|
||||
const localTimelineQuery = getPublic(lastPostId, true);
|
||||
watch(localTimelineQuery, (newTimelineQuery) => {
|
||||
posts.value = newTimelineQuery?.publicTimeline.nodes ?? [];
|
||||
});
|
||||
|
||||
async function loadMore(): Promise<void> {
|
||||
lastPostId.value = posts.value[posts.value.length - 1].id;
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1 @@
|
|||
declare module 'vue-virtual-scroller';
|
|
@ -7,7 +7,7 @@ import { merge } from 'lodash';
|
|||
let message: string | undefined;
|
||||
|
||||
export const zxcvbnValidationMessage = () => {
|
||||
return message ? message : '';
|
||||
return message || '';
|
||||
};
|
||||
|
||||
export const zxcvbnRule: FormKitValidationRule = merge(
|
||||
|
|
1311
kitsune-fe/yarn.lock
1311
kitsune-fe/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -14,6 +14,6 @@ kitsune-retry-policies = { path = "../crates/kitsune-retry-policies" }
|
|||
mimalloc = "0.1.39"
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
toml = "0.8.2"
|
||||
tracing = "0.1.39"
|
||||
tracing = "0.1.40"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -62,7 +62,7 @@ simd-json = "0.12.0"
|
|||
speedy-uuid = { path = "../lib/speedy-uuid" }
|
||||
strum = { version = "0.25.0", features = ["derive", "phf"] }
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
time = "0.3.30"
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.9", features = ["compat"] }
|
||||
|
@ -73,10 +73,10 @@ tower-http = { version = "0.4.4", features = [
|
|||
"timeout",
|
||||
"trace",
|
||||
] }
|
||||
tracing = "0.1.39"
|
||||
tracing = "0.1.40"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = "0.3.17"
|
||||
typed-builder = "0.17.0"
|
||||
typed-builder = "0.18.0"
|
||||
url = "2.4.1"
|
||||
utoipa = { version = "4.0.0", features = ["axum_extras", "uuid"] }
|
||||
utoipa-swagger-ui = { version = "4.0.0", features = ["axum"] }
|
||||
|
@ -84,7 +84,7 @@ utoipa-swagger-ui = { version = "4.0.0", features = ["axum"] }
|
|||
# --- Optional dependencies ---
|
||||
|
||||
# "graphql" feature
|
||||
async-graphql = { version = "6.0.7", default-features = false, features = [
|
||||
async-graphql = { version = "6.0.9", default-features = false, features = [
|
||||
"playground",
|
||||
"tempfile",
|
||||
"time",
|
||||
|
@ -92,7 +92,7 @@ async-graphql = { version = "6.0.7", default-features = false, features = [
|
|||
"unblock",
|
||||
"uuid",
|
||||
], optional = true }
|
||||
async-graphql-axum = { version = "6.0.7", optional = true }
|
||||
async-graphql-axum = { version = "6.0.9", optional = true }
|
||||
|
||||
# "metrics" feature
|
||||
axum-prometheus = { version = "0.4.0", optional = true }
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
use axum::handler::Handler;
|
||||
use futures_util::{future::Either, FutureExt};
|
||||
use http::{header::ACCEPT, Request};
|
||||
use mime::APPLICATION_JSON;
|
||||
|
||||
const APPLICATION_ACTIVITY_JSON: &str = "application/activity+json";
|
||||
const APPLICATION_LD_JSON: &str = "application/ld+json";
|
||||
|
||||
/// Conditional wrapper around two handlers
|
||||
///
|
||||
/// If the conditional wrapper returns `true`, the left future is invoked.
|
||||
/// Otherwise the right future is invoked.
|
||||
#[derive(Clone)]
|
||||
pub struct ConditionalWrapper<C, L, R> {
|
||||
condition: C,
|
||||
left: L,
|
||||
right: R,
|
||||
}
|
||||
|
||||
impl<C, L, R> ConditionalWrapper<C, L, R> {
|
||||
pub fn new(condition: C, left: L, right: R) -> Self {
|
||||
Self {
|
||||
condition,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, L, R, T, S, B> Handler<T, S, B> for ConditionalWrapper<C, L, R>
|
||||
where
|
||||
C: Fn(&Request<B>) -> bool + Clone + Send + 'static,
|
||||
L: Clone + Handler<T, S, B> + Send + 'static,
|
||||
R: Clone + Handler<T, S, B> + Send + 'static,
|
||||
{
|
||||
type Future = Either<L::Future, R::Future>;
|
||||
|
||||
fn call(self, req: Request<B>, state: S) -> Self::Future {
|
||||
if (self.condition)(&req) {
|
||||
self.left.call(req, state).left_future()
|
||||
} else {
|
||||
self.right.call(req, state).right_future()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn html<B, L, R>(
|
||||
left: L,
|
||||
right: R,
|
||||
) -> ConditionalWrapper<impl Fn(&Request<B>) -> bool + Clone + Send + 'static, L, R> {
|
||||
let cond = |req: &Request<B>| {
|
||||
req.headers()
|
||||
.get(ACCEPT)
|
||||
.and_then(|header| {
|
||||
header
|
||||
.to_str()
|
||||
.map(|value| {
|
||||
!(value.contains(APPLICATION_JSON.as_ref())
|
||||
|| value.contains(APPLICATION_ACTIVITY_JSON)
|
||||
|| value.contains(APPLICATION_LD_JSON))
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or(true)
|
||||
};
|
||||
|
||||
ConditionalWrapper::new(cond, left, right)
|
||||
}
|
|
@ -15,6 +15,7 @@ impl InstanceQuery {
|
|||
let url_service = &state.service().url;
|
||||
let captcha = state.service().captcha.backend.clone().map(Into::into);
|
||||
|
||||
let character_limit = instance_service.character_limit();
|
||||
let description = instance_service.description().into();
|
||||
let domain = url_service.webfinger_domain().into();
|
||||
let local_post_count = instance_service.local_post_count().await?;
|
||||
|
@ -24,6 +25,7 @@ impl InstanceQuery {
|
|||
|
||||
Ok(Instance {
|
||||
captcha,
|
||||
character_limit,
|
||||
description,
|
||||
domain,
|
||||
local_post_count,
|
||||
|
|
|
@ -34,7 +34,7 @@ impl TimelineQuery {
|
|||
let get_home = GetHome::builder()
|
||||
.fetching_account_id(ctx.user_data()?.account.id)
|
||||
.max_id(after)
|
||||
.min_id(before);
|
||||
.since_id(before);
|
||||
let get_home = if let Some(first) = first {
|
||||
get_home.limit(first).build()
|
||||
} else {
|
||||
|
@ -76,7 +76,7 @@ impl TimelineQuery {
|
|||
|after, before, first, _last| async move {
|
||||
let get_public = GetPublic::builder()
|
||||
.max_id(after)
|
||||
.min_id(before)
|
||||
.since_id(before)
|
||||
.only_local(only_local);
|
||||
let get_public = if let Some(first) = first {
|
||||
get_public.limit(first).build()
|
||||
|
|
|
@ -30,6 +30,8 @@ impl From<kitsune_captcha::Captcha> for CaptchaInfo {
|
|||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct Instance {
|
||||
pub captcha: Option<CaptchaInfo>,
|
||||
pub character_limit: usize,
|
||||
pub description: String,
|
||||
pub domain: String,
|
||||
pub local_post_count: u64,
|
||||
|
@ -37,5 +39,4 @@ pub struct Instance {
|
|||
pub registrations_open: bool,
|
||||
pub user_count: u64,
|
||||
pub version: &'static str,
|
||||
pub captcha: Option<CaptchaInfo>,
|
||||
}
|
||||
|
|
|
@ -1,56 +1,11 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
http::{
|
||||
cond,
|
||||
page::{PostComponent, PostPage},
|
||||
responder::ActivityPubJson,
|
||||
},
|
||||
state::Zustand,
|
||||
};
|
||||
use crate::{error::Result, http::responder::ActivityPubJson, state::Zustand};
|
||||
use axum::{debug_handler, extract::Path, extract::State, routing, Router};
|
||||
use futures_util::TryStreamExt;
|
||||
use kitsune_core::{consts::VERSION, mapping::IntoObject, service::post::PostService};
|
||||
use kitsune_core::{mapping::IntoObject, service::post::PostService};
|
||||
use kitsune_type::ap::Object;
|
||||
use speedy_uuid::Uuid;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
mod activity;
|
||||
|
||||
#[debug_handler(state = Zustand)]
|
||||
async fn get_html(
|
||||
State(state): State<Zustand>,
|
||||
State(post_service): State<PostService>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<PostPage> {
|
||||
let post = post_service.get_by_id(id, None).await?;
|
||||
let ancestors = post_service
|
||||
.get_ancestors(post.id, None)
|
||||
.map_err(Error::from)
|
||||
.try_fold(VecDeque::new(), |mut acc, item| {
|
||||
let state = &state;
|
||||
async move {
|
||||
let item = PostComponent::prepare(state, item).await?;
|
||||
acc.push_front(item);
|
||||
Ok(acc)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let descendants = post_service
|
||||
.get_descendants(post.id, None)
|
||||
.map_err(Error::from)
|
||||
.and_then(|item| PostComponent::prepare(&state, item))
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
Ok(PostPage {
|
||||
ancestors,
|
||||
descendants,
|
||||
post: PostComponent::prepare(&state, post).await?,
|
||||
version: VERSION,
|
||||
})
|
||||
}
|
||||
|
||||
#[debug_handler(state = Zustand)]
|
||||
async fn get(
|
||||
State(state): State<Zustand>,
|
||||
|
@ -63,6 +18,6 @@ async fn get(
|
|||
|
||||
pub fn routes() -> Router<Zustand> {
|
||||
Router::new()
|
||||
.route("/:id", routing::get(cond::html(get_html, get)))
|
||||
.route("/:id", routing::get(get))
|
||||
.route("/:id/activity", routing::get(activity::get))
|
||||
}
|
||||
|
|
|
@ -1,30 +1,11 @@
|
|||
use crate::{
|
||||
consts::API_DEFAULT_LIMIT,
|
||||
error::{Error, Result},
|
||||
http::{
|
||||
cond,
|
||||
page::{PostComponent, UserPage},
|
||||
responder::ActivityPubJson,
|
||||
},
|
||||
state::Zustand,
|
||||
};
|
||||
use crate::{error::Result, http::responder::ActivityPubJson, state::Zustand};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
extract::{Path, State},
|
||||
routing::{self, post},
|
||||
Router,
|
||||
};
|
||||
use futures_util::{future::OptionFuture, TryStreamExt};
|
||||
use kitsune_core::{
|
||||
error::ApiError,
|
||||
mapping::IntoObject,
|
||||
service::{
|
||||
account::{AccountService, GetPosts},
|
||||
attachment::AttachmentService,
|
||||
url::UrlService,
|
||||
},
|
||||
};
|
||||
use kitsune_core::{error::ApiError, mapping::IntoObject, service::account::AccountService};
|
||||
use kitsune_type::ap::actor::Actor;
|
||||
use serde::Deserialize;
|
||||
use speedy_uuid::Uuid;
|
||||
|
||||
mod followers;
|
||||
|
@ -32,68 +13,10 @@ mod following;
|
|||
mod inbox;
|
||||
mod outbox;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PageQuery {
|
||||
min_id: Option<Uuid>,
|
||||
max_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
async fn get_html(
|
||||
State(state): State<Zustand>,
|
||||
State(account_service): State<AccountService>,
|
||||
State(attachment_service): State<AttachmentService>,
|
||||
State(url_service): State<UrlService>,
|
||||
Path(account_id): Path<Uuid>,
|
||||
Query(query): Query<PageQuery>,
|
||||
) -> Result<UserPage> {
|
||||
let account = account_service
|
||||
.get_by_id(account_id)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
let get_posts = GetPosts::builder()
|
||||
.account_id(account.id)
|
||||
.max_id(query.max_id)
|
||||
.min_id(query.min_id)
|
||||
.limit(API_DEFAULT_LIMIT)
|
||||
.build();
|
||||
|
||||
let posts = account_service
|
||||
.get_posts(get_posts)
|
||||
.await?
|
||||
.map_err(Error::from)
|
||||
.and_then(|post| PostComponent::prepare(&state, post))
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
let mut acct = format!("@{}", account.username);
|
||||
if !account.local {
|
||||
acct.push('@');
|
||||
acct.push_str(&account.domain);
|
||||
}
|
||||
|
||||
let profile_picture_url =
|
||||
OptionFuture::from(account.avatar_id.map(|id| attachment_service.get_url(id)))
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
Ok(UserPage {
|
||||
acct,
|
||||
display_name: account.display_name.unwrap_or(account.username),
|
||||
profile_picture_url: profile_picture_url
|
||||
.unwrap_or_else(|| url_service.default_avatar_url()),
|
||||
bio: account.note.unwrap_or_default(),
|
||||
posts,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get(
|
||||
State(state): State<Zustand>,
|
||||
State(account_service): State<AccountService>,
|
||||
_: State<AttachmentService>, // Needed to get the same types for the conditional routing
|
||||
_: State<UrlService>, // Needed to get the same types for the conditional routing
|
||||
Path(account_id): Path<Uuid>,
|
||||
_: Query<PageQuery>, // Needed to get the same types for the conditional routing
|
||||
) -> Result<ActivityPubJson<Actor>> {
|
||||
let account = account_service
|
||||
.get_by_id(account_id)
|
||||
|
@ -105,7 +28,7 @@ async fn get(
|
|||
|
||||
pub fn routes() -> Router<Zustand> {
|
||||
Router::new()
|
||||
.route("/:user_id", routing::get(cond::html(get_html, get)))
|
||||
.route("/:user_id", routing::get(get))
|
||||
.route("/:user_id/followers", routing::get(followers::get))
|
||||
.route("/:user_id/following", routing::get(following::get))
|
||||
.route("/:user_id/inbox", post(inbox::post))
|
||||
|
|
|
@ -15,13 +15,11 @@ use tower_http::{
|
|||
};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
mod cond;
|
||||
#[cfg(feature = "graphql-api")]
|
||||
mod graphql;
|
||||
mod handler;
|
||||
mod middleware;
|
||||
mod openapi;
|
||||
mod page;
|
||||
#[cfg(feature = "mastodon-api")]
|
||||
mod pagination;
|
||||
mod responder;
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
state::Zustand,
|
||||
};
|
||||
use askama::Template;
|
||||
use diesel::{BelongingToDsl, QueryDsl, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use futures_util::{future::OptionFuture, TryStreamExt};
|
||||
use kitsune_core::try_join;
|
||||
use kitsune_db::{
|
||||
model::{
|
||||
account::Account,
|
||||
media_attachment::{MediaAttachment as DbMediaAttachment, PostMediaAttachment},
|
||||
post::Post,
|
||||
},
|
||||
schema::{accounts, media_attachments},
|
||||
};
|
||||
use scoped_futures::ScopedFutureExt;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct MediaAttachment {
|
||||
pub content_type: String,
|
||||
pub description: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "components/post.html", escape = "none")] // Make sure everything is escaped either on submission or in the template
|
||||
pub struct PostComponent {
|
||||
pub display_name: String,
|
||||
pub acct: String,
|
||||
pub profile_url: String,
|
||||
pub profile_picture_url: String,
|
||||
pub content: String,
|
||||
pub url: String,
|
||||
pub attachments: Vec<MediaAttachment>,
|
||||
}
|
||||
|
||||
impl PostComponent {
|
||||
pub async fn prepare(state: &Zustand, post: Post) -> Result<Self> {
|
||||
let (author, attachments_stream) = state
|
||||
.db_pool()
|
||||
.with_connection(|db_conn| {
|
||||
async {
|
||||
let author_fut = accounts::table
|
||||
.find(post.account_id)
|
||||
.select(Account::as_select())
|
||||
.get_result::<Account>(db_conn);
|
||||
|
||||
let attachments_stream_fut = PostMediaAttachment::belonging_to(&post)
|
||||
.inner_join(media_attachments::table)
|
||||
.select(DbMediaAttachment::as_select())
|
||||
.load_stream::<DbMediaAttachment>(db_conn);
|
||||
|
||||
try_join!(author_fut, attachments_stream_fut)
|
||||
}
|
||||
.scoped()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let attachments = attachments_stream
|
||||
.map_err(Error::from)
|
||||
.and_then(|attachment| async move {
|
||||
let url = state.service().attachment.get_url(attachment.id).await?;
|
||||
|
||||
Ok(MediaAttachment {
|
||||
content_type: attachment.content_type,
|
||||
description: attachment.description,
|
||||
url,
|
||||
})
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
let profile_picture_url = OptionFuture::from(
|
||||
author
|
||||
.avatar_id
|
||||
.map(|id| state.service().attachment.get_url(id)),
|
||||
)
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
let mut acct = format!("@{}", author.username);
|
||||
if !author.local {
|
||||
acct.push('@');
|
||||
acct.push_str(&author.domain);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
attachments,
|
||||
display_name: author
|
||||
.display_name
|
||||
.unwrap_or_else(|| author.username.clone()),
|
||||
acct,
|
||||
profile_url: author.url,
|
||||
profile_picture_url: profile_picture_url
|
||||
.unwrap_or_else(|| state.service().url.default_avatar_url()),
|
||||
content: post.content,
|
||||
url: post.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/posts.html", escape = "none")]
|
||||
pub struct PostPage {
|
||||
pub ancestors: VecDeque<PostComponent>,
|
||||
pub post: PostComponent,
|
||||
pub descendants: Vec<PostComponent>,
|
||||
pub version: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/users.html", escape = "none")]
|
||||
pub struct UserPage {
|
||||
pub acct: String,
|
||||
pub display_name: String,
|
||||
pub profile_picture_url: String,
|
||||
pub bio: String,
|
||||
pub posts: Vec<PostComponent>,
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/public/template.css">
|
||||
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
|
||||
<title>
|
||||
{% block title %}
|
||||
{% endblock %}
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,32 +0,0 @@
|
|||
<div class="post">
|
||||
<a href='{{ profile_url|escape("html") }}'>
|
||||
<div class="account-info">
|
||||
<img class="profile-picture" src='{{ profile_picture_url|escape("html") }}'
|
||||
alt="{{ display_name }}'s profile picture" />
|
||||
<div class="account-info__names">
|
||||
<strong>{{ display_name }}</strong>
|
||||
{{ acct|escape("html") }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="post__url" href='{{ url|escape("html") }}'>
|
||||
<div class="post__content">{{ content }}</div>
|
||||
</a>
|
||||
|
||||
<div class="post__attachments">
|
||||
{% for attachment in attachments %}
|
||||
<div title='{{ attachment.description.as_deref().unwrap_or_default()|escape("html") }}'
|
||||
href='{{ attachment.url.as_str()|escape("html") }}'>
|
||||
{% if attachment.content_type.starts_with("audio") %}
|
||||
<audio src='{{ attachment.url|escape("html") }}' />
|
||||
{% else if attachment.content_type.starts_with("video") %}
|
||||
<video src='{{ attachment.url|escape("html") }}' controls />
|
||||
{% else %}
|
||||
<img src='{{ attachment.url|escape("html") }}'
|
||||
alt='{{ attachment.description.as_deref().unwrap_or_default()|escape("html") }}' />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,51 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "../base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/public/template.css">
|
||||
<title>OAuth Login</title>
|
||||
</head>
|
||||
{% block title %}
|
||||
OAuth Login
|
||||
{% endblock %}
|
||||
|
||||
{% macro add_query_params() %}
|
||||
<input type="hidden" name="client_id" value="{{ query.client_id }}" />
|
||||
<input type="hidden" name="redirect_uri" value="{{ query.redirect_uri }}" />
|
||||
<input type="hidden" name="response_type" value="{{ query.response_type }}" />
|
||||
<input type="hidden" name="scope" value="{{ query.scope }}" />
|
||||
<input type="hidden" name="state" value="{{ query.state.as_deref().unwrap_or("") }}" />
|
||||
<input type="hidden" name="client_id" value="{{ query.client_id }}" />
|
||||
<input type="hidden" name="redirect_uri" value="{{ query.redirect_uri }}" />
|
||||
<input type="hidden" name="response_type" value="{{ query.response_type }}" />
|
||||
<input type="hidden" name="scope" value="{{ query.scope }}" />
|
||||
<input type="hidden" name="state" value="{{ query.state.as_deref().unwrap_or("") }}" />
|
||||
{% endmacro %}
|
||||
|
||||
<body>
|
||||
<p class="header">Kitsune</p>
|
||||
{% block body %}
|
||||
<p class="header">
|
||||
Kitsune
|
||||
</p>
|
||||
|
||||
<div class="consent-text">
|
||||
<span class="appName">{{app_name}}</span> wants the following access to your Kitsune account:
|
||||
<p>
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
<li>{{ scope.get_message().unwrap() }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
<li>
|
||||
{{ scope.get_message().unwrap() }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="consent-forms">
|
||||
<form method="get">
|
||||
{% call add_query_params() %}
|
||||
<input type="hidden" name="login_consent" value="accept" />
|
||||
<input class="formButton" type="submit" value="Accept" />
|
||||
</form>
|
||||
|
||||
<form method="get">
|
||||
{% call add_query_params() %}
|
||||
<input type="hidden" name="login_consent" value="deny" />
|
||||
<input class="formButton" type="submit" value="Deny" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="disclaimer">
|
||||
Authenticated as <span class="authUsername">{{ authenticated_username }}</span> ~
|
||||
<a href="#"
|
||||
onclick="document.cookie='user_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC;';window.navigation.reload();">Logout</a>
|
||||
onclick="document.cookie='user_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC;';window.location.reload();">Logout</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "../base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/public/template.css">
|
||||
<title>OAuth Login</title>
|
||||
</head>
|
||||
{% block title %}
|
||||
OAuth Login
|
||||
{% endblock %}
|
||||
|
||||
<body>
|
||||
{% block body %}
|
||||
<p class="header">Kitsune</p>
|
||||
|
||||
{% for (_level, msg) in flash_messages %}
|
||||
<div class="message">
|
||||
{{ msg }}
|
||||
</div>
|
||||
<div class="message">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<form method="post">
|
||||
<div>
|
||||
<label class="label" for="username">Username</label><br />
|
||||
|
@ -25,6 +22,4 @@
|
|||
</div>
|
||||
<input class="formButton" type="submit" value="Submit" />
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% endblock %}
|
|
@ -1,20 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "../base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/public/template.css">
|
||||
<title>OAuth Token</title>
|
||||
</head>
|
||||
{% block title %}
|
||||
OAuth Token
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<p class="header">
|
||||
{{domain}}
|
||||
</p>
|
||||
|
||||
<body>
|
||||
<p class="header">{{domain}}</p>
|
||||
<div class="tokenContainer">
|
||||
<p>Copy & Paste this token into <span class="appName">{{app_name}}</span></p>
|
||||
<code class="token">{{token}}</code>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% endblock %}
|
|
@ -1,3 +0,0 @@
|
|||
<footer class="footer">
|
||||
Powered by <a target="_blank" href="https://github.com/kitsune-soc/kitsune">Kitsune</a> v{{ version }}
|
||||
</footer>
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Post - Kitsune</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/public/colours.css" />
|
||||
<link rel="stylesheet" href="/public/post-style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="posts__ancestors">
|
||||
{% for ancestor in ancestors %}
|
||||
{{ ancestor }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="posts__main">
|
||||
{{ post }}
|
||||
</div>
|
||||
|
||||
<div class="posts__descendants">
|
||||
{% for descendant in descendants %}
|
||||
{{ descendant }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include "footer.html" %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,28 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{ acct|escape("html") }} - Kitsune</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/public/colours.css" />
|
||||
<link rel="stylesheet" href="/public/post-style.css" />
|
||||
<link rel="stylesheet" href="/public/user-style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="user__header">
|
||||
<img class="user__profile-picture" src='{{ profile_picture_url|escape("html") }}' />
|
||||
<div class="user__names">
|
||||
<strong>{{ display_name }}</strong>
|
||||
{{ acct|escape("html") }}
|
||||
</div>
|
||||
<p class="user__bio">{{ bio }}</p>
|
||||
</div>
|
||||
{% for post in posts %}
|
||||
{{ post }}
|
||||
{% endfor %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -26,10 +26,10 @@ serde = { version = "1.0.189", features = ["derive"] }
|
|||
simd-json = "0.12.0"
|
||||
smol_str = "0.2.0"
|
||||
speedy-uuid = { path = "../speedy-uuid", features = ["redis", "serde"] }
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
tokio = { version = "1.33.0", features = ["macros", "rt", "sync"] }
|
||||
tracing = "0.1.39"
|
||||
typed-builder = "0.17.0"
|
||||
tracing = "0.1.40"
|
||||
typed-builder = "0.18.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.3.17"
|
||||
|
|
|
@ -4,13 +4,13 @@ edition.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { version = "6.0.7", default-features = false, optional = true }
|
||||
async-graphql = { version = "6.0.9", default-features = false, optional = true }
|
||||
diesel = { version = "2.1.3", features = [
|
||||
"postgres_backend",
|
||||
"uuid",
|
||||
], optional = true }
|
||||
redis = { version = "0.23.3", default-features = false, optional = true }
|
||||
serde = { version = "1.0.189", optional = true }
|
||||
thiserror = "1.0.49"
|
||||
thiserror = "1.0.50"
|
||||
uuid = { version = "1.5.0", features = ["fast-rng", "v7"] }
|
||||
uuid-simd = { version = "0.8.0", features = ["uuid"] }
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
:root {
|
||||
--dark1: #0f1026;
|
||||
--dark2: #051c30;
|
||||
--dark3: #042f40;
|
||||
--shade1dark: #53a0c4;
|
||||
--shade1light: #afd7fa;
|
||||
--shade2dark: #935d7e;
|
||||
--shade2light: #d68fbc;
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
:root {
|
||||
background-color: var(--dark1);
|
||||
color: white;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--shade2light);
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post {
|
||||
border: 1px solid var(--shade2dark);
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.posts__main * {
|
||||
background-color: var(--dark2);
|
||||
}
|
||||
|
||||
.post__content {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.post__attachments {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.post__attachments * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post__url {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account-info__names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
width: 60px;
|
||||
}
|
|
@ -1,12 +1,4 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Play:wght@400;700&display=swap");
|
||||
|
||||
:root {
|
||||
--crimson: #ff124f;
|
||||
--magenta: #ff00a0;
|
||||
--pink: #fe75fe;
|
||||
--indigo: #7a04eb;
|
||||
--darkblue: #120458;
|
||||
|
||||
--dark1: #0f1026;
|
||||
--dark2: #051c30;
|
||||
--dark3: #042f40;
|
||||
|
@ -17,7 +9,7 @@
|
|||
}
|
||||
|
||||
* {
|
||||
font-family: Play;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
.user__header {
|
||||
border: 1px solid var(--shade2dark);
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
background-color: var(--dark2);
|
||||
}
|
||||
|
||||
.user__bio {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user__names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.user__profile-picture {
|
||||
float: left;
|
||||
width: 160px;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
Loading…
Reference in New Issue