]> Untitled Git - lemmy.git/commitdiff
Merge branch 'webmanifest' of https://github.com/kartikynwa/lemmy into kartikynwa...
authorDessalines <tyhou13@gmx.com>
Sat, 29 Aug 2020 21:00:43 +0000 (17:00 -0400)
committerDessalines <tyhou13@gmx.com>
Sat, 29 Aug 2020 21:00:43 +0000 (17:00 -0400)
109 files changed:
.travis.yml
ansible/VERSION
docker/dev/Dockerfile
docker/prod/Dockerfile
docker/prod/docker-compose.yml
docs/src/administration_configuration.md
docs/src/contributing_tests.md
docs/src/contributing_websocket_http_api.md
install.sh
server/Cargo.lock
server/Cargo.toml
server/db-init.sh
server/diesel.toml
server/lemmy_db/Cargo.toml [new file with mode: 0644]
server/lemmy_db/src/activity.rs [moved from server/src/db/activity.rs with 84% similarity]
server/lemmy_db/src/category.rs [moved from server/src/db/category.rs with 95% similarity]
server/lemmy_db/src/comment.rs [moved from server/src/db/comment.rs with 96% similarity]
server/lemmy_db/src/comment_view.rs [moved from server/src/db/comment_view.rs with 95% similarity]
server/lemmy_db/src/community.rs [moved from server/src/db/community.rs with 98% similarity]
server/lemmy_db/src/community_view.rs [moved from server/src/db/community_view.rs with 98% similarity]
server/lemmy_db/src/lib.rs [moved from server/src/db/mod.rs with 78% similarity]
server/lemmy_db/src/moderator.rs [moved from server/src/db/moderator.rs with 99% similarity]
server/lemmy_db/src/moderator_views.rs [moved from server/src/db/moderator_views.rs with 99% similarity]
server/lemmy_db/src/password_reset_request.rs [moved from server/src/db/password_reset_request.rs with 98% similarity]
server/lemmy_db/src/post.rs [moved from server/src/db/post.rs with 96% similarity]
server/lemmy_db/src/post_view.rs [moved from server/src/db/post_view.rs with 94% similarity]
server/lemmy_db/src/private_message.rs [moved from server/src/db/private_message.rs with 92% similarity]
server/lemmy_db/src/private_message_view.rs [moved from server/src/db/private_message_view.rs with 98% similarity]
server/lemmy_db/src/schema.rs [moved from server/src/schema.rs with 98% similarity]
server/lemmy_db/src/site.rs [moved from server/src/db/site.rs with 97% similarity]
server/lemmy_db/src/site_view.rs [moved from server/src/db/site_view.rs with 100% similarity]
server/lemmy_db/src/user.rs [moved from server/src/db/user.rs with 73% similarity]
server/lemmy_db/src/user_mention.rs [moved from server/src/db/user_mention.rs with 96% similarity]
server/lemmy_db/src/user_mention_view.rs [moved from server/src/db/user_mention_view.rs with 97% similarity]
server/lemmy_db/src/user_view.rs [moved from server/src/db/user_view.rs with 96% similarity]
server/lemmy_utils/Cargo.toml [new file with mode: 0644]
server/lemmy_utils/src/lib.rs [new file with mode: 0644]
server/lemmy_utils/src/settings.rs [moved from server/src/settings.rs with 79% similarity]
server/migrations/2020-07-08-202609_add_creator_published/down.sql [new file with mode: 0644]
server/migrations/2020-07-08-202609_add_creator_published/up.sql [new file with mode: 0644]
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/down.sql [new file with mode: 0644]
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/up.sql [new file with mode: 0644]
server/src/api/claims.rs [new file with mode: 0644]
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/apub/activities.rs
server/src/apub/comment.rs
server/src/apub/community.rs
server/src/apub/community_inbox.rs
server/src/apub/extensions/group_extensions.rs
server/src/apub/extensions/signatures.rs
server/src/apub/fetcher.rs
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/private_message.rs
server/src/apub/shared_inbox.rs
server/src/apub/user.rs
server/src/apub/user_inbox.rs
server/src/code_migrations.rs [moved from server/src/db/code_migrations.rs with 86% similarity]
server/src/lib.rs
server/src/main.rs
server/src/rate_limit/mod.rs
server/src/routes/federation.rs
server/src/routes/feeds.rs
server/src/routes/index.rs
server/src/routes/nodeinfo.rs
server/src/routes/webfinger.rs
server/src/version.rs
server/test.sh [new file with mode: 0755]
ui/assets/css/choices.min.css [new file with mode: 0644]
ui/assets/css/main.css
ui/assets/css/selectr.min.css [deleted file]
ui/fuse.js
ui/package.json
ui/src/components/cake-day.tsx [new file with mode: 0644]
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/communities.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/create-private-message.tsx
ui/src/components/data-type-select.tsx
ui/src/components/inbox.tsx
ui/src/components/listing-type-select.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post.tsx
ui/src/components/search.tsx
ui/src/components/sort-select.tsx
ui/src/components/symbols.tsx
ui/src/components/user-details.tsx [new file with mode: 0644]
ui/src/components/user-listing.tsx
ui/src/components/user.tsx
ui/src/index.html
ui/src/interfaces.ts
ui/src/utils.ts
ui/src/version.ts
ui/translations/en.json
ui/translations/eu.json
ui/translations/it.json
ui/translations/sr_Latn.json
ui/yarn.lock

index 602a8613dc62ce4e27f38ed5bbf0221ad94057ef..9541afaae1e57f50bf98ad5d46caaa4f906d2463 100644 (file)
@@ -24,10 +24,11 @@ script:
   - cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
   - cargo install diesel_cli --no-default-features --features postgres --force
   - diesel migration run
-  - cargo test
+  - cargo test --workspace
 env:
   global:
     - DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+    - LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
     - RUST_TEST_THREADS=1
 
 addons:
index 3a952db0209ddd08a1743d00f4e7b8442121e446..913ced209c4b246b99872f13a665da8eeecdde09 100644 (file)
@@ -1 +1 @@
-v0.7.13
+v0.7.21
index 82a03f3c954c46a922727d544148237645e455d8..b86618d8574b478d6356f103b5fd57e0967c0953 100644 (file)
@@ -18,11 +18,12 @@ RUN sudo chown -R rust:rust .
 RUN USER=root cargo new server
 WORKDIR /app/server
 COPY server/Cargo.toml server/Cargo.lock ./
-RUN sudo chown -R rust:rust .
+COPY server/lemmy_db ./lemmy_db
+COPY server/lemmy_utils ./lemmy_utils
 RUN mkdir -p ./src/bin \
-  && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs 
+   && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
 RUN cargo build
-RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
+RUN find target/debug -type f -name "$(echo "lemmy_server" | tr '-' '_')*" -exec touch -t 200001010000 {} +
 COPY server/src ./src/
 COPY server/migrations ./migrations/
 
index 54485a37e8bf7ad9446100ec072627041e1bec41..774387404d930cdbc7b807ef72fac03c428eca08 100644 (file)
@@ -10,14 +10,15 @@ WORKDIR /app
 RUN sudo chown -R rust:rust .
 RUN USER=root cargo new server
 WORKDIR /app/server
-COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./
-#RUN sudo chown -R rust:rust .
+COPY server/Cargo.toml server/Cargo.lock ./
+COPY server/lemmy_db ./lemmy_db
+COPY server/lemmy_utils ./lemmy_utils
 RUN mkdir -p ./src/bin \
    && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
 RUN cargo build --release
-RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server*
-COPY --chown=rust:rust server/src ./src/
-COPY --chown=rust:rust server/migrations ./migrations/
+RUN find target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR -type f -name "$(echo "lemmy_server" | tr '-' '_')*" -exec touch -t 200001010000 {} +
+COPY server/src ./src/
+COPY server/migrations ./migrations/
 
 # build for release
 # workaround for https://github.com/rust-lang/rust/issues/62896
index f05a9b384c3db38c10f71600c43f39a7a407bf4c..38f9c7db427c4dd5b9524dc8ba13b513a2b1f0bb 100644 (file)
@@ -12,7 +12,7 @@ services:
     restart: always
 
   lemmy:
-    image: dessalines/lemmy:v0.7.13
+    image: dessalines/lemmy:v0.7.21
     ports:
       - "127.0.0.1:8536:8536"
     restart: always
index 56448de46e8ad60047b9db8c401c0fdfc977a762..cc4c568987a4d0b309d9ed8cf0eb9a66937f4316 100644 (file)
@@ -5,6 +5,8 @@ The configuration is based on the file
 This file also contains documentation for all the available options. To override the defaults, you
 can copy the options you want to change into your local `config.hjson` file.
 
+To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`.
+
 Additionally, you can override any config files with environment variables. These have the same
 name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
 `database.password` with `LEMMY_DATABASE__POOL_SIZE=10`.
index 13e5d1222321f6444e271bc5c56e6a79494206a5..d4168e190608039732b28414ab03ccaa8f8ba85a 100644 (file)
@@ -7,9 +7,7 @@ following commands in the `server` subfolder:
 
 ```bash
 psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
-export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
-diesel migration run
-RUST_TEST_THREADS=1 cargo test
+./test.sh
 ```
 
 ### Federation
index 567f674cd9eb6a8277155e30e855f6c995d316cc..6ed25b98ebe09290185a3d5d8cb48e287ac70a93 100644 (file)
@@ -1149,6 +1149,7 @@ Post listing types are `All, Subscribed, Community`
     page: Option<i64>,
     limit: Option<i64>,
     community_id: Option<i32>,
+    community_name: Option<String>,
     auth: Option<String>
   }
 }
index fb42b26d121110fe132a75255fa633e9cd08796e..19b847b1c58057cb2d57625597fac2700413085d 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 set -e
 
 # Set the database variable to the default first.
index 6d6364d4e30f877544c131c330994d3c0ecdbef5..d90b96799b2e5fdb583135c78054e1eceaafd92b 100644 (file)
@@ -1399,12 +1399,6 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "htmlescape"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
-
 [[package]]
 name = "http"
 version = "0.2.1"
@@ -1428,9 +1422,9 @@ dependencies = [
 
 [[package]]
 name = "http-signature-normalization-actix"
-version = "0.4.0-alpha.0"
+version = "0.4.0-alpha.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09afff6987c7edbed101d1cddd2185786fb0af0dd9c06b654aca73a0a763680f"
+checksum = "131fc982391a6b37847888b568cbe0e9cd302f1b0015f4f6f4a50234bebd049c"
 dependencies = [
  "actix-http",
  "actix-web",
@@ -1572,6 +1566,21 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lemmy_db"
+version = "0.1.0"
+dependencies = [
+ "bcrypt",
+ "chrono",
+ "diesel",
+ "log",
+ "serde 1.0.114",
+ "serde_json",
+ "sha2",
+ "strum",
+ "strum_macros",
+]
+
 [[package]]
 name = "lemmy_server"
 version = "0.0.1"
@@ -1589,27 +1598,23 @@ dependencies = [
  "base64 0.12.3",
  "bcrypt",
  "chrono",
- "comrak",
- "config",
  "diesel",
  "diesel_migrations",
  "dotenv",
  "env_logger",
  "failure",
  "futures",
- "htmlescape",
  "http",
  "http-signature-normalization-actix",
  "itertools",
  "jsonwebtoken",
  "lazy_static",
- "lettre",
- "lettre_email",
+ "lemmy_db",
+ "lemmy_utils",
  "log",
  "openssl",
  "percent-encoding",
  "rand 0.7.3",
- "regex",
  "rss",
  "serde 1.0.114",
  "serde_json",
@@ -1621,6 +1626,26 @@ dependencies = [
  "uuid 0.8.1",
 ]
 
+[[package]]
+name = "lemmy_utils"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "comrak",
+ "config",
+ "itertools",
+ "lazy_static",
+ "lettre",
+ "lettre_email",
+ "log",
+ "openssl",
+ "rand 0.7.3",
+ "regex",
+ "serde 1.0.114",
+ "serde_json",
+ "url",
+]
+
 [[package]]
 name = "lettre"
 version = "0.9.3"
index 8daf72c4a0aaf7063dc6da17c1aec48286218dab..2aa3c139b7c8b1511527c69efd959925de0b3865 100644 (file)
@@ -1,14 +1,21 @@
 [package]
 name = "lemmy_server"
 version = "0.0.1"
-authors = ["Dessalines <tyhou13@gmx.com>"]
 edition = "2018"
 
 [profile.release]
 lto = true
 
+[workspace]
+members = [
+    "lemmy_utils",
+    "lemmy_db"
+]
+
 [dependencies]
-diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
+lemmy_utils = { path = "./lemmy_utils" }
+lemmy_db = { path = "./lemmy_db" }
+diesel = "1.4.4"
 diesel_migrations = "1.4.0"
 dotenv = "0.15.0"
 activitystreams = "0.6.2"
@@ -31,19 +38,13 @@ rand = "0.7.3"
 strum = "0.18.0"
 strum_macros = "0.18.0"
 jsonwebtoken = "7.0.1"
-regex = "1.3.5"
 lazy_static = "1.3.0"
-lettre = "0.9.3"
-lettre_email = "0.9.4"
 rss = "1.9.0"
-htmlescape = "0.3.1"
 url = { version = "2.1.1", features = ["serde"] }
-config = {version = "0.10.1", default-features = false, features = ["hjson"] }
 percent-encoding = "2.1.0"
-comrak = "0.7"
 openssl = "0.10"
 http = "0.2.1"
-http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] }
+http-signature-normalization-actix = { version = "0.4.0-alpha.2", default-features = false, features = ["sha-2"] }
 base64 = "0.12.1"
 tokio = "0.2.21"
 futures = "0.3.5"
index a2ad77b5945830dfb1e1973654d8bc99a8f79493..ccecb7de717892efb4c72128656f67b42c7769dc 100755 (executable)
@@ -1,4 +1,5 @@
-#!/bin/sh
+#!/bin/bash
+set -e
 
 # Default configurations
 username=lemmy
index 92267c829f2027cdf0e641efa95622d7ddd8bb74..1644558f1e01b333c70fc393c0193cba221b359a 100644 (file)
@@ -2,4 +2,4 @@
 # see diesel.rs/guides/configuring-diesel-cli
 
 [print_schema]
-file = "src/schema.rs"
+file = "lemmy_db/src/schema.rs"
diff --git a/server/lemmy_db/Cargo.toml b/server/lemmy_db/Cargo.toml
new file mode 100644 (file)
index 0000000..d94cf5f
--- /dev/null
@@ -0,0 +1,15 @@
+[package]
+name = "lemmy_db"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
+chrono = { version = "0.4.7", features = ["serde"] }
+serde = { version = "1.0.105", features = ["derive"] }
+serde_json = { version = "1.0.52", features = ["preserve_order"]}
+strum = "0.18.0"
+strum_macros = "0.18.0"
+log = "0.4.0"
+sha2 = "0.9"
+bcrypt = "0.8.0"
\ No newline at end of file
similarity index 84%
rename from server/src/db/activity.rs
rename to server/lemmy_db/src/activity.rs
index 8c2b0c7425b72a9421740863877b55b91f130908..83f85ca1eea9ae94593e3a16534df2a9e7e4a796 100644 (file)
@@ -1,9 +1,12 @@
-use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError};
+use crate::{schema::activity, Crud};
 use diesel::{dsl::*, result::Error, *};
 use log::debug;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use std::fmt::Debug;
+use std::{
+  fmt::Debug,
+  io::{Error as IoError, ErrorKind},
+};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "activity"]
@@ -55,46 +58,43 @@ impl Crud<ActivityForm> for Activity {
   }
 }
 
-pub async fn insert_activity<T>(
-  user_id: i32,
-  data: T,
-  local: bool,
-  pool: &DbPool,
-) -> Result<(), LemmyError>
-where
-  T: Serialize + Debug + Send + 'static,
-{
-  blocking(pool, move |conn| {
-    do_insert_activity(conn, user_id, &data, local)
-  })
-  .await??;
-  Ok(())
-}
-
-fn do_insert_activity<T>(
+pub fn do_insert_activity<T>(
   conn: &PgConnection,
   user_id: i32,
   data: &T,
   local: bool,
-) -> Result<(), LemmyError>
+) -> Result<Activity, IoError>
 where
   T: Serialize + Debug,
 {
+  debug!("inserting activity for user {}, data {:?}", user_id, &data);
   let activity_form = ActivityForm {
     user_id,
     data: serde_json::to_value(&data)?,
     local,
     updated: None,
   };
-  debug!("inserting activity for user {}, data {:?}", user_id, data);
-  Activity::create(&conn, &activity_form)?;
-  Ok(())
+  let result = Activity::create(&conn, &activity_form);
+  match result {
+    Ok(s) => Ok(s),
+    Err(e) => Err(IoError::new(
+      ErrorKind::Other,
+      format!("Failed to insert activity into database: {}", e),
+    )),
+  }
 }
 
 #[cfg(test)]
 mod tests {
-  use super::{super::user::*, *};
-  use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType};
+  use crate::{
+    activity::{Activity, ActivityForm},
+    tests::establish_unpooled_connection,
+    user::{UserForm, User_},
+    Crud,
+    ListingType,
+    SortType,
+  };
+  use serde_json::Value;
 
   #[test]
   fn test_crud() {
similarity index 95%
rename from server/src/db/category.rs
rename to server/lemmy_db/src/category.rs
index ff49bbbee3057194d284ef349c2e104902a50a54..ec2efc7b7a17aaa9f71fd921d4e305553fad103f 100644 (file)
@@ -1,6 +1,6 @@
 use crate::{
-  db::Crud,
   schema::{category, category::dsl::*},
+  Crud,
 };
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
@@ -52,8 +52,7 @@ impl Category {
 
 #[cfg(test)]
 mod tests {
-  use super::*;
-  use crate::db::establish_unpooled_connection;
+  use crate::{category::Category, tests::establish_unpooled_connection};
 
   #[test]
   fn test_crud() {
similarity index 96%
rename from server/src/db/comment.rs
rename to server/lemmy_db/src/comment.rs
index 7e76770f671ec29ebbf106aea36d8bcfbd1f8fe3..602070d51e405a540ae72e8330bcde79d75e8e5a 100644 (file)
@@ -1,9 +1,5 @@
 use super::{post::Post, *};
-use crate::{
-  apub::{make_apub_endpoint, EndpointType},
-  naive_now,
-  schema::{comment, comment_like, comment_saved},
-};
+use crate::schema::{comment, comment_like, comment_saved};
 
 // WITH RECURSIVE MyTree AS (
 //     SELECT * FROM comment WHERE parent_id IS NULL
@@ -77,12 +73,15 @@ impl Crud<CommentForm> for Comment {
 }
 
 impl Comment {
-  pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+  pub fn update_ap_id(
+    conn: &PgConnection,
+    comment_id: i32,
+    apub_id: String,
+  ) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
 
-    let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
     diesel::update(comment.find(comment_id))
-      .set(ap_id.eq(apid))
+      .set(ap_id.eq(apub_id))
       .get_result::<Self>(conn)
   }
 
@@ -204,10 +203,8 @@ impl Saveable<CommentSavedForm> for CommentSaved {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{community::*, post::*, user::*},
-    *,
-  };
+  use crate::{comment::*, community::*, post::*, tests::establish_unpooled_connection, user::*};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
similarity index 95%
rename from server/src/db/comment_view.rs
rename to server/lemmy_db/src/comment_view.rs
index d1b27a3c86946434a743b09500c6229f164891c5..4af13c2d9012abed1f12d878fd56052eb20a0a2d 100644 (file)
@@ -1,5 +1,5 @@
 // TODO, remove the cross join here, just join to user directly
-use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
+use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
 use diesel::{dsl::*, pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -9,6 +9,7 @@ table! {
     id -> Int4,
     creator_id -> Int4,
     post_id -> Int4,
+    post_name -> Varchar,
     parent_id -> Nullable<Int4>,
     content -> Text,
     removed -> Bool,
@@ -27,6 +28,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
@@ -44,6 +46,7 @@ table! {
     id -> Int4,
     creator_id -> Int4,
     post_id -> Int4,
+    post_name -> Varchar,
     parent_id -> Nullable<Int4>,
     content -> Text,
     removed -> Bool,
@@ -62,6 +65,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
@@ -82,6 +86,7 @@ pub struct CommentView {
   pub id: i32,
   pub creator_id: i32,
   pub post_id: i32,
+  pub post_name: String,
   pub parent_id: Option<i32>,
   pub content: String,
   pub removed: bool,
@@ -100,6 +105,7 @@ pub struct CommentView {
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_published: chrono::NaiveDateTime,
   pub creator_avatar: Option<String>,
   pub score: i64,
   pub upvotes: i64,
@@ -295,6 +301,7 @@ table! {
     id -> Int4,
     creator_id -> Int4,
     post_id -> Int4,
+    post_name -> Varchar,
     parent_id -> Nullable<Int4>,
     content -> Text,
     removed -> Bool,
@@ -314,6 +321,7 @@ table! {
     creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
+    creator_published -> Timestamp,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
@@ -334,6 +342,7 @@ pub struct ReplyView {
   pub id: i32,
   pub creator_id: i32,
   pub post_id: i32,
+  pub post_name: String,
   pub parent_id: Option<i32>,
   pub content: String,
   pub removed: bool,
@@ -353,6 +362,7 @@ pub struct ReplyView {
   pub creator_local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
+  pub creator_published: chrono::NaiveDateTime,
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
@@ -455,11 +465,17 @@ impl<'a> ReplyQueryBuilder<'a> {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{comment::*, community::*, post::*, user::*},
+  use crate::{
+    comment::*,
+    comment_view::*,
+    community::*,
+    post::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    Crud,
+    Likeable,
     *,
   };
-  use crate::db::{establish_unpooled_connection, Crud, Likeable};
 
   #[test]
   fn test_crud() {
@@ -565,6 +581,7 @@ mod tests {
       content: "A test comment 32".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      post_name: inserted_post.name.to_owned(),
       community_id: inserted_community.id,
       community_name: inserted_community.name.to_owned(),
       parent_id: None,
@@ -576,6 +593,7 @@ mod tests {
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
+      creator_published: inserted_user.published,
       creator_avatar: None,
       score: 1,
       downvotes: 0,
@@ -598,6 +616,7 @@ mod tests {
       content: "A test comment 32".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      post_name: inserted_post.name.to_owned(),
       community_id: inserted_community.id,
       community_name: inserted_community.name.to_owned(),
       parent_id: None,
@@ -609,6 +628,7 @@ mod tests {
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
+      creator_published: inserted_user.published,
       creator_avatar: None,
       score: 1,
       downvotes: 0,
similarity index 98%
rename from server/src/db/community.rs
rename to server/lemmy_db/src/community.rs
index 461ba473ab4bc649cfaa3b948c18402b7ee5479e..607520803b7aa2621a2f10d2e997b4ceb57f1716 100644 (file)
@@ -1,6 +1,9 @@
 use crate::{
-  db::{Bannable, Crud, Followable, Joinable},
   schema::{community, community_follower, community_moderator, community_user_ban},
+  Bannable,
+  Crud,
+  Followable,
+  Joinable,
 };
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
@@ -232,8 +235,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
 
 #[cfg(test)]
 mod tests {
-  use super::{super::user::*, *};
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+  use crate::{community::*, tests::establish_unpooled_connection, user::*, ListingType, SortType};
 
   #[test]
   fn test_crud() {
similarity index 98%
rename from server/src/db/community_view.rs
rename to server/lemmy_db/src/community_view.rs
index 4ec839acf579598b94661af9b6e0761d378d0e15..5c6bd81a19590dc2c0dd62ae221616cc7a2cd983 100644 (file)
@@ -1,5 +1,5 @@
 use super::community_view::community_fast_view::BoxedQuery;
-use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
+use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
 use diesel::{pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -299,6 +299,7 @@ impl CommunityModeratorView {
     use super::community_view::community_moderator_view::dsl::*;
     community_moderator_view
       .filter(community_id.eq(from_community_id))
+      .order_by(published)
       .load::<Self>(conn)
   }
 
@@ -306,6 +307,7 @@ impl CommunityModeratorView {
     use super::community_view::community_moderator_view::dsl::*;
     community_moderator_view
       .filter(user_id.eq(from_user_id))
+      .order_by(published)
       .load::<Self>(conn)
   }
 }
similarity index 78%
rename from server/src/db/mod.rs
rename to server/lemmy_db/src/lib.rs
index da69f8dcdc88dcf925dda24edb93dc8aa5ffae0a..2eead841d7dc25f3e22170d1fdbf8416f5704731 100644 (file)
@@ -1,10 +1,22 @@
-use crate::settings::Settings;
+#[macro_use]
+pub extern crate diesel;
+#[macro_use]
+pub extern crate strum_macros;
+pub extern crate bcrypt;
+pub extern crate chrono;
+pub extern crate log;
+pub extern crate serde;
+pub extern crate serde_json;
+pub extern crate sha2;
+pub extern crate strum;
+
+use chrono::NaiveDateTime;
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
+use std::{env, env::VarError};
 
 pub mod activity;
 pub mod category;
-pub mod code_migrations;
 pub mod comment;
 pub mod comment_view;
 pub mod community;
@@ -16,6 +28,7 @@ pub mod post;
 pub mod post_view;
 pub mod private_message;
 pub mod private_message_view;
+pub mod schema;
 pub mod site;
 pub mod site_view;
 pub mod user;
@@ -111,9 +124,8 @@ impl<T> MaybeOptional<T> for Option<T> {
   }
 }
 
-pub fn establish_unpooled_connection() -> PgConnection {
-  let db_url = Settings::get().get_database_url();
-  PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
+pub fn get_database_url_from_env() -> Result<String, VarError> {
+  env::var("LEMMY_DATABASE_URL")
 }
 
 #[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
@@ -155,9 +167,28 @@ pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
   let offset = limit * (page - 1);
   (limit, offset)
 }
+
+pub fn naive_now() -> NaiveDateTime {
+  chrono::prelude::Utc::now().naive_utc()
+}
+
 #[cfg(test)]
 mod tests {
   use super::fuzzy_search;
+  use crate::get_database_url_from_env;
+  use diesel::{Connection, PgConnection};
+
+  pub fn establish_unpooled_connection() -> PgConnection {
+    let db_url = match get_database_url_from_env() {
+      Ok(url) => url,
+      Err(e) => panic!(
+        "Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
+        e
+      ),
+    };
+    PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
+  }
+
   #[test]
   fn test_fuzzy_search() {
     let test = "This is a fuzzy search";
similarity index 99%
rename from server/src/db/moderator.rs
rename to server/lemmy_db/src/moderator.rs
index 44b04ec630e23ba4226dcc9606123cc63a7ad6c5..f5d33d9672b9c04d228d7cb7813290d0f4dc7cda 100644 (file)
@@ -1,5 +1,4 @@
 use crate::{
-  db::Crud,
   schema::{
     mod_add,
     mod_add_community,
@@ -11,6 +10,7 @@ use crate::{
     mod_remove_post,
     mod_sticky_post,
   },
+  Crud,
 };
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
@@ -437,11 +437,16 @@ impl Crud<ModAddForm> for ModAdd {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{comment::*, community::*, post::*, user::*},
-    *,
+  use crate::{
+    comment::*,
+    community::*,
+    moderator::*,
+    post::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    ListingType,
+    SortType,
   };
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
 
   // use Crud;
   #[test]
similarity index 99%
rename from server/src/db/moderator_views.rs
rename to server/lemmy_db/src/moderator_views.rs
index f5b109fe6160f91437dfd147c41f96cea37e196d..024907c39dbe3f7cdfd81ea079c035dd03b35611 100644 (file)
@@ -1,4 +1,4 @@
-use crate::db::limit_and_offset;
+use crate::limit_and_offset;
 use diesel::{result::Error, *};
 use serde::{Deserialize, Serialize};
 
similarity index 98%
rename from server/src/db/password_reset_request.rs
rename to server/lemmy_db/src/password_reset_request.rs
index 4a071f0780fed79196a363cc63279fbccbdabfb9..a2692add863bdda3a3aac2fa3d5d8a61c0d400b0 100644 (file)
@@ -1,6 +1,6 @@
 use crate::{
-  db::Crud,
   schema::{password_reset_request, password_reset_request::dsl::*},
+  Crud,
 };
 use diesel::{dsl::*, result::Error, *};
 use sha2::{Digest, Sha256};
@@ -82,7 +82,7 @@ impl PasswordResetRequest {
 #[cfg(test)]
 mod tests {
   use super::{super::user::*, *};
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+  use crate::{tests::establish_unpooled_connection, ListingType, SortType};
 
   #[test]
   fn test_crud() {
similarity index 96%
rename from server/src/db/post.rs
rename to server/lemmy_db/src/post.rs
index 91c1dcbffc31be41a5bc4ec34d1cb4246de6a591..1525a675f169e18346ac2c8b800d0289a9e4d762 100644 (file)
@@ -1,8 +1,10 @@
 use crate::{
-  apub::{make_apub_endpoint, EndpointType},
-  db::{Crud, Likeable, Readable, Saveable},
   naive_now,
   schema::{post, post_like, post_read, post_saved},
+  Crud,
+  Likeable,
+  Readable,
+  Saveable,
 };
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
@@ -75,12 +77,11 @@ impl Post {
     post.filter(ap_id.eq(object_id)).first::<Self>(conn)
   }
 
-  pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+  pub fn update_ap_id(conn: &PgConnection, post_id: i32, apub_id: String) -> Result<Self, Error> {
     use crate::schema::post::dsl::*;
 
-    let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
     diesel::update(post.find(post_id))
-      .set(ap_id.eq(apid))
+      .set(ap_id.eq(apub_id))
       .get_result::<Self>(conn)
   }
 
@@ -241,11 +242,14 @@ impl Readable<PostReadForm> for PostRead {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{community::*, user::*},
-    *,
+  use crate::{
+    community::*,
+    post::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    ListingType,
+    SortType,
   };
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
 
   #[test]
   fn test_crud() {
similarity index 94%
rename from server/src/db/post_view.rs
rename to server/lemmy_db/src/post_view.rs
index 808cf28c4a2dab1b6d3b3133b659bef6a4a214c3..3e9f8737676891d4cb5a07ce4c69a7379479c1ce 100644 (file)
@@ -1,5 +1,5 @@
 use super::post_view::post_fast_view::BoxedQuery;
-use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
+use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
 use diesel::{dsl::*, pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -28,6 +28,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
@@ -75,6 +76,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
@@ -125,6 +127,7 @@ pub struct PostView {
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_published: chrono::NaiveDateTime,
   pub creator_avatar: Option<String>,
   pub banned: bool,
   pub banned_from_community: bool,
@@ -155,6 +158,7 @@ pub struct PostQueryBuilder<'a> {
   my_user_id: Option<i32>,
   for_creator_id: Option<i32>,
   for_community_id: Option<i32>,
+  for_community_name: Option<String>,
   search_term: Option<String>,
   url_search: Option<String>,
   show_nsfw: bool,
@@ -178,6 +182,7 @@ impl<'a> PostQueryBuilder<'a> {
       my_user_id: None,
       for_creator_id: None,
       for_community_id: None,
+      for_community_name: None,
       search_term: None,
       url_search: None,
       show_nsfw: true,
@@ -203,6 +208,11 @@ impl<'a> PostQueryBuilder<'a> {
     self
   }
 
+  pub fn for_community_name<T: MaybeOptional<String>>(mut self, for_community_name: T) -> Self {
+    self.for_community_name = for_community_name.get_optional();
+    self
+  }
+
   pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
     self.for_creator_id = for_creator_id.get_optional();
     self
@@ -262,6 +272,11 @@ impl<'a> PostQueryBuilder<'a> {
       query = query.then_order_by(stickied.desc());
     }
 
+    if let Some(for_community_name) = self.for_community_name {
+      query = query.filter(community_name.eq(for_community_name));
+      query = query.then_order_by(stickied.desc());
+    }
+
     if let Some(url_search) = self.url_search {
       query = query.filter(url.eq(url_search));
     }
@@ -364,11 +379,16 @@ impl PostView {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{community::*, post::*, user::*},
+  use crate::{
+    community::*,
+    post::*,
+    post_view::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    Crud,
+    Likeable,
     *,
   };
-  use crate::db::{establish_unpooled_connection, Crud, Likeable};
 
   #[test]
   fn test_crud() {
@@ -499,6 +519,7 @@ mod tests {
       body: None,
       creator_id: inserted_user.id,
       creator_name: user_name.to_owned(),
+      creator_published: inserted_user.published,
       creator_avatar: None,
       banned: false,
       banned_from_community: false,
@@ -548,6 +569,7 @@ mod tests {
       stickied: false,
       creator_id: inserted_user.id,
       creator_name: user_name,
+      creator_published: inserted_user.published,
       creator_avatar: None,
       banned: false,
       banned_from_community: false,
similarity index 92%
rename from server/src/db/private_message.rs
rename to server/lemmy_db/src/private_message.rs
index 9d362bbf1b235c702c61f6c13c8fb325d77b2c04..1c0b455f3cbbd6584c943e4e009eaf8b9be05023 100644 (file)
@@ -1,8 +1,4 @@
-use crate::{
-  apub::{make_apub_endpoint, EndpointType},
-  db::Crud,
-  schema::private_message,
-};
+use crate::{schema::private_message, Crud};
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -66,16 +62,15 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
 }
 
 impl PrivateMessage {
-  pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
+  pub fn update_ap_id(
+    conn: &PgConnection,
+    private_message_id: i32,
+    apub_id: String,
+  ) -> Result<Self, Error> {
     use crate::schema::private_message::dsl::*;
 
-    let apid = make_apub_endpoint(
-      EndpointType::PrivateMessage,
-      &private_message_id.to_string(),
-    )
-    .to_string();
     diesel::update(private_message.find(private_message_id))
-      .set(ap_id.eq(apid))
+      .set(ap_id.eq(apub_id))
       .get_result::<Self>(conn)
   }
 
@@ -89,8 +84,13 @@ impl PrivateMessage {
 
 #[cfg(test)]
 mod tests {
-  use super::{super::user::*, *};
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+  use crate::{
+    private_message::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    ListingType,
+    SortType,
+  };
 
   #[test]
   fn test_crud() {
similarity index 98%
rename from server/src/db/private_message_view.rs
rename to server/lemmy_db/src/private_message_view.rs
index 899a1084d81a824c230169c0f9da7f2cf8d783cf..dfb11c444c2bf0003b87fa75805c3508606025b5 100644 (file)
@@ -1,4 +1,4 @@
-use crate::db::{limit_and_offset, MaybeOptional};
+use crate::{limit_and_offset, MaybeOptional};
 use diesel::{pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
similarity index 98%
rename from server/src/schema.rs
rename to server/lemmy_db/src/schema.rs
index 0367c7506342657db405f43fe2541b26360dc937..9608fb7d423260a360c40cc4d95a1b701b784319 100644 (file)
@@ -47,6 +47,7 @@ table! {
         deleted -> Nullable<Bool>,
         ap_id -> Nullable<Varchar>,
         local -> Nullable<Bool>,
+        post_name -> Nullable<Varchar>,
         community_id -> Nullable<Int4>,
         community_actor_id -> Nullable<Varchar>,
         community_local -> Nullable<Bool>,
@@ -56,6 +57,7 @@ table! {
         creator_actor_id -> Nullable<Varchar>,
         creator_local -> Nullable<Bool>,
         creator_name -> Nullable<Varchar>,
+        creator_published -> Nullable<Timestamp>,
         creator_avatar -> Nullable<Text>,
         score -> Nullable<Int8>,
         upvotes -> Nullable<Int8>,
@@ -317,6 +319,7 @@ table! {
         creator_actor_id -> Nullable<Varchar>,
         creator_local -> Nullable<Bool>,
         creator_name -> Nullable<Varchar>,
+        creator_published -> Nullable<Timestamp>,
         creator_avatar -> Nullable<Text>,
         banned -> Nullable<Bool>,
         banned_from_community -> Nullable<Bool>,
similarity index 97%
rename from server/src/db/site.rs
rename to server/lemmy_db/src/site.rs
index c752bfe796a6a1c731a1534182566469a9d91888..066ae0b1a68f152d3f51f82db864ff9072dc02be 100644 (file)
@@ -1,4 +1,4 @@
-use crate::{db::Crud, schema::site};
+use crate::{schema::site, Crud};
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
similarity index 73%
rename from server/src/db/user.rs
rename to server/lemmy_db/src/user.rs
index 4ca0a04197e136040634a1d640f46bf4c041d511..556fc1a75d52b5c6680f87bef52354b8fd751a62 100644 (file)
@@ -1,14 +1,10 @@
 use crate::{
-  db::Crud,
-  is_email_regex,
   naive_now,
   schema::{user_, user_::dsl::*},
-  settings::Settings,
+  Crud,
 };
 use bcrypt::{hash, DEFAULT_COST};
 use diesel::{dsl::*, result::Error, *};
-use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
-use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
 #[table_name = "user_"]
@@ -131,90 +127,23 @@ impl User_ {
   }
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Claims {
-  pub id: i32,
-  pub username: String,
-  pub iss: String,
-  pub show_nsfw: bool,
-  pub theme: String,
-  pub default_sort_type: i16,
-  pub default_listing_type: i16,
-  pub lang: String,
-  pub avatar: Option<String>,
-  pub show_avatars: bool,
-}
-
-impl Claims {
-  pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
-    let v = Validation {
-      validate_exp: false,
-      ..Validation::default()
-    };
-    decode::<Claims>(
-      &jwt,
-      &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
-      &v,
-    )
-  }
-}
-
-type Jwt = String;
 impl User_ {
-  pub fn jwt(&self) -> Jwt {
-    let my_claims = Claims {
-      id: self.id,
-      username: self.name.to_owned(),
-      iss: Settings::get().hostname,
-      show_nsfw: self.show_nsfw,
-      theme: self.theme.to_owned(),
-      default_sort_type: self.default_sort_type,
-      default_listing_type: self.default_listing_type,
-      lang: self.lang.to_owned(),
-      avatar: self.avatar.to_owned(),
-      show_avatars: self.show_avatars.to_owned(),
-    };
-    encode(
-      &Header::default(),
-      &my_claims,
-      &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
-    )
-    .unwrap()
-  }
-
-  pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
+  pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
     user_.filter(name.eq(username)).first::<User_>(conn)
   }
 
-  pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
+  pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<User_, Error> {
     user_.filter(email.eq(from_email)).first::<User_>(conn)
   }
 
-  pub fn find_by_email_or_username(
-    conn: &PgConnection,
-    username_or_email: &str,
-  ) -> Result<Self, Error> {
-    if is_email_regex(username_or_email) {
-      User_::find_by_email(conn, username_or_email)
-    } else {
-      User_::find_by_username(conn, username_or_email)
-    }
-  }
-
-  pub fn get_profile_url(&self) -> String {
-    format!("https://{}/u/{}", Settings::get().hostname, self.name)
-  }
-
-  pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
-    let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
-    Self::read(&conn, claims.id)
+  pub fn get_profile_url(&self, hostname: &str) -> String {
+    format!("https://{}/u/{}", hostname, self.name)
   }
 }
 
 #[cfg(test)]
 mod tests {
-  use super::{User_, *};
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+  use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
 
   #[test]
   fn test_crud() {
similarity index 96%
rename from server/src/db/user_mention.rs
rename to server/lemmy_db/src/user_mention.rs
index 1d54fa988c7cdf63933d3feb18dd866ad131f2aa..9f23f4410c2434db011ba0e6e1aeb4c8d0f2bfba 100644 (file)
@@ -1,5 +1,5 @@
 use super::comment::Comment;
-use crate::{db::Crud, schema::user_mention};
+use crate::{schema::user_mention, Crud};
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -54,11 +54,16 @@ impl Crud<UserMentionForm> for UserMention {
 
 #[cfg(test)]
 mod tests {
-  use super::{
-    super::{comment::*, community::*, post::*, user::*},
-    *,
+  use crate::{
+    comment::*,
+    community::*,
+    post::*,
+    tests::establish_unpooled_connection,
+    user::*,
+    user_mention::*,
+    ListingType,
+    SortType,
   };
-  use crate::db::{establish_unpooled_connection, ListingType, SortType};
 
   #[test]
   fn test_crud() {
similarity index 97%
rename from server/src/db/user_mention_view.rs
rename to server/lemmy_db/src/user_mention_view.rs
index 59aefb200077759ba81fa2e4b44c3b5362314ade..359f166d67acee569563e70b8036c8cb0a95bffd 100644 (file)
@@ -1,4 +1,4 @@
-use crate::db::{limit_and_offset, MaybeOptional, SortType};
+use crate::{limit_and_offset, MaybeOptional, SortType};
 use diesel::{dsl::*, pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -11,6 +11,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     post_id -> Int4,
+    post_name -> Varchar,
     parent_id -> Nullable<Int4>,
     content -> Text,
     removed -> Bool,
@@ -47,6 +48,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     post_id -> Int4,
+    post_name -> Varchar,
     parent_id -> Nullable<Int4>,
     content -> Text,
     removed -> Bool,
@@ -86,6 +88,7 @@ pub struct UserMentionView {
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub post_id: i32,
+  pub post_name: String,
   pub parent_id: Option<i32>,
   pub content: String,
   pub removed: bool,
similarity index 96%
rename from server/src/db/user_view.rs
rename to server/lemmy_db/src/user_view.rs
index 490521721e8e44b4ec4008528f721f0cf5e365e2..f2ac47422492ecc9ad0a2bf7d918a333d2ea855c 100644 (file)
@@ -1,5 +1,5 @@
 use super::user_view::user_fast::BoxedQuery;
-use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
+use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
 use diesel::{dsl::*, pg::Pg, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -157,7 +157,10 @@ impl UserView {
 
   pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
     use super::user_view::user_fast::dsl::*;
-    user_fast.filter(admin.eq(true)).load::<Self>(conn)
+    user_fast
+      .filter(admin.eq(true))
+      .order_by(published)
+      .load::<Self>(conn)
   }
 
   pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
diff --git a/server/lemmy_utils/Cargo.toml b/server/lemmy_utils/Cargo.toml
new file mode 100644 (file)
index 0000000..fed22f5
--- /dev/null
@@ -0,0 +1,22 @@
+[package]
+name = "lemmy_utils"
+version = "0.1.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+regex = "1.3.5"
+config = { version = "0.10.1", default-features = false, features = ["hjson"] }
+chrono = { version = "0.4.7", features = ["serde"] }
+lettre = "0.9.3"
+lettre_email = "0.9.4"
+log = "0.4.0"
+itertools = "0.9.0"
+rand = "0.7.3"
+serde = { version = "1.0.105", features = ["derive"] }
+serde_json = { version = "1.0.52", features = ["preserve_order"]}
+comrak = "0.7"
+lazy_static = "1.3.0"
+openssl = "0.10"
+url = { version = "2.1.1", features = ["serde"] }
\ No newline at end of file
diff --git a/server/lemmy_utils/src/lib.rs b/server/lemmy_utils/src/lib.rs
new file mode 100644 (file)
index 0000000..d88335e
--- /dev/null
@@ -0,0 +1,337 @@
+#[macro_use]
+pub extern crate lazy_static;
+pub extern crate comrak;
+pub extern crate lettre;
+pub extern crate lettre_email;
+pub extern crate openssl;
+pub extern crate rand;
+pub extern crate regex;
+pub extern crate serde_json;
+pub extern crate url;
+
+pub mod settings;
+
+use crate::settings::Settings;
+use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
+use itertools::Itertools;
+use lettre::{
+  smtp::{
+    authentication::{Credentials, Mechanism},
+    extension::ClientId,
+    ConnectionReuseParameters,
+  },
+  ClientSecurity,
+  SmtpClient,
+  Transport,
+};
+use lettre_email::Email;
+use openssl::{pkey::PKey, rsa::Rsa};
+use rand::{distributions::Alphanumeric, thread_rng, Rng};
+use regex::{Regex, RegexBuilder};
+use std::io::{Error, ErrorKind};
+use url::Url;
+
+pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
+  DateTime::<Utc>::from_utc(ndt, Utc)
+}
+
+pub fn naive_from_unix(time: i64) -> NaiveDateTime {
+  NaiveDateTime::from_timestamp(time, 0)
+}
+
+pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
+  let now = Local::now();
+  DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
+}
+
+pub fn is_email_regex(test: &str) -> bool {
+  EMAIL_REGEX.is_match(test)
+}
+
+pub fn remove_slurs(test: &str) -> String {
+  SLUR_REGEX.replace_all(test, "*removed*").to_string()
+}
+
+pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
+  let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
+
+  // Unique
+  matches.sort_unstable();
+  matches.dedup();
+
+  if matches.is_empty() {
+    Ok(())
+  } else {
+    Err(matches)
+  }
+}
+
+pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
+  let start = "No slurs - ";
+  let combined = &slurs.join(", ");
+  [start, combined].concat()
+}
+
+pub fn generate_random_string() -> String {
+  thread_rng().sample_iter(&Alphanumeric).take(30).collect()
+}
+
+pub fn send_email(
+  subject: &str,
+  to_email: &str,
+  to_username: &str,
+  html: &str,
+) -> Result<(), String> {
+  let email_config = Settings::get().email.ok_or("no_email_setup")?;
+
+  let email = Email::builder()
+    .to((to_email, to_username))
+    .from(email_config.smtp_from_address.to_owned())
+    .subject(subject)
+    .html(html)
+    .build()
+    .unwrap();
+
+  let mailer = if email_config.use_tls {
+    SmtpClient::new_simple(&email_config.smtp_server).unwrap()
+  } else {
+    SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
+  }
+  .hello_name(ClientId::Domain(Settings::get().hostname))
+  .smtp_utf8(true)
+  .authentication_mechanism(Mechanism::Plain)
+  .connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
+  let mailer = if let (Some(login), Some(password)) =
+    (&email_config.smtp_login, &email_config.smtp_password)
+  {
+    mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
+  } else {
+    mailer
+  };
+
+  let mut transport = mailer.transport();
+  let result = transport.send(email.into());
+  transport.close();
+
+  match result {
+    Ok(_) => Ok(()),
+    Err(e) => Err(e.to_string()),
+  }
+}
+
+pub fn markdown_to_html(text: &str) -> String {
+  comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
+}
+
+// TODO nothing is done with community / group webfingers yet, so just ignore those for now
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct MentionData {
+  pub name: String,
+  pub domain: String,
+}
+
+impl MentionData {
+  pub fn is_local(&self) -> bool {
+    Settings::get().hostname.eq(&self.domain)
+  }
+  pub fn full_name(&self) -> String {
+    format!("@{}@{}", &self.name, &self.domain)
+  }
+}
+
+pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
+  let mut out: Vec<MentionData> = Vec::new();
+  for caps in MENTIONS_REGEX.captures_iter(text) {
+    out.push(MentionData {
+      name: caps["name"].to_string(),
+      domain: caps["domain"].to_string(),
+    });
+  }
+  out.into_iter().unique().collect()
+}
+
+pub fn is_valid_username(name: &str) -> bool {
+  VALID_USERNAME_REGEX.is_match(name)
+}
+
+pub fn is_valid_community_name(name: &str) -> bool {
+  VALID_COMMUNITY_NAME_REGEX.is_match(name)
+}
+
+pub fn is_valid_post_title(title: &str) -> bool {
+  VALID_POST_TITLE_REGEX.is_match(title)
+}
+
+#[cfg(test)]
+mod tests {
+  use crate::{
+    is_email_regex,
+    is_valid_community_name,
+    is_valid_post_title,
+    is_valid_username,
+    remove_slurs,
+    scrape_text_for_mentions,
+    slur_check,
+    slurs_vec_to_str,
+  };
+
+  #[test]
+  fn test_mentions_regex() {
+    let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
+    let mentions = scrape_text_for_mentions(text);
+
+    assert_eq!(mentions[0].name, "tedu".to_string());
+    assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
+    assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
+  }
+
+  #[test]
+  fn test_email() {
+    assert!(is_email_regex("gush@gmail.com"));
+    assert!(!is_email_regex("nada_neutho"));
+  }
+
+  #[test]
+  fn test_valid_register_username() {
+    assert!(is_valid_username("Hello_98"));
+    assert!(is_valid_username("ten"));
+    assert!(!is_valid_username("Hello-98"));
+    assert!(!is_valid_username("a"));
+    assert!(!is_valid_username(""));
+  }
+
+  #[test]
+  fn test_valid_community_name() {
+    assert!(is_valid_community_name("example"));
+    assert!(is_valid_community_name("example_community"));
+    assert!(!is_valid_community_name("Example"));
+    assert!(!is_valid_community_name("Ex"));
+    assert!(!is_valid_community_name(""));
+  }
+
+  #[test]
+  fn test_valid_post_title() {
+    assert!(is_valid_post_title("Post Title"));
+    assert!(is_valid_post_title("   POST TITLE 😃😃😃😃😃"));
+    assert!(!is_valid_post_title("\n \n \n \n                  ")); // tabs/spaces/newlines
+  }
+
+  #[test]
+  fn test_slur_filter() {
+    let test =
+      "coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
+    let slur_free = "No slurs here";
+    assert_eq!(
+      remove_slurs(&test),
+      "*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
+        .to_string()
+    );
+
+    let has_slurs_vec = vec![
+      "Niggerz",
+      "coons",
+      "dindu",
+      "ladyboy",
+      "retardeds",
+      "tranny",
+    ];
+    let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
+
+    assert_eq!(slur_check(test), Err(has_slurs_vec));
+    assert_eq!(slur_check(slur_free), Ok(()));
+    if let Err(slur_vec) = slur_check(test) {
+      assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
+    }
+  }
+
+  // These helped with testing
+  // #[test]
+  // fn test_send_email() {
+  //  let result =  send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
+  //   assert!(result.is_ok());
+  // }
+}
+
+lazy_static! {
+  static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
+  static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
+  static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
+  // TODO keep this old one, it didn't work with port well tho
+  // static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
+  static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
+  static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
+  static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
+  static ref VALID_POST_TITLE_REGEX: Regex = Regex::new(r".*\S.*").unwrap();
+  pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
+    "^group:([a-z0-9_]{{3, 20}})@{}$",
+    Settings::get().hostname
+  ))
+  .unwrap();
+  pub static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
+    "^acct:([a-z0-9_]{{3, 20}})@{}$",
+    Settings::get().hostname
+  ))
+  .unwrap();
+  pub static ref CACHE_CONTROL_REGEX: Regex =
+    Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
+}
+
+pub struct Keypair {
+  pub private_key: String,
+  pub public_key: String,
+}
+
+/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
+pub fn generate_actor_keypair() -> Result<Keypair, Error> {
+  let rsa = Rsa::generate(2048)?;
+  let pkey = PKey::from_rsa(rsa)?;
+  let public_key = pkey.public_key_to_pem()?;
+  let private_key = pkey.private_key_to_pem_pkcs8()?;
+  let key_to_string = |key| match String::from_utf8(key) {
+    Ok(s) => Ok(s),
+    Err(e) => Err(Error::new(
+      ErrorKind::Other,
+      format!("Failed converting key to string: {}", e),
+    )),
+  };
+  Ok(Keypair {
+    private_key: key_to_string(private_key)?,
+    public_key: key_to_string(public_key)?,
+  })
+}
+
+pub enum EndpointType {
+  Community,
+  User,
+  Post,
+  Comment,
+  PrivateMessage,
+}
+
+pub fn get_apub_protocol_string() -> &'static str {
+  if Settings::get().federation.tls_enabled {
+    "https"
+  } else {
+    "http"
+  }
+}
+
+/// Generates the ActivityPub ID for a given object type and ID.
+pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
+  let point = match endpoint_type {
+    EndpointType::Community => "c",
+    EndpointType::User => "u",
+    EndpointType::Post => "post",
+    EndpointType::Comment => "comment",
+    EndpointType::PrivateMessage => "private_message",
+  };
+
+  Url::parse(&format!(
+    "{}://{}/{}/{}",
+    get_apub_protocol_string(),
+    Settings::get().hostname,
+    point,
+    name
+  ))
+  .unwrap()
+}
similarity index 79%
rename from server/src/settings.rs
rename to server/lemmy_utils/src/settings.rs
index 12ffaceabf0a3eb39b520b6ffddca4fdc1b1ef99..097063b6a55bcdc168737b5f94237cdd053a7525 100644 (file)
@@ -1,7 +1,6 @@
-use crate::LemmyError;
 use config::{Config, ConfigError, Environment, File};
 use serde::Deserialize;
-use std::{env, fs, net::IpAddr, sync::RwLock};
+use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
 
 static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
 static CONFIG_FILE: &str = "config/config.hjson";
@@ -76,12 +75,15 @@ impl Settings {
   /// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten
   /// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are
   /// added to the config.
+  ///
+  /// Note: The env var `LEMMY_DATABASE_URL` is parsed in
+  /// `server/lemmy_db/src/lib.rs::get_database_url_from_env()`
   fn init() -> Result<Self, ConfigError> {
     let mut s = Config::new();
 
     s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
 
-    s.merge(File::with_name(CONFIG_FILE).required(false))?;
+    s.merge(File::with_name(&Self::get_config_location()).required(false))?;
 
     // Add in settings from the environment (with a prefix of LEMMY)
     // Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
@@ -98,32 +100,31 @@ impl Settings {
     SETTINGS.read().unwrap().to_owned()
   }
 
-  /// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
-  /// otherwise the connection url is generated from the config.
   pub fn get_database_url(&self) -> String {
-    match env::var("LEMMY_DATABASE_URL") {
-      Ok(url) => url,
-      Err(_) => format!(
-        "postgres://{}:{}@{}:{}/{}",
-        self.database.user,
-        self.database.password,
-        self.database.host,
-        self.database.port,
-        self.database.database
-      ),
-    }
+    format!(
+      "postgres://{}:{}@{}:{}/{}",
+      self.database.user,
+      self.database.password,
+      self.database.host,
+      self.database.port,
+      self.database.database
+    )
   }
 
   pub fn api_endpoint(&self) -> String {
     format!("{}/api/v1", self.hostname)
   }
 
-  pub fn read_config_file() -> Result<String, LemmyError> {
-    Ok(fs::read_to_string(CONFIG_FILE)?)
+  pub fn get_config_location() -> String {
+    env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
   }
 
-  pub fn save_config_file(data: &str) -> Result<String, LemmyError> {
-    fs::write(CONFIG_FILE, data)?;
+  pub fn read_config_file() -> Result<String, Error> {
+    fs::read_to_string(Self::get_config_location())
+  }
+
+  pub fn save_config_file(data: &str) -> Result<String, Error> {
+    fs::write(Self::get_config_location(), data)?;
 
     // Reload the new settings
     // From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
diff --git a/server/migrations/2020-07-08-202609_add_creator_published/down.sql b/server/migrations/2020-07-08-202609_add_creator_published/down.sql
new file mode 100644 (file)
index 0000000..b8e4452
--- /dev/null
@@ -0,0 +1,388 @@
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+create view comment_aggregates_view as 
+select
+       ct.*,
+       -- community details
+       p.community_id,
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id 
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all 
+
+select 
+    cav.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all 
+
+select 
+    cav.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select 
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as 
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select 
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id, 
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as 
+with closereply as (
+    select 
+    c2.id, 
+    c2.creator_id as sender_id, 
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
+
+-- add creator_published to the post view
+drop view post_fast_view;
+drop table post_aggregates_fast;
+drop view post_view;
+drop view post_aggregates_view;
+
+create view post_aggregates_view as
+select
+       p.*,
+       -- creator details
+       u.actor_id as creator_actor_id,
+       u."local" as creator_local,
+       u."name" as creator_name,
+       u.avatar as creator_avatar,
+  u.banned as banned,
+  cb.id::bool as banned_from_community,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       c.removed as community_removed,
+       c.deleted as community_deleted,
+       c.nsfw as community_nsfw,
+       -- post score data/comment count
+       coalesce(ct.comments, 0) as number_of_comments,
+       coalesce(pl.score, 0) as score,
+       coalesce(pl.upvotes, 0) as upvotes,
+       coalesce(pl.downvotes, 0) as downvotes,
+       hot_rank(
+               coalesce(pl.score , 0), (
+                       case
+                               when (p.published < ('now'::timestamp - '1 month'::interval))
+                               then p.published
+                               else greatest(ct.recent_comment_time, p.published)
+                       end
+               )
+       ) as hot_rank,
+       (
+               case
+                       when (p.published < ('now'::timestamp - '1 month'::interval))
+                       then p.published
+                       else greatest(ct.recent_comment_time, p.published)
+               end
+       ) as newest_activity_time
+from post p
+left join user_ u on p.creator_id = u.id
+left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
+left join community c on p.community_id = c.id
+left join (
+       select
+               post_id,
+               count(*) as comments,
+               max(published) as recent_comment_time
+       from comment
+       group by post_id
+) ct on ct.post_id = p.id
+left join (
+       select
+               post_id,
+               sum(score) as score,
+               sum(score) filter (where score = 1) as upvotes,
+               -sum(score) filter (where score = -1) as downvotes
+       from post_like
+       group by post_id
+) pl on pl.post_id = p.id
+order by p.id;
+
+create view post_view as
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_view pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_view pav;
+
+create table post_aggregates_fast as select * from post_aggregates_view;
+alter table post_aggregates_fast add primary key (id);
+
+create view post_fast_view as 
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_fast pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_fast pav;
\ No newline at end of file
diff --git a/server/migrations/2020-07-08-202609_add_creator_published/up.sql b/server/migrations/2020-07-08-202609_add_creator_published/up.sql
new file mode 100644 (file)
index 0000000..1f2b59e
--- /dev/null
@@ -0,0 +1,390 @@
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+create view comment_aggregates_view as 
+select
+       ct.*,
+       -- community details
+       p.community_id,
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id 
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all 
+
+select 
+    cav.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all 
+
+select 
+    cav.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select 
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as 
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select 
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id, 
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as 
+with closereply as (
+    select 
+    c2.id, 
+    c2.creator_id as sender_id, 
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
+
+-- add creator_published to the post view
+drop view post_fast_view;
+drop table post_aggregates_fast;
+drop view post_view;
+drop view post_aggregates_view;
+
+create view post_aggregates_view as
+select
+       p.*,
+       -- creator details
+       u.actor_id as creator_actor_id,
+       u."local" as creator_local,
+       u."name" as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+  u.banned as banned,
+  cb.id::bool as banned_from_community,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       c.removed as community_removed,
+       c.deleted as community_deleted,
+       c.nsfw as community_nsfw,
+       -- post score data/comment count
+       coalesce(ct.comments, 0) as number_of_comments,
+       coalesce(pl.score, 0) as score,
+       coalesce(pl.upvotes, 0) as upvotes,
+       coalesce(pl.downvotes, 0) as downvotes,
+       hot_rank(
+               coalesce(pl.score , 0), (
+                       case
+                               when (p.published < ('now'::timestamp - '1 month'::interval))
+                               then p.published
+                               else greatest(ct.recent_comment_time, p.published)
+                       end
+               )
+       ) as hot_rank,
+       (
+               case
+                       when (p.published < ('now'::timestamp - '1 month'::interval))
+                       then p.published
+                       else greatest(ct.recent_comment_time, p.published)
+               end
+       ) as newest_activity_time
+from post p
+left join user_ u on p.creator_id = u.id
+left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
+left join community c on p.community_id = c.id
+left join (
+       select
+               post_id,
+               count(*) as comments,
+               max(published) as recent_comment_time
+       from comment
+       group by post_id
+) ct on ct.post_id = p.id
+left join (
+       select
+               post_id,
+               sum(score) as score,
+               sum(score) filter (where score = 1) as upvotes,
+               -sum(score) filter (where score = -1) as downvotes
+       from post_like
+       group by post_id
+) pl on pl.post_id = p.id
+order by p.id;
+
+create view post_view as
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_view pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_view pav;
+
+create table post_aggregates_fast as select * from post_aggregates_view;
+alter table post_aggregates_fast add primary key (id);
+
+create view post_fast_view as 
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_fast pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_fast pav;
\ No newline at end of file
diff --git a/server/migrations/2020-07-12-100442_add_post_title_to_comments_view/down.sql b/server/migrations/2020-07-12-100442_add_post_title_to_comments_view/down.sql
new file mode 100644 (file)
index 0000000..b7c9d51
--- /dev/null
@@ -0,0 +1,249 @@
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+create view comment_aggregates_view as
+select
+       ct.*,
+       -- community details
+       p.community_id,
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id,
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as
+with closereply as (
+    select
+    c2.id,
+    c2.creator_id as sender_id,
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
\ No newline at end of file
diff --git a/server/migrations/2020-07-12-100442_add_post_title_to_comments_view/up.sql b/server/migrations/2020-07-12-100442_add_post_title_to_comments_view/up.sql
new file mode 100644 (file)
index 0000000..4cfa7ed
--- /dev/null
@@ -0,0 +1,254 @@
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+create view comment_aggregates_view as
+select
+       ct.*,
+       -- post details
+       p."name" as post_name,
+       p.community_id,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.post_name,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id,
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as
+with closereply as (
+    select
+    c2.id,
+    c2.creator_id as sender_id,
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
\ No newline at end of file
diff --git a/server/src/api/claims.rs b/server/src/api/claims.rs
new file mode 100644 (file)
index 0000000..eec9d1a
--- /dev/null
@@ -0,0 +1,73 @@
+use diesel::{result::Error, PgConnection};
+use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
+use lemmy_db::{user::User_, Crud};
+use lemmy_utils::{is_email_regex, settings::Settings};
+use serde::{Deserialize, Serialize};
+
+type Jwt = String;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Claims {
+  pub id: i32,
+  pub username: String,
+  pub iss: String,
+  pub show_nsfw: bool,
+  pub theme: String,
+  pub default_sort_type: i16,
+  pub default_listing_type: i16,
+  pub lang: String,
+  pub avatar: Option<String>,
+  pub show_avatars: bool,
+}
+
+impl Claims {
+  pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
+    let v = Validation {
+      validate_exp: false,
+      ..Validation::default()
+    };
+    decode::<Claims>(
+      &jwt,
+      &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
+      &v,
+    )
+  }
+
+  pub fn jwt(user: User_, hostname: String) -> Jwt {
+    let my_claims = Claims {
+      id: user.id,
+      username: user.name.to_owned(),
+      iss: hostname,
+      show_nsfw: user.show_nsfw,
+      theme: user.theme.to_owned(),
+      default_sort_type: user.default_sort_type,
+      default_listing_type: user.default_listing_type,
+      lang: user.lang.to_owned(),
+      avatar: user.avatar.to_owned(),
+      show_avatars: user.show_avatars.to_owned(),
+    };
+    encode(
+      &Header::default(),
+      &my_claims,
+      &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
+    )
+    .unwrap()
+  }
+
+  // TODO: move these into user?
+  pub fn find_by_email_or_username(
+    conn: &PgConnection,
+    username_or_email: &str,
+  ) -> Result<User_, Error> {
+    if is_email_regex(username_or_email) {
+      User_::find_by_email(conn, username_or_email)
+    } else {
+      User_::find_by_username(conn, username_or_email)
+    }
+  }
+
+  pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
+    let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
+    User_::read(&conn, claims.id)
+  }
+}
index c7406b370035ef72560e6311850b20bb3b015a12..f8bdf5d5b17f3f708de2b72a05373ea5cdf8036b 100644 (file)
@@ -1,28 +1,7 @@
 use crate::{
-  api::{APIError, Oper, Perform},
+  api::{claims::Claims, APIError, Oper, Perform},
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
-  db::{
-    comment::*,
-    comment_view::*,
-    community_view::*,
-    moderator::*,
-    post::*,
-    site_view::*,
-    user::*,
-    user_mention::*,
-    user_view::*,
-    Crud,
-    Likeable,
-    ListingType,
-    Saveable,
-    SortType,
-  },
-  naive_now,
-  remove_slurs,
-  scrape_text_for_mentions,
-  send_email,
-  settings::Settings,
   websocket::{
     server::{JoinCommunityRoom, SendComment},
     UserOperation,
@@ -30,6 +9,31 @@ use crate::{
   },
   DbPool,
   LemmyError,
+};
+use lemmy_db::{
+  comment::*,
+  comment_view::*,
+  community_view::*,
+  moderator::*,
+  naive_now,
+  post::*,
+  site_view::*,
+  user::*,
+  user_mention::*,
+  user_view::*,
+  Crud,
+  Likeable,
+  ListingType,
+  Saveable,
+  SortType,
+};
+use lemmy_utils::{
+  make_apub_endpoint,
+  remove_slurs,
+  scrape_text_for_mentions,
+  send_email,
+  settings::Settings,
+  EndpointType,
   MentionData,
 };
 use log::error;
@@ -155,7 +159,9 @@ impl Perform for Oper<CreateComment> {
 
     let inserted_comment_id = inserted_comment.id;
     let updated_comment: Comment = match blocking(pool, move |conn| {
-      Comment::update_ap_id(&conn, inserted_comment_id)
+      let apub_id =
+        make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
+      Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
     })
     .await?
     {
@@ -237,28 +243,28 @@ impl Perform for Oper<EditComment> {
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
 
+    let mut editors: Vec<i32> = vec![orig_comment.creator_id];
+    let mut moderators: Vec<i32> = vec![];
+
+    let community_id = orig_comment.community_id;
+    moderators.append(
+      &mut blocking(pool, move |conn| {
+        CommunityModeratorView::for_community(&conn, community_id)
+          .map(|v| v.into_iter().map(|m| m.user_id).collect())
+      })
+      .await??,
+    );
+    moderators.append(
+      &mut blocking(pool, move |conn| {
+        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+      })
+      .await??,
+    );
+
+    editors.extend(&moderators);
     // You are allowed to mark the comment as read even if you're banned.
     if data.read.is_none() {
       // Verify its the creator or a mod, or an admin
-      let mut editors: Vec<i32> = vec![data.creator_id];
-      let community_id = orig_comment.community_id;
-      editors.append(
-        &mut blocking(pool, move |conn| {
-          Ok(
-            CommunityModeratorView::for_community(&conn, community_id)?
-              .into_iter()
-              .map(|m| m.user_id)
-              .collect(),
-          ) as Result<_, LemmyError>
-        })
-        .await??,
-      );
-      editors.append(
-        &mut blocking(pool, move |conn| {
-          Ok(UserView::admins(conn)?.into_iter().map(|a| a.id).collect()) as Result<_, LemmyError>
-        })
-        .await??,
-      );
 
       if !editors.contains(&user_id) {
         return Err(APIError::err("no_comment_edit_allowed").into());
@@ -276,6 +282,25 @@ impl Perform for Oper<EditComment> {
       if user.banned {
         return Err(APIError::err("site_ban").into());
       }
+    } else {
+      // check that user can mark as read
+      let parent_id = orig_comment.parent_id;
+      match parent_id {
+        Some(pid) => {
+          let parent_comment =
+            blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
+          if user_id != parent_comment.creator_id {
+            return Err(APIError::err("no_comment_edit_allowed").into());
+          }
+        }
+        None => {
+          let parent_post_id = orig_comment.post_id;
+          let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
+          if user_id != parent_post.creator_id {
+            return Err(APIError::err("no_comment_edit_allowed").into());
+          }
+        }
+      }
     }
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
@@ -283,22 +308,45 @@ impl Perform for Oper<EditComment> {
     let edit_id = data.edit_id;
     let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
 
-    let comment_form = CommentForm {
-      content: content_slurs_removed,
-      parent_id: data.parent_id,
-      post_id: data.post_id,
-      creator_id: data.creator_id,
-      removed: data.removed.to_owned(),
-      deleted: data.deleted.to_owned(),
-      read: data.read.to_owned(),
-      published: None,
-      updated: if data.read.is_some() {
-        orig_comment.updated
+    let comment_form = {
+      if data.read.is_none() {
+        // the ban etc checks should been made and have passed
+        // the comment can be properly edited
+        let post_removed = if moderators.contains(&user_id) {
+          data.removed
+        } else {
+          Some(read_comment.removed)
+        };
+
+        CommentForm {
+          content: content_slurs_removed,
+          parent_id: read_comment.parent_id,
+          post_id: read_comment.post_id,
+          creator_id: read_comment.creator_id,
+          removed: post_removed.to_owned(),
+          deleted: data.deleted.to_owned(),
+          read: Some(read_comment.read),
+          published: None,
+          updated: Some(naive_now()),
+          ap_id: read_comment.ap_id,
+          local: read_comment.local,
+        }
       } else {
-        Some(naive_now())
-      },
-      ap_id: read_comment.ap_id,
-      local: read_comment.local,
+        // the only field that can be updated it the read field
+        CommentForm {
+          content: read_comment.content,
+          parent_id: read_comment.parent_id,
+          post_id: read_comment.post_id,
+          creator_id: read_comment.creator_id,
+          removed: Some(read_comment.removed).to_owned(),
+          deleted: Some(read_comment.deleted).to_owned(),
+          read: data.read.to_owned(),
+          published: None,
+          updated: orig_comment.updated,
+          ap_id: read_comment.ap_id,
+          local: read_comment.local,
+        }
+      }
     };
 
     let edit_id = data.edit_id;
@@ -312,30 +360,47 @@ impl Perform for Oper<EditComment> {
       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
-    if let Some(deleted) = data.deleted.to_owned() {
-      if deleted {
-        updated_comment
-          .send_delete(&user, &self.client, pool)
-          .await?;
+    if data.read.is_none() {
+      if let Some(deleted) = data.deleted.to_owned() {
+        if deleted {
+          updated_comment
+            .send_delete(&user, &self.client, pool)
+            .await?;
+        } else {
+          updated_comment
+            .send_undo_delete(&user, &self.client, pool)
+            .await?;
+        }
+      } else if let Some(removed) = data.removed.to_owned() {
+        if moderators.contains(&user_id) {
+          if removed {
+            updated_comment
+              .send_remove(&user, &self.client, pool)
+              .await?;
+          } else {
+            updated_comment
+              .send_undo_remove(&user, &self.client, pool)
+              .await?;
+          }
+        }
       } else {
         updated_comment
-          .send_undo_delete(&user, &self.client, pool)
+          .send_update(&user, &self.client, pool)
           .await?;
       }
-    } else if let Some(removed) = data.removed.to_owned() {
-      if removed {
-        updated_comment
-          .send_remove(&user, &self.client, pool)
-          .await?;
-      } else {
-        updated_comment
-          .send_undo_remove(&user, &self.client, pool)
-          .await?;
+
+      // Mod tables
+      if moderators.contains(&user_id) {
+        if let Some(removed) = data.removed.to_owned() {
+          let form = ModRemoveCommentForm {
+            mod_user_id: user_id,
+            comment_id: data.edit_id,
+            removed: Some(removed),
+            reason: data.reason.to_owned(),
+          };
+          blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
+        }
       }
-    } else {
-      updated_comment
-        .send_update(&user, &self.client, pool)
-        .await?;
     }
 
     let post_id = data.post_id;
@@ -344,17 +409,6 @@ impl Perform for Oper<EditComment> {
     let mentions = scrape_text_for_mentions(&comment_form.content);
     let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
 
-    // Mod tables
-    if let Some(removed) = data.removed.to_owned() {
-      let form = ModRemoveCommentForm {
-        mod_user_id: user_id,
-        comment_id: data.edit_id,
-        removed: Some(removed),
-        reason: data.reason.to_owned(),
-      };
-      blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
-    }
-
     let edit_id = data.edit_id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
index 02071c577254329ad98982a3cb26333bc8ed0bbe..e5063e0ff0af6093704387674a6d36f235ee6891 100644 (file)
@@ -1,26 +1,24 @@
 use super::*;
 use crate::{
-  api::{APIError, Oper, Perform},
-  apub::{
-    extensions::signatures::generate_actor_keypair,
-    make_apub_endpoint,
-    ActorType,
-    EndpointType,
-  },
+  api::{claims::Claims, APIError, Oper, Perform},
+  apub::ActorType,
   blocking,
-  db::{Bannable, Crud, Followable, Joinable, SortType},
-  is_valid_community_name,
-  naive_from_unix,
-  naive_now,
-  slur_check,
-  slurs_vec_to_str,
   websocket::{
     server::{JoinCommunityRoom, SendCommunityRoomMessage},
     UserOperation,
     WebsocketInfo,
   },
   DbPool,
-  LemmyError,
+};
+use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
+use lemmy_utils::{
+  generate_actor_keypair,
+  is_valid_community_name,
+  make_apub_endpoint,
+  naive_from_unix,
+  slur_check,
+  slurs_vec_to_str,
+  EndpointType,
 };
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
@@ -394,7 +392,7 @@ impl Perform for Oper<EditCommunity> {
       title: data.title.to_owned(),
       description: data.description.to_owned(),
       category_id: data.category_id.to_owned(),
-      creator_id: user_id,
+      creator_id: read_community.creator_id,
       removed: data.removed.to_owned(),
       deleted: data.deleted.to_owned(),
       nsfw: data.nsfw,
@@ -654,6 +652,28 @@ impl Perform for Oper<BanFromCommunity> {
 
     let user_id = claims.id;
 
+    let mut community_moderators: Vec<i32> = vec![];
+
+    let community_id = data.community_id;
+
+    community_moderators.append(
+      &mut blocking(pool, move |conn| {
+        CommunityModeratorView::for_community(&conn, community_id)
+          .map(|v| v.into_iter().map(|m| m.user_id).collect())
+      })
+      .await??,
+    );
+    community_moderators.append(
+      &mut blocking(pool, move |conn| {
+        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+      })
+      .await??,
+    );
+
+    if !community_moderators.contains(&user_id) {
+      return Err(APIError::err("couldnt_update_community").into());
+    }
+
     let community_user_ban_form = CommunityUserBanForm {
       community_id: data.community_id,
       user_id: data.user_id,
@@ -731,6 +751,28 @@ impl Perform for Oper<AddModToCommunity> {
       user_id: data.user_id,
     };
 
+    let mut community_moderators: Vec<i32> = vec![];
+
+    let community_id = data.community_id;
+
+    community_moderators.append(
+      &mut blocking(pool, move |conn| {
+        CommunityModeratorView::for_community(&conn, community_id)
+          .map(|v| v.into_iter().map(|m| m.user_id).collect())
+      })
+      .await??,
+    );
+    community_moderators.append(
+      &mut blocking(pool, move |conn| {
+        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+      })
+      .await??,
+    );
+
+    if !community_moderators.contains(&user_id) {
+      return Err(APIError::err("couldnt_update_community").into());
+    }
+
     if data.added {
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
       if blocking(pool, join).await?.is_err() {
index 6df9909c58d7ae98b1a1ca84092ed7caeb4bed87..bb65815ad931f80f64ec02af2df34262f79356bb 100644 (file)
@@ -1,11 +1,8 @@
-use crate::{
-  db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
-  websocket::WebsocketInfo,
-  DbPool,
-  LemmyError,
-};
+use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
 use actix_web::client::Client;
+use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
 
+pub mod claims;
 pub mod comment;
 pub mod community;
 pub mod post;
index 840f1530569bab132244f5c2ac971fcb0c049880..b9518f0e956f0bad5ba345bf5cb3e0c9c31ddd4d 100644 (file)
@@ -1,27 +1,8 @@
 use crate::{
-  api::{APIError, Oper, Perform},
+  api::{claims::Claims, APIError, Oper, Perform},
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
-  db::{
-    comment_view::*,
-    community_view::*,
-    moderator::*,
-    post::*,
-    post_view::*,
-    site::*,
-    site_view::*,
-    user::*,
-    user_view::*,
-    Crud,
-    Likeable,
-    ListingType,
-    Saveable,
-    SortType,
-  },
   fetch_iframely_and_pictrs_data,
-  naive_now,
-  slur_check,
-  slurs_vec_to_str,
   websocket::{
     server::{JoinCommunityRoom, JoinPostRoom, SendPost},
     UserOperation,
@@ -30,6 +11,30 @@ use crate::{
   DbPool,
   LemmyError,
 };
+use lemmy_db::{
+  comment_view::*,
+  community_view::*,
+  moderator::*,
+  naive_now,
+  post::*,
+  post_view::*,
+  site::*,
+  site_view::*,
+  user::*,
+  user_view::*,
+  Crud,
+  Likeable,
+  ListingType,
+  Saveable,
+  SortType,
+};
+use lemmy_utils::{
+  is_valid_post_title,
+  make_apub_endpoint,
+  slur_check,
+  slurs_vec_to_str,
+  EndpointType,
+};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
@@ -71,6 +76,7 @@ pub struct GetPosts {
   page: Option<i64>,
   limit: Option<i64>,
   pub community_id: Option<i32>,
+  pub community_name: Option<String>,
   auth: Option<String>,
 }
 
@@ -136,6 +142,10 @@ impl Perform for Oper<CreatePost> {
       }
     }
 
+    if !is_valid_post_title(&data.name) {
+      return Err(APIError::err("invalid_post_title").into());
+    }
+
     let user_id = claims.id;
 
     // Check for a community ban
@@ -157,7 +167,7 @@ impl Perform for Oper<CreatePost> {
       fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
 
     let post_form = PostForm {
-      name: data.name.to_owned(),
+      name: data.name.trim().to_owned(),
       url: data.url.to_owned(),
       body: data.body.to_owned(),
       community_id: data.community_id,
@@ -191,11 +201,16 @@ impl Perform for Oper<CreatePost> {
     };
 
     let inserted_post_id = inserted_post.id;
-    let updated_post =
-      match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? {
-        Ok(post) => post,
-        Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
-      };
+    let updated_post = match blocking(pool, move |conn| {
+      let apub_id =
+        make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
+      Post::update_ap_id(conn, inserted_post_id, apub_id)
+    })
+    .await?
+    {
+      Ok(post) => post,
+      Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
+    };
 
     updated_post.send_create(&user, &self.client, pool).await?;
 
@@ -361,12 +376,14 @@ impl Perform for Oper<GetPosts> {
     let page = data.page;
     let limit = data.limit;
     let community_id = data.community_id;
+    let community_name = data.community_name.to_owned();
     let posts = match blocking(pool, move |conn| {
       PostQueryBuilder::create(conn)
         .listing_type(type_)
         .sort(&sort)
         .show_nsfw(show_nsfw)
         .for_community_id(community_id)
+        .for_community_name(community_name)
         .my_user_id(user_id)
         .page(page)
         .limit(limit)
@@ -512,6 +529,10 @@ impl Perform for Oper<EditPost> {
       }
     }
 
+    if !is_valid_post_title(&data.name) {
+      return Err(APIError::err("invalid_post_title").into());
+    }
+
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => return Err(APIError::err("not_logged_in").into()),
@@ -519,28 +540,36 @@ impl Perform for Oper<EditPost> {
 
     let user_id = claims.id;
 
+    let edit_id = data.edit_id;
+    let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
+
     // Verify its the creator or a mod or admin
-    let community_id = data.community_id;
-    let mut editors: Vec<i32> = vec![data.creator_id];
-    editors.append(
+    let community_id = read_post.community_id;
+    let mut editors: Vec<i32> = vec![read_post.creator_id];
+    let mut moderators: Vec<i32> = vec![];
+
+    moderators.append(
       &mut blocking(pool, move |conn| {
         CommunityModeratorView::for_community(conn, community_id)
           .map(|v| v.into_iter().map(|m| m.user_id).collect())
       })
       .await??,
     );
-    editors.append(
+    moderators.append(
       &mut blocking(pool, move |conn| {
         UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
       })
       .await??,
     );
+
+    editors.extend(&moderators);
+
     if !editors.contains(&user_id) {
       return Err(APIError::err("no_post_edit_allowed").into());
     }
 
     // Check for a community ban
-    let community_id = data.community_id;
+    let community_id = read_post.community_id;
     let is_banned =
       move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
     if blocking(pool, is_banned).await? {
@@ -557,28 +586,51 @@ impl Perform for Oper<EditPost> {
     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
       fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
 
-    let edit_id = data.edit_id;
-    let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
-
-    let post_form = PostForm {
-      name: data.name.to_owned(),
-      url: data.url.to_owned(),
-      body: data.body.to_owned(),
-      creator_id: data.creator_id.to_owned(),
-      community_id: data.community_id,
-      removed: data.removed.to_owned(),
-      deleted: data.deleted.to_owned(),
-      nsfw: data.nsfw,
-      locked: data.locked.to_owned(),
-      stickied: data.stickied.to_owned(),
-      updated: Some(naive_now()),
-      embed_title: iframely_title,
-      embed_description: iframely_description,
-      embed_html: iframely_html,
-      thumbnail_url: pictrs_thumbnail,
-      ap_id: read_post.ap_id,
-      local: read_post.local,
-      published: None,
+    let post_form = {
+      // only modify some properties if they are a moderator
+      if moderators.contains(&user_id) {
+        PostForm {
+          name: data.name.trim().to_owned(),
+          url: data.url.to_owned(),
+          body: data.body.to_owned(),
+          creator_id: read_post.creator_id.to_owned(),
+          community_id: read_post.community_id,
+          removed: data.removed.to_owned(),
+          deleted: data.deleted.to_owned(),
+          nsfw: data.nsfw,
+          locked: data.locked.to_owned(),
+          stickied: data.stickied.to_owned(),
+          updated: Some(naive_now()),
+          embed_title: iframely_title,
+          embed_description: iframely_description,
+          embed_html: iframely_html,
+          thumbnail_url: pictrs_thumbnail,
+          ap_id: read_post.ap_id,
+          local: read_post.local,
+          published: None,
+        }
+      } else {
+        PostForm {
+          name: read_post.name.trim().to_owned(),
+          url: data.url.to_owned(),
+          body: data.body.to_owned(),
+          creator_id: read_post.creator_id.to_owned(),
+          community_id: read_post.community_id,
+          removed: Some(read_post.removed),
+          deleted: data.deleted.to_owned(),
+          nsfw: data.nsfw,
+          locked: Some(read_post.locked),
+          stickied: Some(read_post.stickied),
+          updated: Some(naive_now()),
+          embed_title: iframely_title,
+          embed_description: iframely_description,
+          embed_html: iframely_html,
+          thumbnail_url: pictrs_thumbnail,
+          ap_id: read_post.ap_id,
+          local: read_post.local,
+          published: None,
+        }
+      }
     };
 
     let edit_id = data.edit_id;
@@ -596,33 +648,35 @@ impl Perform for Oper<EditPost> {
       }
     };
 
-    // Mod tables
-    if let Some(removed) = data.removed.to_owned() {
-      let form = ModRemovePostForm {
-        mod_user_id: user_id,
-        post_id: data.edit_id,
-        removed: Some(removed),
-        reason: data.reason.to_owned(),
-      };
-      blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
-    }
+    if moderators.contains(&user_id) {
+      // Mod tables
+      if let Some(removed) = data.removed.to_owned() {
+        let form = ModRemovePostForm {
+          mod_user_id: user_id,
+          post_id: data.edit_id,
+          removed: Some(removed),
+          reason: data.reason.to_owned(),
+        };
+        blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
+      }
 
-    if let Some(locked) = data.locked.to_owned() {
-      let form = ModLockPostForm {
-        mod_user_id: user_id,
-        post_id: data.edit_id,
-        locked: Some(locked),
-      };
-      blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
-    }
+      if let Some(locked) = data.locked.to_owned() {
+        let form = ModLockPostForm {
+          mod_user_id: user_id,
+          post_id: data.edit_id,
+          locked: Some(locked),
+        };
+        blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
+      }
 
-    if let Some(stickied) = data.stickied.to_owned() {
-      let form = ModStickyPostForm {
-        mod_user_id: user_id,
-        post_id: data.edit_id,
-        stickied: Some(stickied),
-      };
-      blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
+      if let Some(stickied) = data.stickied.to_owned() {
+        let form = ModStickyPostForm {
+          mod_user_id: user_id,
+          post_id: data.edit_id,
+          stickied: Some(stickied),
+        };
+        blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
+      }
     }
 
     if let Some(deleted) = data.deleted.to_owned() {
@@ -634,12 +688,14 @@ impl Perform for Oper<EditPost> {
           .await?;
       }
     } else if let Some(removed) = data.removed.to_owned() {
-      if removed {
-        updated_post.send_remove(&user, &self.client, pool).await?;
-      } else {
-        updated_post
-          .send_undo_remove(&user, &self.client, pool)
-          .await?;
+      if moderators.contains(&user_id) {
+        if removed {
+          updated_post.send_remove(&user, &self.client, pool).await?;
+        } else {
+          updated_post
+            .send_undo_remove(&user, &self.client, pool)
+            .await?;
+        }
       }
     } else {
       updated_post.send_update(&user, &self.client, pool).await?;
index f45561a8284072b36ae2449f3e0debf6d4d936bb..241a80e31e97014a4ba45504a1edab4b686a7e9a 100644 (file)
@@ -1,31 +1,28 @@
 use super::user::Register;
 use crate::{
-  api::{APIError, Oper, Perform},
+  api::{claims::Claims, APIError, Oper, Perform},
   apub::fetcher::search_by_apub_id,
   blocking,
-  db::{
-    category::*,
-    comment_view::*,
-    community_view::*,
-    moderator::*,
-    moderator_views::*,
-    post_view::*,
-    site::*,
-    site_view::*,
-    user::*,
-    user_view::*,
-    Crud,
-    SearchType,
-    SortType,
-  },
-  naive_now,
-  settings::Settings,
-  slur_check,
-  slurs_vec_to_str,
   websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
   DbPool,
   LemmyError,
 };
+use lemmy_db::{
+  category::*,
+  comment_view::*,
+  community_view::*,
+  moderator::*,
+  moderator_views::*,
+  naive_now,
+  post_view::*,
+  site::*,
+  site_view::*,
+  user_view::*,
+  Crud,
+  SearchType,
+  SortType,
+};
+use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str};
 use log::{debug, info};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
index 9b72a91997dec6d6287b5dbcb9b40e73cda801cb..d547f64b229e4bc9350c157abfb54f8c690eea1f 100644 (file)
@@ -1,53 +1,53 @@
 use crate::{
-  api::{APIError, Oper, Perform},
-  apub::{
-    extensions::signatures::generate_actor_keypair,
-    make_apub_endpoint,
-    ApubObjectType,
-    EndpointType,
-  },
+  api::{claims::Claims, APIError, Oper, Perform},
+  apub::ApubObjectType,
   blocking,
-  db::{
-    comment::*,
-    comment_view::*,
-    community::*,
-    community_view::*,
-    moderator::*,
-    password_reset_request::*,
-    post::*,
-    post_view::*,
-    private_message::*,
-    private_message_view::*,
-    site::*,
-    site_view::*,
-    user::*,
-    user_mention::*,
-    user_mention_view::*,
-    user_view::*,
-    Crud,
-    Followable,
-    Joinable,
-    ListingType,
-    SortType,
+  websocket::{
+    server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
+    UserOperation,
+    WebsocketInfo,
   },
+  DbPool,
+  LemmyError,
+};
+use bcrypt::verify;
+use lemmy_db::{
+  comment::*,
+  comment_view::*,
+  community::*,
+  community_view::*,
+  moderator::*,
+  naive_now,
+  password_reset_request::*,
+  post::*,
+  post_view::*,
+  private_message::*,
+  private_message_view::*,
+  site::*,
+  site_view::*,
+  user::*,
+  user_mention::*,
+  user_mention_view::*,
+  user_view::*,
+  Crud,
+  Followable,
+  Joinable,
+  ListingType,
+  SortType,
+};
+use lemmy_utils::{
+  generate_actor_keypair,
   generate_random_string,
   is_valid_username,
+  make_apub_endpoint,
   naive_from_unix,
-  naive_now,
   remove_slurs,
   send_email,
   settings::Settings,
   slur_check,
   slurs_vec_to_str,
-  websocket::{
-    server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
-    UserOperation,
-    WebsocketInfo,
-  },
-  DbPool,
-  LemmyError,
+  EndpointType,
 };
-use bcrypt::verify;
 use log::error;
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
@@ -264,7 +264,7 @@ impl Perform for Oper<Login> {
     // Fetch that username / email
     let username_or_email = data.username_or_email.clone();
     let user = match blocking(pool, move |conn| {
-      User_::find_by_email_or_username(conn, &username_or_email)
+      Claims::find_by_email_or_username(conn, &username_or_email)
     })
     .await?
     {
@@ -279,7 +279,9 @@ impl Perform for Oper<Login> {
     }
 
     // Return the jwt
-    Ok(LoginResponse { jwt: user.jwt() })
+    Ok(LoginResponse {
+      jwt: Claims::jwt(user, Settings::get().hostname),
+    })
   }
 }
 
@@ -421,7 +423,7 @@ impl Perform for Oper<Register> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: inserted_user.jwt(),
+      jwt: Claims::jwt(inserted_user, Settings::get().hostname),
     })
   }
 }
@@ -451,6 +453,11 @@ impl Perform for Oper<SaveUserSettings> {
       None => read_user.email,
     };
 
+    let avatar = match &data.avatar {
+      Some(avatar) => Some(avatar.to_owned()),
+      None => read_user.avatar,
+    };
+
     let password_encrypted = match &data.new_password {
       Some(new_password) => {
         match &data.new_password_verify {
@@ -488,7 +495,7 @@ impl Perform for Oper<SaveUserSettings> {
       name: read_user.name,
       email,
       matrix_user_id: data.matrix_user_id.to_owned(),
-      avatar: data.avatar.to_owned(),
+      avatar,
       password_encrypted,
       preferred_username: read_user.preferred_username,
       updated: Some(naive_now()),
@@ -527,7 +534,7 @@ impl Perform for Oper<SaveUserSettings> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: updated_user.jwt(),
+      jwt: Claims::jwt(updated_user, Settings::get().hostname),
     })
   }
 }
@@ -875,23 +882,27 @@ impl Perform for Oper<EditUserMention> {
     let user_id = claims.id;
 
     let user_mention_id = data.user_mention_id;
-    let user_mention =
+    let read_user_mention =
       blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
 
+    if user_id != read_user_mention.recipient_id {
+      return Err(APIError::err("couldnt_update_comment").into());
+    }
+
     let user_mention_form = UserMentionForm {
-      recipient_id: user_id,
-      comment_id: user_mention.comment_id,
+      recipient_id: read_user_mention.recipient_id,
+      comment_id: read_user_mention.comment_id,
       read: data.read.to_owned(),
     };
 
-    let user_mention_id = user_mention.id;
+    let user_mention_id = read_user_mention.id;
     let update_mention =
       move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
     if blocking(pool, update_mention).await?.is_err() {
       return Err(APIError::err("couldnt_update_comment").into());
     };
 
-    let user_mention_id = user_mention.id;
+    let user_mention_id = read_user_mention.id;
     let user_mention_view = blocking(pool, move |conn| {
       UserMentionView::read(conn, user_mention_id, user_id)
     })
@@ -1150,7 +1161,7 @@ impl Perform for Oper<PasswordChange> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: updated_user.jwt(),
+      jwt: Claims::jwt(updated_user, Settings::get().hostname),
     })
   }
 }
@@ -1208,7 +1219,12 @@ impl Perform for Oper<CreatePrivateMessage> {
 
     let inserted_private_message_id = inserted_private_message.id;
     let updated_private_message = match blocking(pool, move |conn| {
-      PrivateMessage::update_ap_id(&conn, inserted_private_message_id)
+      let apub_id = make_apub_endpoint(
+        EndpointType::PrivateMessage,
+        &inserted_private_message_id.to_string(),
+      )
+      .to_string();
+      PrivateMessage::update_ap_id(&conn, inserted_private_message_id, apub_id)
     })
     .await?
     {
@@ -1298,23 +1314,35 @@ impl Perform for Oper<EditPrivateMessage> {
 
     let content_slurs_removed = match &data.content {
       Some(content) => remove_slurs(content),
-      None => orig_private_message.content,
+      None => orig_private_message.content.clone(),
     };
 
-    let private_message_form = PrivateMessageForm {
-      content: content_slurs_removed,
-      creator_id: orig_private_message.creator_id,
-      recipient_id: orig_private_message.recipient_id,
-      deleted: data.deleted.to_owned(),
-      read: data.read.to_owned(),
-      updated: if data.read.is_some() {
-        orig_private_message.updated
+    let private_message_form = {
+      if data.read.is_some() {
+        PrivateMessageForm {
+          content: orig_private_message.content.to_owned(),
+          creator_id: orig_private_message.creator_id,
+          recipient_id: orig_private_message.recipient_id,
+          read: data.read.to_owned(),
+          updated: orig_private_message.updated,
+          deleted: Some(orig_private_message.deleted),
+          ap_id: orig_private_message.ap_id,
+          local: orig_private_message.local,
+          published: None,
+        }
       } else {
-        Some(naive_now())
-      },
-      ap_id: orig_private_message.ap_id,
-      local: orig_private_message.local,
-      published: None,
+        PrivateMessageForm {
+          content: content_slurs_removed,
+          creator_id: orig_private_message.creator_id,
+          recipient_id: orig_private_message.recipient_id,
+          deleted: data.deleted.to_owned(),
+          read: Some(orig_private_message.read),
+          updated: Some(naive_now()),
+          ap_id: orig_private_message.ap_id,
+          local: orig_private_message.local,
+          published: None,
+        }
+      }
     };
 
     let edit_id = data.edit_id;
@@ -1327,14 +1355,20 @@ impl Perform for Oper<EditPrivateMessage> {
       Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
     };
 
-    if let Some(deleted) = data.deleted.to_owned() {
-      if deleted {
-        updated_private_message
-          .send_delete(&user, &self.client, pool)
-          .await?;
+    if data.read.is_none() {
+      if let Some(deleted) = data.deleted.to_owned() {
+        if deleted {
+          updated_private_message
+            .send_delete(&user, &self.client, pool)
+            .await?;
+        } else {
+          updated_private_message
+            .send_undo_delete(&user, &self.client, pool)
+            .await?;
+        }
       } else {
         updated_private_message
-          .send_undo_delete(&user, &self.client, pool)
+          .send_update(&user, &self.client, pool)
           .await?;
       }
     } else {
index e5dc70457c2cb1c78576b539635d262719395146..204a380d39b387e66abb7a334e4ff6d94d9215f7 100644 (file)
@@ -1,12 +1,18 @@
 use crate::{
-  apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
-  db::{activity::insert_activity, community::Community, user::User_},
+  apub::{
+    community::do_announce,
+    extensions::signatures::sign,
+    insert_activity,
+    is_apub_id_valid,
+    ActorType,
+  },
   request::retry_custom,
   DbPool,
   LemmyError,
 };
 use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
 use actix_web::client::Client;
+use lemmy_db::{community::Community, user::User_};
 use log::debug;
 use serde::Serialize;
 use std::fmt::Debug;
@@ -43,7 +49,7 @@ where
 
   // if this is a local community, we need to do an announce from the community instead
   if community.local {
-    Community::do_announce(activity, &community, creator, client, pool).await?;
+    do_announce(activity, &community, creator, client, pool).await?;
   } else {
     send_activity(client, &activity, creator, to).await?;
   }
index dbc15909e8d9136a78ef4b04f16de463923f2e80..9e5e53a7b48225238873fecf223dff6884ecfa31 100644 (file)
@@ -17,19 +17,9 @@ use crate::{
     ToApub,
   },
   blocking,
-  convert_datetime,
-  db::{
-    comment::{Comment, CommentForm},
-    community::Community,
-    post::Post,
-    user::User_,
-    Crud,
-  },
   routes::DbPoolParam,
-  scrape_text_for_mentions,
   DbPool,
   LemmyError,
-  MentionData,
 };
 use activitystreams::{
   activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@@ -40,6 +30,14 @@ use activitystreams::{
 use activitystreams_new::object::Tombstone;
 use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
 use itertools::Itertools;
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  community::Community,
+  post::Post,
+  user::User_,
+  Crud,
+};
+use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
 use log::debug;
 use serde::Deserialize;
 
@@ -123,7 +121,7 @@ impl FromApub for CommentForm {
 
   /// Parse an ActivityPub note received from another instance into a Lemmy comment
   async fn from_apub(
-    note: &mut Note,
+    note: &Note,
     client: &Client,
     pool: &DbPool,
   ) -> Result<CommentForm, LemmyError> {
index bfc896af61dac30697bde794a3365cb58b592830..529039fc0261586ea6e06116c2193596fa30d270 100644 (file)
@@ -7,20 +7,13 @@ use crate::{
     extensions::group_extensions::GroupExtension,
     fetcher::get_or_fetch_and_upsert_remote_user,
     get_shared_inbox,
+    insert_activity,
     ActorType,
     FromApub,
     GroupExt,
     ToApub,
   },
   blocking,
-  convert_datetime,
-  db::{
-    activity::insert_activity,
-    community::{Community, CommunityForm},
-    community_view::{CommunityFollowerView, CommunityModeratorView},
-    user::User_,
-  },
-  naive_now,
   routes::DbPoolParam,
   DbPool,
   LemmyError,
@@ -44,6 +37,13 @@ use activitystreams_new::{
 };
 use actix_web::{body::Body, client::Client, web, HttpResponse};
 use itertools::Itertools;
+use lemmy_db::{
+  community::{Community, CommunityForm},
+  community_view::{CommunityFollowerView, CommunityModeratorView},
+  naive_now,
+  user::User_,
+};
+use lemmy_utils::convert_datetime;
 use serde::{Deserialize, Serialize};
 use std::{fmt::Debug, str::FromStr};
 
@@ -367,13 +367,8 @@ impl FromApub for CommunityForm {
   type ApubType = GroupExt;
 
   /// Parse an ActivityPub group received from another instance into a Lemmy community.
-  async fn from_apub(
-    group: &mut GroupExt,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<Self, LemmyError> {
-    // TODO: this is probably gonna cause problems cause fetcher:292 also calls take_attributed_to()
-    let creator_and_moderator_uris = group.clone().take_attributed_to().unwrap();
+  async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
+    let creator_and_moderator_uris = group.attributed_to().unwrap();
     let creator_uri = creator_and_moderator_uris
       .as_many()
       .unwrap()
@@ -386,27 +381,20 @@ impl FromApub for CommunityForm {
     let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
 
     Ok(CommunityForm {
-      name: group
-        .take_name()
-        .unwrap()
-        .as_single_xsd_string()
-        .unwrap()
-        .into(),
-      title: group.inner.take_preferred_username().unwrap(),
+      name: group.name().unwrap().as_single_xsd_string().unwrap().into(),
+      title: group.inner.preferred_username().unwrap().to_string(),
       // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
       //       -> same for post.content etc
       description: group
-        .take_content()
+        .content()
         .map(|s| s.as_single_xsd_string().unwrap().into()),
       category_id: group.ext_one.category.identifier.parse::<i32>()?,
       creator_id: creator.id,
       removed: None,
       published: group
-        .take_published()
-        .map(|u| u.as_ref().to_owned().naive_local()),
-      updated: group
-        .take_updated()
+        .published()
         .map(|u| u.as_ref().to_owned().naive_local()),
+      updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
       deleted: None,
       nsfw: group.ext_one.sensitive,
       actor_id: group.id().unwrap().to_string(),
@@ -462,39 +450,37 @@ pub async fn get_apub_community_followers(
   Ok(create_apub_response(&collection))
 }
 
-impl Community {
-  pub async fn do_announce<A>(
-    activity: A,
-    community: &Community,
-    sender: &dyn ActorType,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<HttpResponse, LemmyError>
-  where
-    A: Activity + Base + Serialize + Debug,
-  {
-    let mut announce = Announce::default();
-    populate_object_props(
-      &mut announce.object_props,
-      vec![community.get_followers_url()],
-      &format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
-    )?;
-    announce
-      .announce_props
-      .set_actor_xsd_any_uri(community.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(activity)?)?;
-
-    insert_activity(community.creator_id, announce.clone(), true, pool).await?;
-
-    // dont send to the instance where the activity originally came from, because that would result
-    // in a database error (same data inserted twice)
-    let mut to = community.get_follower_inboxes(pool).await?;
-
-    // this seems to be the "easiest" stable alternative for remove_item()
-    to.retain(|x| *x != sender.get_shared_inbox_url());
-
-    send_activity(client, &announce, community, to).await?;
-
-    Ok(HttpResponse::Ok().finish())
-  }
+pub async fn do_announce<A>(
+  activity: A,
+  community: &Community,
+  sender: &dyn ActorType,
+  client: &Client,
+  pool: &DbPool,
+) -> Result<HttpResponse, LemmyError>
+where
+  A: Activity + Base + Serialize + Debug,
+{
+  let mut announce = Announce::default();
+  populate_object_props(
+    &mut announce.object_props,
+    vec![community.get_followers_url()],
+    &format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
+  )?;
+  announce
+    .announce_props
+    .set_actor_xsd_any_uri(community.actor_id.to_owned())?
+    .set_object_base_box(BaseBox::from_concrete(activity)?)?;
+
+  insert_activity(community.creator_id, announce.clone(), true, pool).await?;
+
+  // dont send to the instance where the activity originally came from, because that would result
+  // in a database error (same data inserted twice)
+  let mut to = community.get_follower_inboxes(pool).await?;
+
+  // this seems to be the "easiest" stable alternative for remove_item()
+  to.retain(|x| *x != sender.get_shared_inbox_url());
+
+  send_activity(client, &announce, community, to).await?;
+
+  Ok(HttpResponse::Ok().finish())
 }
index 996e0c251468ecd638c3f18e072f91cf8d7ef212..8ea6443419ff5d8604d16065140a92d9c4984c2d 100644 (file)
@@ -2,21 +2,21 @@ use crate::{
   apub::{
     extensions::signatures::verify,
     fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+    insert_activity,
     ActorType,
   },
   blocking,
-  db::{
-    activity::insert_activity,
-    community::{Community, CommunityFollower, CommunityFollowerForm},
-    user::User_,
-    Followable,
-  },
   routes::{ChatServerParam, DbPoolParam},
   LemmyError,
 };
 use activitystreams::activity::Undo;
 use activitystreams_new::activity::Follow;
 use actix_web::{client::Client, web, HttpRequest, HttpResponse};
+use lemmy_db::{
+  community::{Community, CommunityFollower, CommunityFollowerForm},
+  user::User_,
+  Followable,
+};
 use log::debug;
 use serde::Deserialize;
 use std::fmt::Debug;
index 1c24eef57e67265bd182b836b074553fc3204581..2120f6f14f5ba121c3627b441990b7e56d04a2de 100644 (file)
@@ -1,9 +1,7 @@
-use crate::{
-  db::{category::Category, Crud},
-  LemmyError,
-};
+use crate::LemmyError;
 use activitystreams::{ext::Extension, Actor};
 use diesel::PgConnection;
+use lemmy_db::{category::Category, Crud};
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
index af46bc5eeb35117ccef578d05ae35af915626df8..1c930a958ca3c8fc3d22bebae18d5dca4e41b932 100644 (file)
@@ -9,7 +9,6 @@ use log::debug;
 use openssl::{
   hash::MessageDigest,
   pkey::PKey,
-  rsa::Rsa,
   sign::{Signer, Verifier},
 };
 use serde::{Deserialize, Serialize};
@@ -19,23 +18,6 @@ lazy_static! {
   static ref HTTP_SIG_CONFIG: Config = Config::new();
 }
 
-pub struct Keypair {
-  pub private_key: String,
-  pub public_key: String,
-}
-
-/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
-pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
-  let rsa = Rsa::generate(2048)?;
-  let pkey = PKey::from_rsa(rsa)?;
-  let public_key = pkey.public_key_to_pem()?;
-  let private_key = pkey.private_key_to_pem_pkcs8()?;
-  Ok(Keypair {
-    private_key: String::from_utf8(private_key)?,
-    public_key: String::from_utf8(public_key)?,
-  })
-}
-
 /// Signs request headers with the given keypair.
 pub async fn sign(
   request: ClientRequest,
index d8a1e764f64078a2f9d16fd3aec3a5f68fa6870b..f20c9eabe85882e402d4f596b5e0c9488c8e3877 100644 (file)
@@ -1,29 +1,7 @@
 use crate::{
   api::site::SearchResponse,
-  apub::{
-    get_apub_protocol_string,
-    is_apub_id_valid,
-    FromApub,
-    GroupExt,
-    PageExt,
-    PersonExt,
-    APUB_JSON_CONTENT_TYPE,
-  },
+  apub::{is_apub_id_valid, FromApub, GroupExt, PageExt, PersonExt, APUB_JSON_CONTENT_TYPE},
   blocking,
-  db::{
-    comment::{Comment, CommentForm},
-    comment_view::CommentView,
-    community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
-    community_view::CommunityView,
-    post::{Post, PostForm},
-    post_view::PostView,
-    user::{UserForm, User_},
-    user_view::UserView,
-    Crud,
-    Joinable,
-    SearchType,
-  },
-  naive_now,
   request::{retry, RecvError},
   routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
   DbPool,
@@ -34,6 +12,21 @@ use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
 use actix_web::client::Client;
 use chrono::NaiveDateTime;
 use diesel::{result::Error::NotFound, PgConnection};
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  comment_view::CommentView,
+  community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
+  community_view::CommunityView,
+  naive_now,
+  post::{Post, PostForm},
+  post_view::PostView,
+  user::{UserForm, User_},
+  user_view::UserView,
+  Crud,
+  Joinable,
+  SearchType,
+};
+use lemmy_utils::get_apub_protocol_string;
 use log::debug;
 use serde::Deserialize;
 use std::{fmt::Debug, time::Duration};
@@ -171,15 +164,15 @@ pub async fn search_by_apub_id(
 
       response
     }
-    SearchAcceptedObjects::Page(mut p) => {
-      let post_form = PostForm::from_apub(&mut p, client, pool).await?;
+    SearchAcceptedObjects::Page(p) => {
+      let post_form = PostForm::from_apub(&p, client, pool).await?;
 
       let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
       response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
 
       response
     }
-    SearchAcceptedObjects::Comment(mut c) => {
+    SearchAcceptedObjects::Comment(c) => {
       let post_url = c
         .object_props
         .get_many_in_reply_to_xsd_any_uris()
@@ -189,9 +182,9 @@ pub async fn search_by_apub_id(
         .to_string();
 
       // TODO: also fetch parent comments if any
-      let mut post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
-      let post_form = PostForm::from_apub(&mut post, client, pool).await?;
-      let comment_form = CommentForm::from_apub(&mut c, client, pool).await?;
+      let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
+      let post_form = PostForm::from_apub(&post, client, pool).await?;
+      let comment_form = CommentForm::from_apub(&c, client, pool).await?;
 
       blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
       let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
@@ -221,9 +214,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
     // If its older than a day, re-fetch it
     Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
       debug!("Fetching and updating from remote user: {}", apub_id);
-      let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
+      let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
 
-      let mut uf = UserForm::from_apub(&mut person, client, pool).await?;
+      let mut uf = UserForm::from_apub(&person, client, pool).await?;
       uf.last_refreshed_at = Some(naive_now());
       let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
 
@@ -232,9 +225,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
     Ok(u) => Ok(u),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote user: {}", apub_id);
-      let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
+      let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
 
-      let uf = UserForm::from_apub(&mut person, client, pool).await?;
+      let uf = UserForm::from_apub(&person, client, pool).await?;
       let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
 
       Ok(user)
@@ -272,9 +265,9 @@ pub async fn get_or_fetch_and_upsert_remote_community(
   match community {
     Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
       debug!("Fetching and updating from remote community: {}", apub_id);
-      let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
+      let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
 
-      let mut cf = CommunityForm::from_apub(&mut group, client, pool).await?;
+      let mut cf = CommunityForm::from_apub(&group, client, pool).await?;
       cf.last_refreshed_at = Some(naive_now());
       let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
 
@@ -283,13 +276,13 @@ pub async fn get_or_fetch_and_upsert_remote_community(
     Ok(c) => Ok(c),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote community: {}", apub_id);
-      let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
+      let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
 
-      let cf = CommunityForm::from_apub(&mut group, client, pool).await?;
+      let cf = CommunityForm::from_apub(&group, client, pool).await?;
       let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
 
       // Also add the community moderators too
-      let attributed_to = group.inner.take_attributed_to().unwrap();
+      let attributed_to = group.inner.attributed_to().unwrap();
       let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to
         .as_many()
         .unwrap()
@@ -349,8 +342,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
     Ok(p) => Ok(p),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote post: {}", post_ap_id);
-      let mut post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
-      let post_form = PostForm::from_apub(&mut post, client, pool).await?;
+      let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
+      let post_form = PostForm::from_apub(&post, client, pool).await?;
 
       let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
 
@@ -387,8 +380,8 @@ pub async fn get_or_fetch_and_insert_remote_comment(
         "Fetching and creating remote comment and its parents: {}",
         comment_ap_id
       );
-      let mut comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
-      let comment_form = CommentForm::from_apub(&mut comment, client, pool).await?;
+      let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
+      let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
 
       let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
 
index 561dc49a133c770bd65dca19b323706e527d1c8f..cfb539fb169756241016ef955a0be18b8909c24f 100644 (file)
@@ -16,14 +16,11 @@ use crate::{
     page_extension::PageExtension,
     signatures::{PublicKey, PublicKeyExtension},
   },
-  convert_datetime,
-  db::user::User_,
+  blocking,
   request::{retry, RecvError},
   routes::webfinger::WebFingerResponse,
   DbPool,
   LemmyError,
-  MentionData,
-  Settings,
 };
 use activitystreams::object::Page;
 use activitystreams_ext::{Ext1, Ext2};
@@ -35,6 +32,9 @@ use activitystreams_new::{
 };
 use actix_web::{body::Body, client::Client, HttpResponse};
 use chrono::NaiveDateTime;
+use failure::_core::fmt::Debug;
+use lemmy_db::{activity::do_insert_activity, user::User_};
+use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
 use log::debug;
 use serde::Serialize;
 use url::Url;
@@ -45,14 +45,6 @@ type PageExt = Ext1<Page, PageExtension>;
 
 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
 
-pub enum EndpointType {
-  Community,
-  User,
-  Post,
-  Comment,
-  PrivateMessage,
-}
-
 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
 /// headers.
 fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
@@ -73,34 +65,6 @@ where
     .json(data)
 }
 
-/// Generates the ActivityPub ID for a given object type and ID.
-pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
-  let point = match endpoint_type {
-    EndpointType::Community => "c",
-    EndpointType::User => "u",
-    EndpointType::Post => "post",
-    EndpointType::Comment => "comment",
-    EndpointType::PrivateMessage => "private_message",
-  };
-
-  Url::parse(&format!(
-    "{}://{}/{}/{}",
-    get_apub_protocol_string(),
-    Settings::get().hostname,
-    point,
-    name
-  ))
-  .unwrap()
-}
-
-pub fn get_apub_protocol_string() -> &'static str {
-  if Settings::get().federation.tls_enabled {
-    "https"
-  } else {
-    "http"
-  }
-}
-
 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
 fn is_apub_id_valid(apub_id: &Url) -> bool {
   debug!("Checking {}", apub_id);
@@ -165,7 +129,7 @@ fn create_tombstone(
 pub trait FromApub {
   type ApubType;
   async fn from_apub(
-    apub: &mut Self::ApubType,
+    apub: &Self::ApubType,
     client: &Client,
     pool: &DbPool,
   ) -> Result<Self, LemmyError>
@@ -374,3 +338,19 @@ pub async fn fetch_webfinger_url(
     .to_owned()
     .ok_or_else(|| format_err!("No href found.").into())
 }
+
+pub async fn insert_activity<T>(
+  user_id: i32,
+  data: T,
+  local: bool,
+  pool: &DbPool,
+) -> Result<(), LemmyError>
+where
+  T: Serialize + Debug + Send + 'static,
+{
+  blocking(pool, move |conn| {
+    do_insert_activity(conn, user_id, &data, local)
+  })
+  .await??;
+  Ok(())
+}
index 255629e8db2fe81fe3e5900a716bc889370ad16f..36922e4f0cc30592159a5a3620f9dc595c23f597 100644 (file)
@@ -6,7 +6,6 @@ use crate::{
     create_tombstone,
     extensions::page_extension::PageExtension,
     fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
-    get_apub_protocol_string,
     ActorType,
     ApubLikeableType,
     ApubObjectType,
@@ -15,17 +14,9 @@ use crate::{
     ToApub,
   },
   blocking,
-  convert_datetime,
-  db::{
-    community::Community,
-    post::{Post, PostForm},
-    user::User_,
-    Crud,
-  },
   routes::DbPoolParam,
   DbPool,
   LemmyError,
-  Settings,
 };
 use activitystreams::{
   activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@@ -36,6 +27,13 @@ use activitystreams::{
 use activitystreams_ext::Ext1;
 use activitystreams_new::object::Tombstone;
 use actix_web::{body::Body, client::Client, web, HttpResponse};
+use lemmy_db::{
+  community::Community,
+  post::{Post, PostForm},
+  user::User_,
+  Crud,
+};
+use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
 use serde::Deserialize;
 
 #[derive(Deserialize)]
@@ -164,7 +162,7 @@ impl FromApub for PostForm {
 
   /// Parse an ActivityPub page received from another instance into a Lemmy post.
   async fn from_apub(
-    page: &mut PageExt,
+    page: &PageExt,
     client: &Client,
     pool: &DbPool,
   ) -> Result<PostForm, LemmyError> {
index 48bbd1f0f8ddfddb6bedc5aedd66468e903b2777..bc685b2382019d1e26cc8027ee89ab060b722d73 100644 (file)
@@ -3,18 +3,12 @@ use crate::{
     activities::send_activity,
     create_tombstone,
     fetcher::get_or_fetch_and_upsert_remote_user,
+    insert_activity,
     ApubObjectType,
     FromApub,
     ToApub,
   },
   blocking,
-  convert_datetime,
-  db::{
-    activity::insert_activity,
-    private_message::{PrivateMessage, PrivateMessageForm},
-    user::User_,
-    Crud,
-  },
   DbPool,
   LemmyError,
 };
@@ -25,6 +19,12 @@ use activitystreams::{
 };
 use activitystreams_new::object::Tombstone;
 use actix_web::client::Client;
+use lemmy_db::{
+  private_message::{PrivateMessage, PrivateMessageForm},
+  user::User_,
+  Crud,
+};
+use lemmy_utils::convert_datetime;
 
 #[async_trait::async_trait(?Send)]
 impl ToApub for PrivateMessage {
@@ -71,7 +71,7 @@ impl FromApub for PrivateMessageForm {
 
   /// Parse an ActivityPub note received from another instance into a Lemmy Private message
   async fn from_apub(
-    note: &mut Note,
+    note: &Note,
     client: &Client,
     pool: &DbPool,
   ) -> Result<PrivateMessageForm, LemmyError> {
index fa9794b25c23ec202be807eb549bcba72155a5b2..7319f1aee519bac57b64e138008b29c66e614c29 100644 (file)
@@ -5,6 +5,7 @@ use crate::{
     post::PostResponse,
   },
   apub::{
+    community::do_announce,
     extensions::signatures::verify,
     fetcher::{
       get_or_fetch_and_insert_remote_comment,
@@ -12,25 +13,13 @@ use crate::{
       get_or_fetch_and_upsert_remote_community,
       get_or_fetch_and_upsert_remote_user,
     },
+    insert_activity,
     FromApub,
     GroupExt,
     PageExt,
   },
   blocking,
-  db::{
-    activity::insert_activity,
-    comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
-    comment_view::CommentView,
-    community::{Community, CommunityForm},
-    community_view::CommunityView,
-    post::{Post, PostForm, PostLike, PostLikeForm},
-    post_view::PostView,
-    Crud,
-    Likeable,
-  },
-  naive_now,
   routes::{ChatServerParam, DbPoolParam},
-  scrape_text_for_mentions,
   websocket::{
     server::{SendComment, SendCommunityRoomMessage, SendPost},
     UserOperation,
@@ -46,6 +35,18 @@ use activitystreams::{
   BaseBox,
 };
 use actix_web::{client::Client, web, HttpRequest, HttpResponse};
+use lemmy_db::{
+  comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
+  comment_view::CommentView,
+  community::{Community, CommunityForm},
+  community_view::CommunityView,
+  naive_now,
+  post::{Post, PostForm, PostLike, PostLikeForm},
+  post_view::PostView,
+  Crud,
+  Likeable,
+};
+use lemmy_utils::scrape_text_for_mentions;
 use log::debug;
 use serde::{Deserialize, Serialize};
 use std::fmt::Debug;
@@ -234,7 +235,7 @@ where
   if community.local {
     let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?;
 
-    Community::do_announce(activity, &community, &sending_user, client, pool).await
+    do_announce(activity, &community, &sending_user, client, pool).await
   } else {
     Ok(HttpResponse::NotFound().finish())
   }
@@ -335,7 +336,7 @@ async fn receive_create_post(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut page = create
+  let page = create
     .create_props
     .get_object_base_box()
     .to_owned()
@@ -353,7 +354,7 @@ async fn receive_create_post(
 
   insert_activity(user.id, create, false, pool).await?;
 
-  let post = PostForm::from_apub(&mut page, client, pool).await?;
+  let post = PostForm::from_apub(&page, client, pool).await?;
 
   let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
 
@@ -381,7 +382,7 @@ async fn receive_create_comment(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = create
+  let note = create
     .create_props
     .get_object_base_box()
     .to_owned()
@@ -399,7 +400,7 @@ async fn receive_create_comment(
 
   insert_activity(user.id, create, false, pool).await?;
 
-  let comment = CommentForm::from_apub(&mut note, client, pool).await?;
+  let comment = CommentForm::from_apub(&note, client, pool).await?;
 
   let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
 
@@ -440,7 +441,7 @@ async fn receive_update_post(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut page = update
+  let page = update
     .update_props
     .get_object_base_box()
     .to_owned()
@@ -458,7 +459,7 @@ async fn receive_update_post(
 
   insert_activity(user.id, update, false, pool).await?;
 
-  let post = PostForm::from_apub(&mut page, client, pool).await?;
+  let post = PostForm::from_apub(&page, client, pool).await?;
 
   let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
     .await?
@@ -486,7 +487,7 @@ async fn receive_like_post(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut page = like
+  let page = like
     .like_props
     .get_object_base_box()
     .to_owned()
@@ -500,7 +501,7 @@ async fn receive_like_post(
 
   insert_activity(user.id, like, false, pool).await?;
 
-  let post = PostForm::from_apub(&mut page, client, pool).await?;
+  let post = PostForm::from_apub(&page, client, pool).await?;
 
   let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
     .await?
@@ -537,7 +538,7 @@ async fn receive_dislike_post(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut page = dislike
+  let page = dislike
     .dislike_props
     .get_object_base_box()
     .to_owned()
@@ -555,7 +556,7 @@ async fn receive_dislike_post(
 
   insert_activity(user.id, dislike, false, pool).await?;
 
-  let post = PostForm::from_apub(&mut page, client, pool).await?;
+  let post = PostForm::from_apub(&page, client, pool).await?;
 
   let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
     .await?
@@ -592,7 +593,7 @@ async fn receive_update_comment(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = update
+  let note = update
     .update_props
     .get_object_base_box()
     .to_owned()
@@ -610,7 +611,7 @@ async fn receive_update_comment(
 
   insert_activity(user.id, update, false, pool).await?;
 
-  let comment = CommentForm::from_apub(&mut note, client, pool).await?;
+  let comment = CommentForm::from_apub(&note, client, pool).await?;
 
   let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
     .await?
@@ -651,7 +652,7 @@ async fn receive_like_comment(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = like
+  let note = like
     .like_props
     .get_object_base_box()
     .to_owned()
@@ -665,7 +666,7 @@ async fn receive_like_comment(
 
   insert_activity(user.id, like, false, pool).await?;
 
-  let comment = CommentForm::from_apub(&mut note, client, pool).await?;
+  let comment = CommentForm::from_apub(&note, client, pool).await?;
 
   let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
     .await?
@@ -709,7 +710,7 @@ async fn receive_dislike_comment(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = dislike
+  let note = dislike
     .dislike_props
     .get_object_base_box()
     .to_owned()
@@ -727,7 +728,7 @@ async fn receive_dislike_comment(
 
   insert_activity(user.id, dislike, false, pool).await?;
 
-  let comment = CommentForm::from_apub(&mut note, client, pool).await?;
+  let comment = CommentForm::from_apub(&note, client, pool).await?;
 
   let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
     .await?
@@ -777,7 +778,7 @@ async fn receive_delete_community(
     .unwrap()
     .to_string();
 
-  let mut group = delete
+  let group = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -789,7 +790,7 @@ async fn receive_delete_community(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
+  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
     .await?
     .actor_id;
 
@@ -854,7 +855,7 @@ async fn receive_remove_community(
     .unwrap()
     .to_string();
 
-  let mut group = remove
+  let group = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -866,7 +867,7 @@ async fn receive_remove_community(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
+  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
     .await?
     .actor_id;
 
@@ -931,7 +932,7 @@ async fn receive_delete_post(
     .unwrap()
     .to_string();
 
-  let mut page = delete
+  let page = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -943,7 +944,7 @@ async fn receive_delete_post(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
+  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
 
   let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
 
@@ -997,7 +998,7 @@ async fn receive_remove_post(
     .unwrap()
     .to_string();
 
-  let mut page = remove
+  let page = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -1009,7 +1010,7 @@ async fn receive_remove_post(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
+  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
 
   let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
 
@@ -1063,7 +1064,7 @@ async fn receive_delete_comment(
     .unwrap()
     .to_string();
 
-  let mut note = delete
+  let note = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -1075,7 +1076,7 @@ async fn receive_delete_comment(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
+  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
 
   let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
 
@@ -1131,7 +1132,7 @@ async fn receive_remove_comment(
     .unwrap()
     .to_string();
 
-  let mut note = remove
+  let note = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -1143,7 +1144,7 @@ async fn receive_remove_comment(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
+  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
 
   let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
 
@@ -1259,7 +1260,7 @@ async fn receive_undo_delete_comment(
     .unwrap()
     .to_string();
 
-  let mut note = delete
+  let note = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -1271,7 +1272,7 @@ async fn receive_undo_delete_comment(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
+  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
 
   let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
 
@@ -1327,7 +1328,7 @@ async fn receive_undo_remove_comment(
     .unwrap()
     .to_string();
 
-  let mut note = remove
+  let note = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -1339,7 +1340,7 @@ async fn receive_undo_remove_comment(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id;
+  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
 
   let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
 
@@ -1395,7 +1396,7 @@ async fn receive_undo_delete_post(
     .unwrap()
     .to_string();
 
-  let mut page = delete
+  let page = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -1407,7 +1408,7 @@ async fn receive_undo_delete_post(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
+  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
 
   let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
 
@@ -1461,7 +1462,7 @@ async fn receive_undo_remove_post(
     .unwrap()
     .to_string();
 
-  let mut page = remove
+  let page = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -1473,7 +1474,7 @@ async fn receive_undo_remove_post(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id;
+  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
 
   let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
 
@@ -1527,7 +1528,7 @@ async fn receive_undo_delete_community(
     .unwrap()
     .to_string();
 
-  let mut group = delete
+  let group = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -1539,7 +1540,7 @@ async fn receive_undo_delete_community(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
+  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
     .await?
     .actor_id;
 
@@ -1604,7 +1605,7 @@ async fn receive_undo_remove_community(
     .unwrap()
     .to_string();
 
-  let mut group = remove
+  let group = remove
     .remove_props
     .get_object_base_box()
     .to_owned()
@@ -1616,7 +1617,7 @@ async fn receive_undo_remove_community(
 
   insert_activity(mod_.id, remove, false, pool).await?;
 
-  let community_actor_id = CommunityForm::from_apub(&mut group, client, pool)
+  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
     .await?
     .actor_id;
 
@@ -1704,7 +1705,7 @@ async fn receive_undo_like_comment(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = like
+  let note = like
     .like_props
     .get_object_base_box()
     .to_owned()
@@ -1718,7 +1719,7 @@ async fn receive_undo_like_comment(
 
   insert_activity(user.id, like, false, pool).await?;
 
-  let comment = CommentForm::from_apub(&mut note, client, pool).await?;
+  let comment = CommentForm::from_apub(&note, client, pool).await?;
 
   let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
     .await?
@@ -1758,7 +1759,7 @@ async fn receive_undo_like_post(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut page = like
+  let page = like
     .like_props
     .get_object_base_box()
     .to_owned()
@@ -1772,7 +1773,7 @@ async fn receive_undo_like_post(
 
   insert_activity(user.id, like, false, pool).await?;
 
-  let post = PostForm::from_apub(&mut page, client, pool).await?;
+  let post = PostForm::from_apub(&page, client, pool).await?;
 
   let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
     .await?
index a3194355ae29bb75c0e1d24b3c253b6586d19476..2b02486d17a7a4b43962fd7932141266bdf4f619 100644 (file)
@@ -1,12 +1,15 @@
 use crate::{
-  apub::{activities::send_activity, create_apub_response, ActorType, FromApub, PersonExt, ToApub},
-  blocking,
-  convert_datetime,
-  db::{
-    activity::insert_activity,
-    user::{UserForm, User_},
+  api::claims::Claims,
+  apub::{
+    activities::send_activity,
+    create_apub_response,
+    insert_activity,
+    ActorType,
+    FromApub,
+    PersonExt,
+    ToApub,
   },
-  naive_now,
+  blocking,
   routes::DbPoolParam,
   DbPool,
   LemmyError,
@@ -22,6 +25,11 @@ use activitystreams_new::{
 };
 use actix_web::{body::Body, client::Client, web, HttpResponse};
 use failure::_core::str::FromStr;
+use lemmy_db::{
+  naive_now,
+  user::{UserForm, User_},
+};
+use lemmy_utils::convert_datetime;
 use serde::Deserialize;
 
 #[derive(Deserialize)]
@@ -185,8 +193,8 @@ impl ActorType for User_ {
 impl FromApub for UserForm {
   type ApubType = PersonExt;
   /// Parse an ActivityPub person received from another instance into a Lemmy user.
-  async fn from_apub(person: &mut PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
-    let avatar = match person.take_icon() {
+  async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
+    let avatar = match person.icon() {
       Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
         .unwrap()
         .unwrap()
@@ -199,19 +207,19 @@ impl FromApub for UserForm {
 
     Ok(UserForm {
       name: person
-        .take_name()
+        .name()
         .unwrap()
         .as_single_xsd_string()
         .unwrap()
         .into(),
-      preferred_username: person.inner.take_preferred_username(),
+      preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
       password_encrypted: "".to_string(),
       admin: false,
       banned: false,
       email: None,
       avatar,
       updated: person
-        .take_updated()
+        .updated()
         .map(|u| u.as_ref().to_owned().naive_local()),
       show_nsfw: false,
       theme: "".to_string(),
@@ -223,7 +231,7 @@ impl FromApub for UserForm {
       matrix_user_id: None,
       actor_id: person.id().unwrap().to_string(),
       bio: person
-        .take_summary()
+        .summary()
         .map(|s| s.as_single_xsd_string().unwrap().into()),
       local: false,
       private_key: None,
@@ -240,7 +248,7 @@ pub async fn get_apub_user_http(
 ) -> Result<HttpResponse<Body>, LemmyError> {
   let user_name = info.into_inner().user_name;
   let user = blocking(&db, move |conn| {
-    User_::find_by_email_or_username(conn, &user_name)
+    Claims::find_by_email_or_username(conn, &user_name)
   })
   .await??;
   let u = user.to_apub(&db).await?;
index 80280adeb12198710668896e4a197f55aa5c95aa..9bc102a74a368433f95d7635ccc9ecb04584a9da 100644 (file)
@@ -3,19 +3,10 @@ use crate::{
   apub::{
     extensions::signatures::verify,
     fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+    insert_activity,
     FromApub,
   },
   blocking,
-  db::{
-    activity::insert_activity,
-    community::{CommunityFollower, CommunityFollowerForm},
-    private_message::{PrivateMessage, PrivateMessageForm},
-    private_message_view::PrivateMessageView,
-    user::User_,
-    Crud,
-    Followable,
-  },
-  naive_now,
   routes::{ChatServerParam, DbPoolParam},
   websocket::{server::SendUserRoomMessage, UserOperation},
   DbPool,
@@ -26,6 +17,15 @@ use activitystreams::{
   object::Note,
 };
 use actix_web::{client::Client, web, HttpRequest, HttpResponse};
+use lemmy_db::{
+  community::{CommunityFollower, CommunityFollowerForm},
+  naive_now,
+  private_message::{PrivateMessage, PrivateMessageForm},
+  private_message_view::PrivateMessageView,
+  user::User_,
+  Crud,
+  Followable,
+};
 use log::debug;
 use serde::Deserialize;
 use std::fmt::Debug;
@@ -116,7 +116,7 @@ async fn receive_create_private_message(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = create
+  let note = create
     .create_props
     .get_object_base_box()
     .to_owned()
@@ -135,7 +135,7 @@ async fn receive_create_private_message(
 
   insert_activity(user.id, create, false, pool).await?;
 
-  let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
+  let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let inserted_private_message = blocking(pool, move |conn| {
     PrivateMessage::create(conn, &private_message)
@@ -168,7 +168,7 @@ async fn receive_update_private_message(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = update
+  let note = update
     .update_props
     .get_object_base_box()
     .to_owned()
@@ -187,7 +187,7 @@ async fn receive_update_private_message(
 
   insert_activity(user.id, update, false, pool).await?;
 
-  let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
+  let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let private_message_ap_id = private_message_form.ap_id.clone();
   let private_message = blocking(pool, move |conn| {
@@ -228,7 +228,7 @@ async fn receive_delete_private_message(
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let mut note = delete
+  let note = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -247,7 +247,7 @@ async fn receive_delete_private_message(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
+  let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let private_message_ap_id = private_message_form.ap_id;
   let private_message = blocking(pool, move |conn| {
@@ -308,7 +308,7 @@ async fn receive_undo_delete_private_message(
     .to_owned()
     .into_concrete::<Delete>()?;
 
-  let mut note = delete
+  let note = delete
     .delete_props
     .get_object_base_box()
     .to_owned()
@@ -327,7 +327,7 @@ async fn receive_undo_delete_private_message(
 
   insert_activity(user.id, delete, false, pool).await?;
 
-  let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?;
+  let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let private_message_ap_id = private_message.ap_id.clone();
   let private_message_id = blocking(pool, move |conn| {
similarity index 86%
rename from server/src/db/code_migrations.rs
rename to server/src/code_migrations.rs
index 1810fae29a568cef72dbc38a13a5592f06afbfcf..b28e120a1ddb021efff7ac769cdae5f28bcf2fd6 100644 (file)
@@ -1,18 +1,16 @@
 // This is for db migrations that require code
-use super::{
+use crate::LemmyError;
+use diesel::*;
+use lemmy_db::{
   comment::Comment,
   community::{Community, CommunityForm},
+  naive_now,
   post::Post,
   private_message::PrivateMessage,
   user::{UserForm, User_},
+  Crud,
 };
-use crate::{
-  apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
-  db::Crud,
-  naive_now,
-  LemmyError,
-};
-use diesel::*;
+use lemmy_utils::{generate_actor_keypair, make_apub_endpoint, EndpointType};
 use log::info;
 
 pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
@@ -26,7 +24,7 @@ pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
 }
 
 fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
-  use crate::schema::user_::dsl::*;
+  use lemmy_db::schema::user_::dsl::*;
 
   info!("Running user_updates_2020_04_02");
 
@@ -77,7 +75,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
 }
 
 fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
-  use crate::schema::community::dsl::*;
+  use lemmy_db::schema::community::dsl::*;
 
   info!("Running community_updates_2020_04_02");
 
@@ -121,7 +119,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
 }
 
 fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
-  use crate::schema::post::dsl::*;
+  use lemmy_db::schema::post::dsl::*;
 
   info!("Running post_updates_2020_04_03");
 
@@ -134,7 +132,8 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
   sql_query("alter table post disable trigger refresh_post").execute(conn)?;
 
   for cpost in &incorrect_posts {
-    Post::update_ap_id(&conn, cpost.id)?;
+    let apub_id = make_apub_endpoint(EndpointType::Post, &cpost.id.to_string()).to_string();
+    Post::update_ap_id(&conn, cpost.id, apub_id)?;
   }
 
   info!("{} post rows updated.", incorrect_posts.len());
@@ -145,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
 }
 
 fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
-  use crate::schema::comment::dsl::*;
+  use lemmy_db::schema::comment::dsl::*;
 
   info!("Running comment_updates_2020_04_03");
 
@@ -158,7 +157,8 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
   sql_query("alter table comment disable trigger refresh_comment").execute(conn)?;
 
   for ccomment in &incorrect_comments {
-    Comment::update_ap_id(&conn, ccomment.id)?;
+    let apub_id = make_apub_endpoint(EndpointType::Comment, &ccomment.id.to_string()).to_string();
+    Comment::update_ap_id(&conn, ccomment.id, apub_id)?;
   }
 
   sql_query("alter table comment enable trigger refresh_comment").execute(conn)?;
@@ -169,7 +169,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
 }
 
 fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
-  use crate::schema::private_message::dsl::*;
+  use lemmy_db::schema::private_message::dsl::*;
 
   info!("Running private_message_updates_2020_05_05");
 
@@ -180,7 +180,8 @@ fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyEr
     .load::<PrivateMessage>(conn)?;
 
   for cpm in &incorrect_pms {
-    PrivateMessage::update_ap_id(&conn, cpm.id)?;
+    let apub_id = make_apub_endpoint(EndpointType::PrivateMessage, &cpm.id.to_string()).to_string();
+    PrivateMessage::update_ap_id(&conn, cpm.id, apub_id)?;
   }
 
   info!("{} private message rows updated.", incorrect_pms.len());
index 08c3fa98a145c4aa6742a1b7559db3d875676c43..4795cf01ee7bf0afefd138f4241d691492c9d8f9 100644 (file)
@@ -5,76 +5,34 @@ pub extern crate strum_macros;
 pub extern crate lazy_static;
 #[macro_use]
 pub extern crate failure;
-#[macro_use]
-pub extern crate diesel;
 pub extern crate actix;
 pub extern crate actix_web;
 pub extern crate bcrypt;
 pub extern crate chrono;
-pub extern crate comrak;
+pub extern crate diesel;
 pub extern crate dotenv;
 pub extern crate jsonwebtoken;
-pub extern crate lettre;
-pub extern crate lettre_email;
 extern crate log;
 pub extern crate openssl;
-pub extern crate rand;
-pub extern crate regex;
 pub extern crate rss;
 pub extern crate serde;
 pub extern crate serde_json;
 pub extern crate sha2;
 pub extern crate strum;
 
-pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
-where
-  F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
-  T: Send + 'static,
-{
-  let pool = pool.clone();
-  let res = actix_web::web::block(move || {
-    let conn = pool.get()?;
-    let res = (f)(&conn);
-    Ok(res) as Result<_, LemmyError>
-  })
-  .await?;
-
-  Ok(res)
-}
-
 pub mod api;
 pub mod apub;
-pub mod db;
+pub mod code_migrations;
 pub mod rate_limit;
 pub mod request;
 pub mod routes;
-pub mod schema;
-pub mod settings;
 pub mod version;
 pub mod websocket;
 
-use crate::{
-  request::{retry, RecvError},
-  settings::Settings,
-};
+use crate::request::{retry, RecvError};
 use actix_web::{client::Client, dev::ConnectionInfo};
-use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
-use itertools::Itertools;
-use lettre::{
-  smtp::{
-    authentication::{Credentials, Mechanism},
-    extension::ClientId,
-    ConnectionReuseParameters,
-  },
-  ClientSecurity,
-  SmtpClient,
-  Transport,
-};
-use lettre_email::Email;
 use log::error;
 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
-use rand::{distributions::Alphanumeric, thread_rng, Rng};
-use regex::{Regex, RegexBuilder};
 use serde::Deserialize;
 
 pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
@@ -89,14 +47,6 @@ pub struct LemmyError {
   inner: failure::Error,
 }
 
-impl std::fmt::Display for LemmyError {
-  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-    self.inner.fmt(f)
-  }
-}
-
-impl actix_web::error::ResponseError for LemmyError {}
-
 impl<T> From<T> for LemmyError
 where
   T: Into<failure::Error>,
@@ -106,113 +56,13 @@ where
   }
 }
 
-pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
-  DateTime::<Utc>::from_utc(ndt, Utc)
-}
-
-pub fn naive_now() -> NaiveDateTime {
-  chrono::prelude::Utc::now().naive_utc()
-}
-
-pub fn naive_from_unix(time: i64) -> NaiveDateTime {
-  NaiveDateTime::from_timestamp(time, 0)
-}
-
-pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
-  let now = Local::now();
-  DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
-}
-
-pub fn is_email_regex(test: &str) -> bool {
-  EMAIL_REGEX.is_match(test)
-}
-
-pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
-  let response = retry(|| client.get(test).send()).await?;
-
-  if response
-    .headers()
-    .get("Content-Type")
-    .ok_or_else(|| format_err!("No Content-Type header"))?
-    .to_str()?
-    .starts_with("image/")
-  {
-    Ok(())
-  } else {
-    Err(format_err!("Not an image type.").into())
-  }
-}
-
-pub fn remove_slurs(test: &str) -> String {
-  SLUR_REGEX.replace_all(test, "*removed*").to_string()
-}
-
-pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
-  let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
-
-  // Unique
-  matches.sort_unstable();
-  matches.dedup();
-
-  if matches.is_empty() {
-    Ok(())
-  } else {
-    Err(matches)
+impl std::fmt::Display for LemmyError {
+  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+    self.inner.fmt(f)
   }
 }
 
-pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
-  let start = "No slurs - ";
-  let combined = &slurs.join(", ");
-  [start, combined].concat()
-}
-
-pub fn generate_random_string() -> String {
-  thread_rng().sample_iter(&Alphanumeric).take(30).collect()
-}
-
-pub fn send_email(
-  subject: &str,
-  to_email: &str,
-  to_username: &str,
-  html: &str,
-) -> Result<(), String> {
-  let email_config = Settings::get().email.ok_or("no_email_setup")?;
-
-  let email = Email::builder()
-    .to((to_email, to_username))
-    .from(email_config.smtp_from_address.to_owned())
-    .subject(subject)
-    .html(html)
-    .build()
-    .unwrap();
-
-  let mailer = if email_config.use_tls {
-    SmtpClient::new_simple(&email_config.smtp_server).unwrap()
-  } else {
-    SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
-  }
-  .hello_name(ClientId::Domain(Settings::get().hostname))
-  .smtp_utf8(true)
-  .authentication_mechanism(Mechanism::Plain)
-  .connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
-  let mailer = if let (Some(login), Some(password)) =
-    (&email_config.smtp_login, &email_config.smtp_password)
-  {
-    mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
-  } else {
-    mailer
-  };
-
-  let mut transport = mailer.transport();
-  let result = transport.send(email.into());
-  transport.close();
-
-  match result {
-    Ok(_) => Ok(()),
-    Err(e) => Err(e.to_string()),
-  }
-}
+impl actix_web::error::ResponseError for LemmyError {}
 
 #[derive(Deserialize, Debug)]
 pub struct IframelyResponse {
@@ -319,8 +169,20 @@ async fn fetch_iframely_and_pictrs_data(
   }
 }
 
-pub fn markdown_to_html(text: &str) -> String {
-  comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
+pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
+  let response = retry(|| client.get(test).send()).await?;
+
+  if response
+    .headers()
+    .get("Content-Type")
+    .ok_or_else(|| format_err!("No Content-Type header"))?
+    .to_str()?
+    .starts_with("image/")
+  {
+    Ok(())
+  } else {
+    Err(format_err!("Not an image type.").into())
+  }
 }
 
 pub fn get_ip(conn_info: &ConnectionInfo) -> String {
@@ -333,127 +195,37 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
     .to_string()
 }
 
-// TODO nothing is done with community / group webfingers yet, so just ignore those for now
-#[derive(Clone, PartialEq, Eq, Hash)]
-pub struct MentionData {
-  pub name: String,
-  pub domain: String,
-}
-
-impl MentionData {
-  pub fn is_local(&self) -> bool {
-    Settings::get().hostname.eq(&self.domain)
-  }
-  pub fn full_name(&self) -> String {
-    format!("@{}@{}", &self.name, &self.domain)
-  }
-}
-
-pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
-  let mut out: Vec<MentionData> = Vec::new();
-  for caps in WEBFINGER_USER_REGEX.captures_iter(text) {
-    out.push(MentionData {
-      name: caps["name"].to_string(),
-      domain: caps["domain"].to_string(),
-    });
-  }
-  out.into_iter().unique().collect()
-}
-
-pub fn is_valid_username(name: &str) -> bool {
-  VALID_USERNAME_REGEX.is_match(name)
-}
+pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
+where
+  F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
+  T: Send + 'static,
+{
+  let pool = pool.clone();
+  let res = actix_web::web::block(move || {
+    let conn = pool.get()?;
+    let res = (f)(&conn);
+    Ok(res) as Result<_, LemmyError>
+  })
+  .await?;
 
-pub fn is_valid_community_name(name: &str) -> bool {
-  VALID_COMMUNITY_NAME_REGEX.is_match(name)
+  Ok(res)
 }
 
 #[cfg(test)]
 mod tests {
-  use crate::{
-    is_email_regex,
-    is_image_content_type,
-    is_valid_community_name,
-    is_valid_username,
-    remove_slurs,
-    scrape_text_for_mentions,
-    slur_check,
-    slurs_vec_to_str,
-  };
-
-  #[test]
-  fn test_mentions_regex() {
-    let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
-    let mentions = scrape_text_for_mentions(text);
-
-    assert_eq!(mentions[0].name, "tedu".to_string());
-    assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
-    assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
-  }
+  use crate::is_image_content_type;
 
   #[test]
   fn test_image() {
     actix_rt::System::new("tset_image").block_on(async move {
-        let client = actix_web::client::Client::default();
-        assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
-        assert!(is_image_content_type(&client,
-                "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
-        )
-            .await.is_err()
-        );
-      });
-  }
-
-  #[test]
-  fn test_email() {
-    assert!(is_email_regex("gush@gmail.com"));
-    assert!(!is_email_regex("nada_neutho"));
-  }
-
-  #[test]
-  fn test_valid_register_username() {
-    assert!(is_valid_username("Hello_98"));
-    assert!(is_valid_username("ten"));
-    assert!(!is_valid_username("Hello-98"));
-    assert!(!is_valid_username("a"));
-    assert!(!is_valid_username(""));
-  }
-
-  #[test]
-  fn test_valid_community_name() {
-    assert!(is_valid_community_name("example"));
-    assert!(is_valid_community_name("example_community"));
-    assert!(!is_valid_community_name("Example"));
-    assert!(!is_valid_community_name("Ex"));
-    assert!(!is_valid_community_name(""));
-  }
-
-  #[test]
-  fn test_slur_filter() {
-    let test =
-      "coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
-    let slur_free = "No slurs here";
-    assert_eq!(
-      remove_slurs(&test),
-      "*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
-        .to_string()
-    );
-
-    let has_slurs_vec = vec![
-      "Niggerz",
-      "coons",
-      "dindu",
-      "ladyboy",
-      "retardeds",
-      "tranny",
-    ];
-    let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
-
-    assert_eq!(slur_check(test), Err(has_slurs_vec));
-    assert_eq!(slur_check(slur_free), Ok(()));
-    if let Err(slur_vec) = slur_check(test) {
-      assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
-    }
+      let client = actix_web::client::Client::default();
+      assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
+      assert!(is_image_content_type(&client,
+                                    "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
+      )
+        .await.is_err()
+      );
+    });
   }
 
   // These helped with testing
@@ -470,21 +242,4 @@ mod tests {
   //   let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
   //   assert!(res_other.is_err());
   // }
-
-  // #[test]
-  // fn test_send_email() {
-  //  let result =  send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
-  //   assert!(result.is_ok());
-  // }
-}
-
-lazy_static! {
-  static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
-  static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
-  static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
-  // TODO keep this old one, it didn't work with port well tho
-  // static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
-  static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
-  static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
-  static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
 }
index 30be711fa5bf7014380a4b5293a5e286bf332f46..7689d7ad1aa363e87468ce6a0e5ff2870cffafca 100644 (file)
@@ -22,22 +22,20 @@ use diesel::{
   r2d2::{ConnectionManager, Pool},
   PgConnection,
 };
+use lemmy_db::get_database_url_from_env;
 use lemmy_server::{
   blocking,
-  db::code_migrations::run_advanced_migrations,
+  code_migrations::run_advanced_migrations,
   rate_limit::{rate_limiter::RateLimiter, RateLimit},
   routes::{api, federation, feeds, index, nodeinfo, webfinger},
-  settings::Settings,
   websocket::server::*,
   LemmyError,
 };
-use regex::Regex;
+use lemmy_utils::{settings::Settings, CACHE_CONTROL_REGEX};
 use std::sync::Arc;
 use tokio::sync::Mutex;
 
 lazy_static! {
-  static ref CACHE_CONTROL_REGEX: Regex =
-    Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
   // static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 365 * 24 * 60 * 60);
   // Test out 1 hour here, this is breaking some things
   static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 60 * 60);
@@ -51,11 +49,15 @@ async fn main() -> Result<(), LemmyError> {
   let settings = Settings::get();
 
   // Set up the r2d2 connection pool
-  let manager = ConnectionManager::<PgConnection>::new(&settings.get_database_url());
+  let db_url = match get_database_url_from_env() {
+    Ok(url) => url,
+    Err(_) => settings.get_database_url(),
+  };
+  let manager = ConnectionManager::<PgConnection>::new(&db_url);
   let pool = Pool::builder()
     .max_size(settings.database.pool_size)
     .build(manager)
-    .unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url()));
+    .unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
 
   // Run the migrations from code
   blocking(&pool, move |conn| {
index e49a527e8080b04b395cb752032f3d9d8d0bc76d..513c923c6182b006ebedff3cf8c57ac1cbf63f0b 100644 (file)
@@ -1,7 +1,8 @@
-use super::{IPAddr, Settings};
-use crate::{get_ip, settings::RateLimitConfig, LemmyError};
+use super::IPAddr;
+use crate::{get_ip, LemmyError};
 use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
 use futures::future::{ok, Ready};
+use lemmy_utils::settings::{RateLimitConfig, Settings};
 use rate_limiter::{RateLimitType, RateLimiter};
 use std::{
   future::Future,
index fe6e3365789b2bd30a19596adb950ad821f43c0b..cd4c47803dbfbebfaee98da7f1f4cdc7aa505c15 100644 (file)
@@ -1,21 +1,23 @@
-use crate::{
-  apub::{
-    comment::get_apub_comment,
-    community::*,
-    community_inbox::community_inbox,
-    post::get_apub_post,
-    shared_inbox::shared_inbox,
-    user::*,
-    user_inbox::user_inbox,
-    APUB_JSON_CONTENT_TYPE,
-  },
-  settings::Settings,
+use crate::apub::{
+  comment::get_apub_comment,
+  community::*,
+  community_inbox::community_inbox,
+  post::get_apub_post,
+  shared_inbox::shared_inbox,
+  user::*,
+  user_inbox::user_inbox,
+  APUB_JSON_CONTENT_TYPE,
 };
 use actix_web::*;
+use http_signature_normalization_actix::digest::middleware::VerifyDigest;
+use lemmy_utils::settings::Settings;
+use sha2::{Digest, Sha256};
 
 pub fn config(cfg: &mut web::ServiceConfig) {
   if Settings::get().federation.enabled {
     println!("federation enabled, host is {}", Settings::get().hostname);
+    let digest_verifier = VerifyDigest::new(Sha256::new());
+
     cfg
       .service(
         web::scope("/")
@@ -38,8 +40,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
           .route("/comment/{comment_id}", web::get().to(get_apub_comment)),
       )
       // Inboxes dont work with the header guard for some reason.
-      .route("/c/{community_name}/inbox", web::post().to(community_inbox))
-      .route("/u/{user_name}/inbox", web::post().to(user_inbox))
-      .route("/inbox", web::post().to(shared_inbox));
+      .service(
+        web::resource("/c/{community_name}/inbox")
+          .wrap(digest_verifier.clone())
+          .route(web::post().to(community_inbox)),
+      )
+      .service(
+        web::resource("/u/{user_name}/inbox")
+          .wrap(digest_verifier.clone())
+          .route(web::post().to(user_inbox)),
+      )
+      .service(
+        web::resource("/inbox")
+          .wrap(digest_verifier)
+          .route(web::post().to(shared_inbox)),
+      );
   }
 }
index a1c2ba58f98ec354fb2385b0c72dc3bc717605bd..1322feb440e04da295f5dff326ab48e5ffed73e3 100644 (file)
@@ -1,26 +1,21 @@
-use crate::{
-  blocking,
-  db::{
-    comment_view::{ReplyQueryBuilder, ReplyView},
-    community::Community,
-    post_view::{PostQueryBuilder, PostView},
-    site_view::SiteView,
-    user::{Claims, User_},
-    user_mention_view::{UserMentionQueryBuilder, UserMentionView},
-    ListingType,
-    SortType,
-  },
-  markdown_to_html,
-  routes::DbPoolParam,
-  settings::Settings,
-  LemmyError,
-};
+use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
 use actix_web::{error::ErrorBadRequest, *};
 use chrono::{DateTime, NaiveDateTime, Utc};
 use diesel::{
   r2d2::{ConnectionManager, Pool},
   PgConnection,
 };
+use lemmy_db::{
+  comment_view::{ReplyQueryBuilder, ReplyView},
+  community::Community,
+  post_view::{PostQueryBuilder, PostView},
+  site_view::SiteView,
+  user::User_,
+  user_mention_view::{UserMentionQueryBuilder, UserMentionView},
+  ListingType,
+  SortType,
+};
+use lemmy_utils::{markdown_to_html, settings::Settings};
 use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
 use serde::Deserialize;
 use std::str::FromStr;
@@ -131,7 +126,7 @@ fn get_feed_user(
 ) -> Result<ChannelBuilder, LemmyError> {
   let site_view = SiteView::read(&conn)?;
   let user = User_::find_by_username(&conn, &user_name)?;
-  let user_url = user.get_profile_url();
+  let user_url = user.get_profile_url(&Settings::get().hostname);
 
   let posts = PostQueryBuilder::create(&conn)
     .listing_type(ListingType::All)
index 2f462aa5f12492446e4ba5a598ee146297396ba5..b579a1958384fb625680319b77370fe3578c17c5 100644 (file)
@@ -1,6 +1,6 @@
-use crate::settings::Settings;
 use actix_files::NamedFile;
 use actix_web::*;
+use lemmy_utils::settings::Settings;
 
 pub fn config(cfg: &mut web::ServiceConfig) {
   cfg
index ff728fe3e059f4d9bf83ba14348e7169deb7fb1a..5094c2f15e571342376ddb0f46962fa7f37adc77 100644 (file)
@@ -1,13 +1,7 @@
-use crate::{
-  apub::get_apub_protocol_string,
-  blocking,
-  db::site_view::SiteView,
-  routes::DbPoolParam,
-  version,
-  LemmyError,
-  Settings,
-};
+use crate::{blocking, routes::DbPoolParam, version, LemmyError};
 use actix_web::{body::Body, error::ErrorBadRequest, *};
+use lemmy_db::site_view::SiteView;
+use lemmy_utils::{get_apub_protocol_string, settings::Settings};
 use serde::{Deserialize, Serialize};
 use url::Url;
 
index af021dd5f5f71b3ccb7068443962648e7255baf6..e616de0e8ed9bd2116f5fa2c4c5e7c79eb0561b2 100644 (file)
@@ -1,12 +1,7 @@
-use crate::{
-  blocking,
-  db::{community::Community, user::User_},
-  routes::DbPoolParam,
-  LemmyError,
-  Settings,
-};
+use crate::{blocking, routes::DbPoolParam, LemmyError};
 use actix_web::{error::ErrorBadRequest, web::Query, *};
-use regex::Regex;
+use lemmy_db::{community::Community, user::User_};
+use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX};
 use serde::{Deserialize, Serialize};
 
 #[derive(Deserialize)]
@@ -40,19 +35,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
   }
 }
 
-lazy_static! {
-  static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
-    "^group:([a-z0-9_]{{3, 20}})@{}$",
-    Settings::get().hostname
-  ))
-  .unwrap();
-  static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
-    "^acct:([a-z0-9_]{{3, 20}})@{}$",
-    Settings::get().hostname
-  ))
-  .unwrap();
-}
-
 /// Responds to webfinger requests of the following format. There isn't any real documentation for
 /// this, but it described in this blog post:
 /// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social
index cecd8dc6808492df264f93d1a05b31d6bb2b730d..59864dd0dacf97dcb8cdf5e94e7b8ccfbb898f4a 100644 (file)
@@ -1 +1 @@
-pub const VERSION: &str = "v0.7.13";
+pub const VERSION: &str = "v0.7.21";
diff --git a/server/test.sh b/server/test.sh
new file mode 100755 (executable)
index 0000000..9a8e445
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh
+export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+diesel migration run
+export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+RUST_TEST_THREADS=1 cargo test --workspace
diff --git a/ui/assets/css/choices.min.css b/ui/assets/css/choices.min.css
new file mode 100644 (file)
index 0000000..19adaba
--- /dev/null
@@ -0,0 +1 @@
+.choices{position:relative;margin-bottom:24px;font-size:16px}.choices:focus{outline:0}.choices:last-child{margin-bottom:0}.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;-ms-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices [hidden]{display:none!important}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:100%;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.5}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #00bcd4}.choices[data-type*=select-one] .choices__item[data-value=''] .choices__button{display:none}.choices[data-type*=select-one]:after{content:'';height:0;width:0;border-style:solid;border-color:#333 transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open:after{border-color:transparent transparent #333;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]:after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0 -4px 0 8px;padding-left:16px;border-left:1px solid #008fa1;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);background-size:8px;width:8px;line-height:1;opacity:.75;border-radius:0}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:100%;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:44px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#00bcd4;border:1px solid #00a5bb;color:#fff;word-break:break-all;box-sizing:border-box}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#00a5bb;border:1px solid #008fa1}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown{visibility:hidden;z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all;will-change:visibility}.choices__list--dropdown.is-active{visibility:visible}.is-open .choices__list--dropdown{border-color:#b7b7b7}.is-flipped .choices__list--dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable{padding-right:100px}.choices__list--dropdown .choices__item--selectable:after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable:after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted:after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-ms-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus,.choices__input:focus{outline:0}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5}
\ No newline at end of file
index fd65148c7f8f58d31dcfba62459ecd19bd9ec8db..9f744fb1c837c5530e1ce4fb8e9ae2e7f61cfaf7 100644 (file)
@@ -264,3 +264,10 @@ pre {
   width: 0px !important;
   padding: 0 !important;
  }
+
+br.big {
+  display: block;
+  content: "";
+  margin-top: 1rem;
+}
+
diff --git a/ui/assets/css/selectr.min.css b/ui/assets/css/selectr.min.css
deleted file mode 100644 (file)
index 78bab83..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!\r
- * Selectr 2.4.13\r
- * http://mobius.ovh/docs/selectr\r
- *\r
- * Released under the MIT license\r
- */\r
-.selectr-container li,.selectr-option,.selectr-tag{list-style:none}.selectr-container{position:relative}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:'';-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:none;border-radius:10px;background:#acb7bf}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active,.selectr-input-container.active .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:' ';background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;}.input-tag,.taggable .selectr-label{width:auto}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;cursor:pointer;font-weight:400}.has-selected .selectr-placeholder,.selectr-empty,.selectr-option.excluded{display:none}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px}.selectr-tag-input{border:none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:'';-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running spin;-webkit-animation:.5s linear 0s normal forwards infinite running spin;animation:.5s linear 0s normal forwards infinite running spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1}\r
index 00d28cafdd59c3dee187a3320a060e11c6b1040c..48f7ecdbacbe19990928073c9039cbc39eb54006 100644 (file)
@@ -6,12 +6,10 @@ const {
   WebIndexPlugin,
   QuantumPlugin,
 } = require('fuse-box');
-// const transformInferno = require('../../dist').default
 const transformInferno = require('ts-transform-inferno').default;
 const transformClasscat = require('ts-transform-classcat').default;
 let fuse, app;
 let isProduction = false;
-// var setVersion = require('./set_version.js').setVersion;
 
 Sparky.task('config', _ => {
   fuse = new FuseBox({
@@ -45,18 +43,18 @@ Sparky.task('config', _ => {
   });
   app = fuse.bundle('app').instructions('>index.tsx');
 });
-// Sparky.task('version', _ => setVersion());
 Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
 Sparky.task('env', _ => (isProduction = true));
 Sparky.task('copy-assets', () =>
   Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
 );
 Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
-  fuse.dev();
+  fuse.dev({
+    fallback: 'index.html',
+  });
   app.hmr().watch();
   return fuse.run();
 });
 Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
-  // fuse.dev({ reload: true }); // remove after demo
   return fuse.run();
 });
index 0101ce13a59e29f25598e4ad4e6c4d84acb8fe8c..1bd07e86378a1eeb5d6ee54d36f500ded3d8c412 100644 (file)
@@ -15,7 +15,6 @@
   },
   "keywords": [],
   "dependencies": {
-    "@joeattardi/emoji-button": "^2.12.1",
     "@types/autosize": "^3.0.6",
     "@types/js-cookie": "^2.2.6",
     "@types/jwt-decode": "^2.2.1",
@@ -24,6 +23,7 @@
     "@types/node": "^13.11.1",
     "autosize": "^4.0.2",
     "bootswatch": "^4.3.1",
+    "choices.js": "^9.0.1",
     "classcat": "^4.0.2",
     "dotenv": "^8.2.0",
     "emoji-short-name": "^1.0.0",
@@ -37,7 +37,6 @@
     "markdown-it": "^10.0.0",
     "markdown-it-container": "^2.0.0",
     "markdown-it-emoji": "^1.4.0",
-    "mobius1-selectr": "^2.4.13",
     "moment": "^2.24.0",
     "node-fetch": "^2.6.0",
     "prettier": "^2.0.4",
@@ -47,7 +46,6 @@
     "tippy.js": "^6.1.1",
     "toastify-js": "^1.7.0",
     "tributejs": "^5.1.3",
-    "twemoji": "^12.1.2",
     "ws": "^7.2.3"
   },
   "devDependencies": {
@@ -72,7 +70,7 @@
   "engineStrict": true,
   "husky": {
     "hooks": {
-      "pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
+      "pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
     }
   },
   "lint-staged": {
diff --git a/ui/src/components/cake-day.tsx b/ui/src/components/cake-day.tsx
new file mode 100644 (file)
index 0000000..f28be33
--- /dev/null
@@ -0,0 +1,25 @@
+import { Component } from 'inferno';
+import { i18n } from '../i18next';
+
+interface CakeDayProps {
+  creatorName: string;
+}
+
+export class CakeDay extends Component<CakeDayProps, any> {
+  render() {
+    return (
+      <div
+        className={`mx-2 d-inline-block unselectable pointer`}
+        data-tippy-content={this.cakeDayTippy()}
+      >
+        <svg class="icon icon-inline">
+          <use xlinkHref="#icon-cake"></use>
+        </svg>
+      </div>
+    );
+  }
+
+  cakeDayTippy(): string {
+    return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
+  }
+}
index 770c127c7f45e54aa697479721bd60bdcd7477a6..72a4f398b5a6bf469cb521abaeb09e1ab268f032 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { Prompt } from 'inferno-router';
@@ -17,7 +18,6 @@ import {
   toast,
   setupTribute,
   wsJsonToRes,
-  emojiPicker,
   pictrsDeleteToast,
 } from '../utils';
 import { WebSocketService, UserService } from '../services';
@@ -25,6 +25,7 @@ import autosize from 'autosize';
 import Tribute from 'tributejs/src/Tribute.js';
 import emojiShortName from 'emoji-short-name';
 import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface CommentFormProps {
   postId?: number;
@@ -32,6 +33,7 @@ interface CommentFormProps {
   onReplyCancel?(): any;
   edit?: boolean;
   disabled?: boolean;
+  focus?: boolean;
 }
 
 interface CommentFormState {
@@ -72,7 +74,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     super(props, context);
 
     this.tribute = setupTribute();
-    this.setupEmojiPicker();
 
     this.state = this.emptyState;
 
@@ -98,14 +99,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   }
 
   componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.commentForm.content = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
+    let textarea: any = document.getElementById(this.id);
+    if (textarea) {
+      autosize(textarea);
+      this.tribute.attach(textarea);
+      textarea.addEventListener('tribute-replaced', () => {
+        this.state.commentForm.content = textarea.value;
+        this.setState(this.state);
+        autosize.update(textarea);
+      });
+
+      // Quoting of selected text
+      let selectedText = window.getSelection().toString();
+      if (selectedText) {
+        let quotedText =
+          selectedText
+            .split('\n')
+            .map(t => `> ${t}`)
+            .join('\n') + '\n\n';
+        this.state.commentForm.content = quotedText;
+        this.setState(this.state);
+        // Not sure why this needs a delay
+        setTimeout(() => autosize.update(textarea), 10);
+      }
+
+      if (this.props.focus) {
+        textarea.focus();
+      }
+    }
   }
 
   componentDidUpdate() {
@@ -128,133 +149,126 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
           when={this.state.commentForm.content}
           message={i18n.t('block_leaving')}
         />
-        <form
-          id={this.formId}
-          onSubmit={linkEvent(this, this.handleCommentSubmit)}
-        >
-          <div class="form-group row">
-            <div className={`col-sm-12`}>
-              <textarea
-                id={this.id}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                value={this.state.commentForm.content}
-                onInput={linkEvent(this, this.handleCommentContentChange)}
-                onPaste={linkEvent(this, this.handleImageUploadPaste)}
-                required
-                disabled={this.props.disabled}
-                rows={2}
-                maxLength={10000}
-              />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(
-                    this.state.commentForm.content
-                  )}
+        {UserService.Instance.user ? (
+          <form
+            id={this.formId}
+            onSubmit={linkEvent(this, this.handleCommentSubmit)}
+          >
+            <div class="form-group row">
+              <div className={`col-sm-12`}>
+                <textarea
+                  id={this.id}
+                  className={`form-control ${
+                    this.state.previewMode && 'd-none'
+                  }`}
+                  value={this.state.commentForm.content}
+                  onInput={linkEvent(this, this.handleCommentContentChange)}
+                  onPaste={linkEvent(this, this.handleImageUploadPaste)}
+                  required
+                  disabled={this.props.disabled}
+                  rows={2}
+                  maxLength={10000}
                 />
-              )}
-            </div>
-          </div>
-          <div class="row">
-            <div class="col-sm-12">
-              <button
-                type="submit"
-                class="btn btn-sm btn-secondary mr-2"
-                disabled={this.props.disabled || this.state.loading}
-              >
-                {this.state.loading ? (
-                  <svg class="icon icon-spinner spin">
-                    <use xlinkHref="#icon-spinner"></use>
-                  </svg>
-                ) : (
-                  <span>{this.state.buttonTitle}</span>
+                {this.state.previewMode && (
+                  <div
+                    className="card card-body md-div"
+                    dangerouslySetInnerHTML={mdToHtml(
+                      this.state.commentForm.content
+                    )}
+                  />
                 )}
-              </button>
-              {this.state.commentForm.content && (
-                <button
-                  className={`btn btn-sm mr-2 btn-secondary ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
-              {this.props.node && (
+              </div>
+            </div>
+            <div class="row">
+              <div class="col-sm-12">
                 <button
-                  type="button"
+                  type="submit"
                   class="btn btn-sm btn-secondary mr-2"
-                  onClick={linkEvent(this, this.handleReplyCancel)}
+                  disabled={this.props.disabled || this.state.loading}
                 >
-                  {i18n.t('cancel')}
+                  {this.state.loading ? (
+                    <svg class="icon icon-spinner spin">
+                      <use xlinkHref="#icon-spinner"></use>
+                    </svg>
+                  ) : (
+                    <span>{this.state.buttonTitle}</span>
+                  )}
                 </button>
-              )}
-              <a
-                href={markdownHelpUrl}
-                target="_blank"
-                class="d-inline-block float-right text-muted font-weight-bold"
-                title={i18n.t('formatting_help')}
-                rel="noopener"
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-help-circle"></use>
-                </svg>
-              </a>
-              <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
-                <label
-                  htmlFor={`file-upload-${this.id}`}
-                  className={`${UserService.Instance.user && 'pointer'}`}
-                  data-tippy-content={i18n.t('upload_image')}
+                {this.state.commentForm.content && (
+                  <button
+                    className={`btn btn-sm mr-2 btn-secondary ${
+                      this.state.previewMode && 'active'
+                    }`}
+                    onClick={linkEvent(this, this.handlePreviewToggle)}
+                  >
+                    {i18n.t('preview')}
+                  </button>
+                )}
+                {this.props.node && (
+                  <button
+                    type="button"
+                    class="btn btn-sm btn-secondary mr-2"
+                    onClick={linkEvent(this, this.handleReplyCancel)}
+                  >
+                    {i18n.t('cancel')}
+                  </button>
+                )}
+                <a
+                  href={markdownHelpUrl}
+                  target="_blank"
+                  class="d-inline-block float-right text-muted font-weight-bold"
+                  title={i18n.t('formatting_help')}
+                  rel="noopener"
                 >
                   <svg class="icon icon-inline">
-                    <use xlinkHref="#icon-image"></use>
+                    <use xlinkHref="#icon-help-circle"></use>
                   </svg>
-                </label>
-                <input
-                  id={`file-upload-${this.id}`}
-                  type="file"
-                  accept="image/*,video/*"
-                  name="file"
-                  class="d-none"
-                  disabled={!UserService.Instance.user}
-                  onChange={linkEvent(this, this.handleImageUpload)}
-                />
-              </form>
-              {this.state.imageLoading && (
-                <svg class="icon icon-spinner spin">
-                  <use xlinkHref="#icon-spinner"></use>
-                </svg>
-              )}
-              <span
-                onClick={linkEvent(this, this.handleEmojiPickerClick)}
-                class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
-                data-tippy-content={i18n.t('emoji_picker')}
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-smile"></use>
-                </svg>
-              </span>
+                </a>
+                <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
+                  <label
+                    htmlFor={`file-upload-${this.id}`}
+                    className={`${UserService.Instance.user && 'pointer'}`}
+                    data-tippy-content={i18n.t('upload_image')}
+                  >
+                    <svg class="icon icon-inline">
+                      <use xlinkHref="#icon-image"></use>
+                    </svg>
+                  </label>
+                  <input
+                    id={`file-upload-${this.id}`}
+                    type="file"
+                    accept="image/*,video/*"
+                    name="file"
+                    class="d-none"
+                    disabled={!UserService.Instance.user}
+                    onChange={linkEvent(this, this.handleImageUpload)}
+                  />
+                </form>
+                {this.state.imageLoading && (
+                  <svg class="icon icon-spinner spin">
+                    <use xlinkHref="#icon-spinner"></use>
+                  </svg>
+                )}
+              </div>
             </div>
+          </form>
+        ) : (
+          <div class="alert alert-light" role="alert">
+            <svg class="icon icon-inline mr-2">
+              <use xlinkHref="#icon-alert-triangle"></use>
+            </svg>
+            <T i18nKey="must_login" class="d-inline">
+              #
+              <Link class="alert-link" to="/login">
+                #
+              </Link>
+            </T>
           </div>
-        </form>
+        )}
       </div>
     );
   }
 
-  setupEmojiPicker() {
-    emojiPicker.on('emoji', twemojiHtmlStr => {
-      if (this.state.commentForm.content == null) {
-        this.state.commentForm.content = '';
-      }
-      var el = document.createElement('div');
-      el.innerHTML = twemojiHtmlStr;
-      let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
-      let shortName = `:${emojiShortName[nativeUnicode]}:`;
-      this.state.commentForm.content += shortName;
-      this.setState(this.state);
-    });
-  }
-
   handleFinished(op: UserOperation, data: CommentResponse) {
     let isReply =
       this.props.node !== undefined && data.comment.parent_id !== null;
@@ -302,10 +316,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     i.setState(i.state);
   }
 
-  handleEmojiPickerClick(_i: CommentForm, event: any) {
-    emojiPicker.togglePicker(event.target);
-  }
-
   handleCommentContentChange(i: CommentForm, event: any) {
     i.state.commentForm.content = event.target.value;
     i.setState(i.state);
index 839a01dcc41540aa364f2a548830c4e9402fe4ae..a6b9b7bac1921d888575d0be21fecf65ea9383f0 100644 (file)
@@ -32,6 +32,7 @@ import { MomentTime } from './moment-time';
 import { CommentForm } from './comment-form';
 import { CommentNodes } from './comment-nodes';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import { i18n } from '../i18next';
 
 interface CommentNodeState {
@@ -158,9 +159,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                     id: node.comment.creator_id,
                     local: node.comment.creator_local,
                     actor_id: node.comment.creator_actor_id,
+                    published: node.comment.creator_published,
                   }}
                 />
               </span>
+
               {this.isMod && (
                 <div className="badge badge-light d-none d-sm-inline mr-2">
                   {i18n.t('mod')}
@@ -184,13 +187,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               {this.props.showCommunity && (
                 <>
                   <span class="mx-1">{i18n.t('to')}</span>
-                  <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
-                    {node.comment.community_name}
+                  <CommunityLink
+                    community={{
+                      name: node.comment.community_name,
+                      id: node.comment.community_id,
+                      local: node.comment.community_local,
+                      actor_id: node.comment.community_actor_id,
+                    }}
+                  />
+                  <span class="mx-2">•</span>
+                  <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
+                    {node.comment.post_name}
                   </Link>
                 </>
               )}
-              <div
-                className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
+              <button
+                class="btn btn-sm text-muted"
                 onClick={linkEvent(this, this.handleCommentCollapse)}
               >
                 {this.state.collapsed ? (
@@ -202,9 +214,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                     <use xlinkHref="#icon-minus-square"></use>
                   </svg>
                 )}
-              </div>
-              <span
-                className={`unselectable pointer ${this.scoreColor}`}
+              </button>
+              {/* This is an expanding spacer for mobile */}
+              <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
+              <button
+                className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
                 onClick={linkEvent(node, this.handleCommentUpvote)}
                 data-tippy-content={this.pointsTippy}
               >
@@ -212,7 +226,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                   <use xlinkHref="#icon-zap"></use>
                 </svg>
                 <span class="mr-1">{this.state.score}</span>
-              </span>
+              </button>
               <span className="mr-1">•</span>
               <span>
                 <MomentTime data={node.comment} />
@@ -225,6 +239,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 edit
                 onReplyCancel={this.handleReplyCancel}
                 disabled={this.props.locked}
+                focus
               />
             )}
             {!this.state.showEdit && !this.state.collapsed && (
@@ -693,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             node={node}
             onReplyCancel={this.handleReplyCancel}
             disabled={this.props.locked}
+            focus
           />
         )}
         {node.children && !this.state.collapsed && (
index 441f7bb148686f4ef9bb7e15a7411f4839b09792..ba362accdba1ae07e891ec5aa93dd5072b01c26b 100644 (file)
@@ -13,7 +13,7 @@ import {
   GetSiteResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import { wsJsonToRes, toast } from '../utils';
+import { wsJsonToRes, toast, getPageFromProps } from '../utils';
 import { CommunityLink } from './community-link';
 import { i18n } from '../i18next';
 
@@ -27,12 +27,16 @@ interface CommunitiesState {
   loading: boolean;
 }
 
+interface CommunitiesProps {
+  page: number;
+}
+
 export class Communities extends Component<any, CommunitiesState> {
   private subscription: Subscription;
   private emptyState: CommunitiesState = {
     communities: [],
     loading: true,
-    page: this.getPageFromProps(this.props),
+    page: getPageFromProps(this.props),
   };
 
   constructor(props: any, context: any) {
@@ -50,19 +54,19 @@ export class Communities extends Component<any, CommunitiesState> {
     WebSocketService.Instance.getSite();
   }
 
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
-    if (nextProps.history.action == 'POP') {
-      this.state = this.emptyState;
-      this.state.page = this.getPageFromProps(nextProps);
+  static getDerivedStateFromProps(props: any): CommunitiesProps {
+    return {
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: CommunitiesState) {
+    if (lastState.page !== this.state.page) {
+      this.setState({ loading: true });
       this.refetch();
     }
   }
@@ -160,7 +164,7 @@ export class Communities extends Component<any, CommunitiesState> {
           </button>
         )}
 
-        {this.state.communities.length == communityLimit && (
+        {this.state.communities.length > 0 && (
           <button
             class="btn btn-sm btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
@@ -172,22 +176,17 @@ export class Communities extends Component<any, CommunitiesState> {
     );
   }
 
-  updateUrl() {
-    this.props.history.push(`/communities/page/${this.state.page}`);
+  updateUrl(paramUpdates: CommunitiesProps) {
+    const page = paramUpdates.page || this.state.page;
+    this.props.history.push(`/communities/page/${page}`);
   }
 
   nextPage(i: Communities) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.refetch();
+    i.updateUrl({ page: i.state.page + 1 });
   }
 
   prevPage(i: Communities) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.refetch();
+    i.updateUrl({ page: i.state.page - 1 });
   }
 
   handleUnsubscribe(communityId: number) {
index ff50c3dc72d70f37559c9ff3c14e5a338550f2e3..99b692cacf96694740d3bca8327278fae481328a 100644 (file)
@@ -65,6 +65,18 @@ interface State {
   site: Site;
 }
 
+interface CommunityProps {
+  dataType: DataType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  dataType?: string;
+  sort?: string;
+  page?: number;
+}
+
 export class Community extends Component<any, State> {
   private subscription: Subscription;
   private emptyState: State = {
@@ -143,16 +155,21 @@ export class Community extends Component<any, State> {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): CommunityProps {
+    return {
+      dataType: getDataTypeFromProps(props),
+      sort: getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: State) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.dataType !== this.state.dataType ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state.dataType = getDataTypeFromProps(nextProps);
-      this.state.sort = getSortTypeFromProps(nextProps);
-      this.state.page = getPageFromProps(nextProps);
-      this.setState(this.state);
+      this.setState({ loading: true });
       this.fetchData();
     }
   }
@@ -260,7 +277,7 @@ export class Community extends Component<any, State> {
             {i18n.t('prev')}
           </button>
         )}
-        {this.state.posts.length == fetchLimit && (
+        {this.state.posts.length > 0 && (
           <button
             class="btn btn-sm btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
@@ -273,46 +290,33 @@ export class Community extends Component<any, State> {
   }
 
   nextPage(i: Community) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    i.updateUrl({ page: i.state.page + 1 });
     window.scrollTo(0, 0);
   }
 
   prevPage(i: Community) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    i.updateUrl({ page: i.state.page - 1 });
     window.scrollTo(0, 0);
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
-    this.state.dataType = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
     window.scrollTo(0, 0);
   }
 
-  updateUrl() {
-    let dataTypeStr = DataType[this.state.dataType].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const dataTypeStr =
+      paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
+    const sortStr =
+      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const page = paramUpdates.page || this.state.page;
     this.props.history.push(
-      `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
index 8692989481a933eed9b520d79b028bf39ea4b3ff..3a5d943d4ac64bc81c90056f4958e8860631e103 100644 (file)
@@ -9,7 +9,7 @@ import {
   GetSiteResponse,
 } from '../interfaces';
 import { toast, wsJsonToRes } from '../utils';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
 import { i18n } from '../i18next';
 
 interface CreateCommunityState {
@@ -26,6 +26,11 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
     this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
     this.state = this.emptyState;
 
+    if (!UserService.Instance.user) {
+      toast(i18n.t('not_logged_in'), 'danger');
+      this.context.router.history.push(`/login`);
+    }
+
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
index 348ba0cb8a9d171328e928b6d1b0f94a661a91c6..4554326daaa34de2af329a0cf6253d93a035cf49 100644 (file)
@@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PostForm } from './post-form';
 import { toast, wsJsonToRes } from '../utils';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
 import {
   UserOperation,
   PostFormParams,
@@ -41,6 +41,11 @@ export class CreatePost extends Component<any, CreatePostState> {
     this.handlePostCreate = this.handlePostCreate.bind(this);
     this.state = this.emptyState;
 
+    if (!UserService.Instance.user) {
+      toast(i18n.t('not_logged_in'), 'danger');
+      this.context.router.history.push(`/login`);
+    }
+
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
index 21ed04c7814662fc20db94cad648cf7a11151257..c309cbe3e3ce269df06e1146f9ddd3c7e13842a6 100644 (file)
@@ -2,7 +2,7 @@ import { Component } from 'inferno';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PrivateMessageForm } from './private-message-form';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
 import {
   UserOperation,
   WebSocketJsonResponse,
@@ -20,6 +20,11 @@ export class CreatePrivateMessage extends Component<any, any> {
       this
     );
 
+    if (!UserService.Instance.user) {
+      toast(i18n.t('not_logged_in'), 'danger');
+      this.context.router.history.push(`/login`);
+    }
+
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
index f2539c8103ad17331cabc27b3214d9015737d39c..d16c785d8bafeac01c3446ded7ffb598f18956a0 100644 (file)
@@ -25,6 +25,12 @@ export class DataTypeSelect extends Component<
     this.state = this.emptyState;
   }
 
+  static getDerivedStateFromProps(props: any): DataTypeSelectProps {
+    return {
+      type_: props.type_,
+    };
+  }
+
   render() {
     return (
       <div class="btn-group btn-group-toggle">
@@ -42,8 +48,9 @@ export class DataTypeSelect extends Component<
           {i18n.t('posts')}
         </label>
         <label
-          className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
-            DataType.Comment && 'active'}`}
+          className={`pointer btn btn-sm btn-secondary ${
+            this.state.type_ == DataType.Comment && 'active'
+          }`}
         >
           <input
             type="radio"
@@ -58,8 +65,6 @@ export class DataTypeSelect extends Component<
   }
 
   handleTypeChange(i: DataTypeSelect, event: any) {
-    i.state.type_ = Number(event.target.value);
-    i.setState(i.state);
-    i.props.onChange(i.state.type_);
+    i.props.onChange(Number(event.target.value));
   }
 }
index a88d45c52b4c2efc3b9096631b3d101cfc3a4703..8e148921fe1b1f41070fcfeb3c3f33ae5f32de4b 100644 (file)
@@ -267,6 +267,7 @@ export class Inbox extends Component<any, InboxState> {
               nodes={[{ comment: i }]}
               noIndent
               markable
+              showCommunity
               showContext
               enableDownvotes={this.state.enableDownvotes}
             />
@@ -285,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
           nodes={commentsToFlatNodes(this.state.replies)}
           noIndent
           markable
+          showCommunity
           showContext
           enableDownvotes={this.state.enableDownvotes}
         />
@@ -300,6 +302,7 @@ export class Inbox extends Component<any, InboxState> {
             nodes={[{ comment: mention }]}
             noIndent
             markable
+            showCommunity
             showContext
             enableDownvotes={this.state.enableDownvotes}
           />
@@ -329,12 +332,14 @@ export class Inbox extends Component<any, InboxState> {
             {i18n.t('prev')}
           </button>
         )}
-        <button
-          class="btn btn-sm btn-secondary"
-          onClick={linkEvent(this, this.nextPage)}
-        >
-          {i18n.t('next')}
-        </button>
+        {this.unreadCount() > 0 && (
+          <button
+            class="btn btn-sm btn-secondary"
+            onClick={linkEvent(this, this.nextPage)}
+          >
+            {i18n.t('next')}
+          </button>
+        )}
       </div>
     );
   }
@@ -534,15 +539,19 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   sendUnreadCount() {
-    let count =
+    UserService.Instance.user.unreadCount = this.unreadCount();
+    UserService.Instance.sub.next({
+      user: UserService.Instance.user,
+    });
+  }
+
+  unreadCount(): number {
+    return (
       this.state.replies.filter(r => !r.read).length +
       this.state.mentions.filter(r => !r.read).length +
       this.state.messages.filter(
         r => !r.read && r.creator_id !== UserService.Instance.user.id
-      ).length;
-    UserService.Instance.user.unreadCount = count;
-    UserService.Instance.sub.next({
-      user: UserService.Instance.user,
-    });
+      ).length
+    );
   }
 }
index d583b93cd668983d5888c92d6529753ebf190853..e9b5a03125b7798554d6a3c2b109897aa238e220 100644 (file)
@@ -26,6 +26,12 @@ export class ListingTypeSelect extends Component<
     this.state = this.emptyState;
   }
 
+  static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
+    return {
+      type_: props.type_,
+    };
+  }
+
   render() {
     return (
       <div class="btn-group btn-group-toggle">
@@ -45,8 +51,9 @@ export class ListingTypeSelect extends Component<
           {i18n.t('subscribed')}
         </label>
         <label
-          className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
-            ListingType.All && 'active'}`}
+          className={`pointer btn btn-sm btn-secondary ${
+            this.state.type_ == ListingType.All && 'active'
+          }`}
         >
           <input
             type="radio"
@@ -61,8 +68,6 @@ export class ListingTypeSelect extends Component<
   }
 
   handleTypeChange(i: ListingTypeSelect, event: any) {
-    i.state.type_ = Number(event.target.value);
-    i.setState(i.state);
-    i.props.onChange(i.state.type_);
+    i.props.onChange(Number(event.target.value));
   }
 }
index eb1d0bafc7069f784d0019b61e7e34befe543324..4dd3821a3a3b6cc1fa9e1abe0aa87da86a641e97 100644 (file)
@@ -120,14 +120,15 @@ export class Login extends Component<any, State> {
                 class="form-control"
                 required
               />
-              <button
-                type="button"
-                disabled={!validEmail(this.state.loginForm.username_or_email)}
-                onClick={linkEvent(this, this.handlePasswordReset)}
-                className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
-              >
-                {i18n.t('forgot_password')}
-              </button>
+              {validEmail(this.state.loginForm.username_or_email) && (
+                <button
+                  type="button"
+                  onClick={linkEvent(this, this.handlePasswordReset)}
+                  className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
+                >
+                  {i18n.t('forgot_password')}
+                </button>
+              )}
             </div>
           </div>
           <div class="form-group row">
@@ -186,6 +187,14 @@ export class Login extends Component<any, State> {
               onInput={linkEvent(this, this.handleRegisterEmailChange)}
               minLength={3}
             />
+            {!validEmail(this.state.registerForm.email) && (
+              <div class="mt-2 mb-0 alert alert-light" role="alert">
+                <svg class="icon icon-inline mr-2">
+                  <use xlinkHref="#icon-alert-triangle"></use>
+                </svg>
+                {i18n.t('no_password_reset')}
+              </div>
+            )}
           </div>
         </div>
 
index 207350496a8dcd482fcdf835c47b8d846f160cd8..0392090a5f3f26bc601a9470db0a7a644aba4d84 100644 (file)
@@ -70,6 +70,20 @@ interface MainState {
   page: number;
 }
 
+interface MainProps {
+  listingType: ListingType;
+  dataType: DataType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  listingType?: string;
+  dataType?: string;
+  sort?: string;
+  page?: number;
+}
+
 export class Main extends Component<any, MainState> {
   private subscription: Subscription;
   private emptyState: MainState = {
@@ -141,17 +155,23 @@ export class Main extends Component<any, MainState> {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): MainProps {
+    return {
+      listingType: getListingTypeFromProps(props),
+      dataType: getDataTypeFromProps(props),
+      sort: getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: MainState) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.listingType !== this.state.listingType ||
+      lastState.dataType !== this.state.dataType ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state.listingType = getListingTypeFromProps(nextProps);
-      this.state.dataType = getDataTypeFromProps(nextProps);
-      this.state.sort = getSortTypeFromProps(nextProps);
-      this.state.page = getPageFromProps(nextProps);
-      this.setState(this.state);
+      this.setState({ loading: true });
       this.fetchData();
     }
   }
@@ -257,12 +277,17 @@ export class Main extends Component<any, MainState> {
     );
   }
 
-  updateUrl() {
-    let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
-    let dataTypeStr = DataType[this.state.dataType].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const listingTypeStr =
+      paramUpdates.listingType ||
+      ListingType[this.state.listingType].toLowerCase();
+    const dataTypeStr =
+      paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
+    const sortStr =
+      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const page = paramUpdates.page || this.state.page;
     this.props.history.push(
-      `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
@@ -373,17 +398,21 @@ export class Main extends Component<any, MainState> {
                 #
               </a>
               <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
-              <br></br>
+              <br class="big"></br>
               <code>#</code>
               <br></br>
               <b>#</b>
-              <br></br>
+              <br class="big"></br>
               <a href={repoUrl}>#</a>
-              <br></br>
+              <br class="big"></br>
               <a href="https://www.rust-lang.org">#</a>
               <a href="https://actix.rs/">#</a>
               <a href="https://infernojs.org">#</a>
               <a href="https://www.typescriptlang.org/">#</a>
+              <br class="big"></br>
+              <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
+                #
+              </a>
             </T>
           </p>
         </div>
@@ -493,7 +522,7 @@ export class Main extends Component<any, MainState> {
             {i18n.t('prev')}
           </button>
         )}
-        {this.state.posts.length == fetchLimit && (
+        {this.state.posts.length > 0 && (
           <button
             class="btn btn-sm btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
@@ -525,50 +554,27 @@ export class Main extends Component<any, MainState> {
   }
 
   nextPage(i: Main) {
-    i.state.page++;
-    i.state.loading = true;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    i.updateUrl({ page: i.state.page + 1 });
     window.scrollTo(0, 0);
   }
 
   prevPage(i: Main) {
-    i.state.page--;
-    i.state.loading = true;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    i.updateUrl({ page: i.state.page - 1 });
     window.scrollTo(0, 0);
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleListingTypeChange(val: ListingType) {
-    this.state.listingType = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
-    this.state.dataType = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
     window.scrollTo(0, 0);
   }
 
index a88d38c7e302a839c15cd5f69052fc09da026efb..e5efeaac508e5e8d439878dbeb410a7e4ec594d6 100644 (file)
@@ -33,14 +33,14 @@ import {
   randomStr,
   setupTribute,
   setupTippy,
-  emojiPicker,
   hostname,
   pictrsDeleteToast,
+  validTitle,
 } from '../utils';
 import autosize from 'autosize';
 import Tribute from 'tributejs/src/Tribute.js';
 import emojiShortName from 'emoji-short-name';
-import Selectr from 'mobius1-selectr';
+import Choices from 'choices.js';
 import { i18n } from '../i18next';
 
 const MAX_POST_TITLE_LENGTH = 200;
@@ -70,6 +70,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   private id = `post-form-${randomStr()}`;
   private tribute: Tribute;
   private subscription: Subscription;
+  private choices: Choices;
   private emptyState: PostFormState = {
     postForm: {
       name: null,
@@ -95,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
 
     this.tribute = setupTribute();
-    this.setupEmojiPicker();
 
     this.state = this.emptyState;
 
@@ -166,6 +166,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
+    this.choices && this.choices.destroy();
     window.onbeforeunload = null;
   }
 
@@ -271,12 +272,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                 value={this.state.postForm.name}
                 id="post-title"
                 onInput={linkEvent(this, this.handlePostNameChange)}
-                class="form-control"
+                class={`form-control ${
+                  !validTitle(this.state.postForm.name) && 'is-invalid'
+                }`}
                 required
                 rows={2}
                 minLength={3}
                 maxLength={MAX_POST_TITLE_LENGTH}
               />
+              {!validTitle(this.state.postForm.name) && (
+                <div class="invalid-feedback">
+                  {i18n.t('invalid_post_title')}
+                </div>
+              )}
               {this.state.suggestedPosts.length > 0 && (
                 <>
                   <div class="my-1 text-muted small font-weight-bold">
@@ -332,15 +340,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                   <use xlinkHref="#icon-help-circle"></use>
                 </svg>
               </a>
-              <span
-                onClick={linkEvent(this, this.handleEmojiPickerClick)}
-                class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
-                data-tippy-content={i18n.t('emoji_picker')}
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-smile"></use>
-                </svg>
-              </span>
             </div>
           </div>
           {!this.props.post && (
@@ -420,20 +419,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     );
   }
 
-  setupEmojiPicker() {
-    emojiPicker.on('emoji', twemojiHtmlStr => {
-      if (this.state.postForm.body == null) {
-        this.state.postForm.body = '';
-      }
-      var el = document.createElement('div');
-      el.innerHTML = twemojiHtmlStr;
-      let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
-      let shortName = `:${emojiShortName[nativeUnicode]}:`;
-      this.state.postForm.body += shortName;
-      this.setState(this.state);
-    });
-  }
-
   handlePostSubmit(i: PostForm, event: any) {
     event.preventDefault();
 
@@ -596,10 +581,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       });
   }
 
-  handleEmojiPickerClick(_i: PostForm, event: any) {
-    emojiPicker.togglePicker(event.target);
-  }
-
   parseMessage(msg: WebSocketJsonResponse) {
     let res = wsJsonToRes(msg);
     if (msg.error) {
@@ -625,11 +606,45 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       // Set up select searching
       let selectId: any = document.getElementById('post-community');
       if (selectId) {
-        let selector = new Selectr(selectId, { nativeDropdown: false });
-        selector.on('selectr.select', option => {
-          this.state.postForm.community_id = Number(option.value);
-          this.setState(this.state);
+        this.choices = new Choices(selectId, {
+          shouldSort: false,
+          classNames: {
+            containerOuter: 'choices',
+            containerInner: 'choices__inner bg-secondary border-0',
+            input: 'form-control',
+            inputCloned: 'choices__input--cloned',
+            list: 'choices__list',
+            listItems: 'choices__list--multiple',
+            listSingle: 'choices__list--single',
+            listDropdown: 'choices__list--dropdown',
+            item: 'choices__item bg-secondary',
+            itemSelectable: 'choices__item--selectable',
+            itemDisabled: 'choices__item--disabled',
+            itemChoice: 'choices__item--choice',
+            placeholder: 'choices__placeholder',
+            group: 'choices__group',
+            groupHeading: 'choices__heading',
+            button: 'choices__button',
+            activeState: 'is-active',
+            focusState: 'is-focused',
+            openState: 'is-open',
+            disabledState: 'is-disabled',
+            highlightedState: 'text-info',
+            selectedState: 'text-info',
+            flippedState: 'is-flipped',
+            loadingState: 'is-loading',
+            noResults: 'has-no-results',
+            noChoices: 'has-no-choices',
+          },
         });
+        this.choices.passedElement.element.addEventListener(
+          'choice',
+          (e: any) => {
+            this.state.postForm.community_id = Number(e.detail.choice.value);
+            this.setState(this.state);
+          },
+          false
+        );
       }
     } else if (res.op == UserOperation.CreatePost) {
       let data = res.data as PostResponse;
index ba1006478190351c11a5c8d195785baced636e11..418fe7b486a6c64e301b16f536091a43c23d77ee 100644 (file)
@@ -33,6 +33,7 @@ import {
   setupTippy,
   hostname,
   previewLines,
+  toast,
 } from '../utils';
 import { i18n } from '../i18next';
 
@@ -434,8 +435,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                       id: post.creator_id,
                       local: post.creator_local,
                       actor_id: post.creator_actor_id,
+                      published: post.creator_published,
                     }}
                   />
+
                   {this.isMod && (
                     <span className="mx-1 badge badge-light">
                       {i18n.t('mod')}
@@ -1030,6 +1033,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handlePostLike(i: PostListing) {
+    if (!UserService.Instance.user) {
+      this.context.router.history.push(`/login`);
+    }
+
     let new_vote = i.state.my_vote == 1 ? 0 : 1;
 
     if (i.state.my_vote == 1) {
@@ -1057,6 +1064,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handlePostDisLike(i: PostListing) {
+    if (!UserService.Instance.user) {
+      this.context.router.history.push(`/login`);
+    }
+
     let new_vote = i.state.my_vote == -1 ? 0 : -1;
 
     if (i.state.my_vote == 1) {
index 5d76808ebe13946440dba22c3275e855822fa127..9eef286c9744cb5fea34aa4c91e1e03cb8da447d 100644 (file)
@@ -11,6 +11,7 @@ import {
   CommentForm as CommentFormI,
   CommentResponse,
   CommentSortType,
+  CommentViewType,
   CommunityUser,
   CommunityResponse,
   CommentNode as CommentNodeI,
@@ -49,6 +50,7 @@ interface PostState {
   post: PostI;
   comments: Array<Comment>;
   commentSort: CommentSortType;
+  commentViewType: CommentViewType;
   community: Community;
   moderators: Array<CommunityUser>;
   online: number;
@@ -65,6 +67,7 @@ export class Post extends Component<any, PostState> {
     post: null,
     comments: [],
     commentSort: CommentSortType.Hot,
+    commentViewType: CommentViewType.Tree,
     community: null,
     moderators: [],
     online: null,
@@ -208,12 +211,12 @@ export class Post extends Component<any, PostState> {
                 disabled={this.state.post.locked}
               />
               {this.state.comments.length > 0 && this.sortRadios()}
-              {this.commentsTree()}
-            </div>
-            <div class="col-12 col-sm-12 col-md-4">
-              {this.state.comments.length > 0 && this.newComments()}
-              {this.sidebar()}
+              {this.state.commentViewType == CommentViewType.Tree &&
+                this.commentsTree()}
+              {this.state.commentViewType == CommentViewType.Chat &&
+                this.commentsFlat()}
             </div>
+            <div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
           </div>
         )}
       </div>
@@ -222,79 +225,94 @@ export class Post extends Component<any, PostState> {
 
   sortRadios() {
     return (
-      <div class="btn-group btn-group-toggle mb-2">
-        <label
-          className={`btn btn-sm btn-secondary pointer ${
-            this.state.commentSort === CommentSortType.Hot && 'active'
-          }`}
-        >
-          {i18n.t('hot')}
-          <input
-            type="radio"
-            value={CommentSortType.Hot}
-            checked={this.state.commentSort === CommentSortType.Hot}
-            onChange={linkEvent(this, this.handleCommentSortChange)}
-          />
-        </label>
-        <label
-          className={`btn btn-sm btn-secondary pointer ${
-            this.state.commentSort === CommentSortType.Top && 'active'
-          }`}
-        >
-          {i18n.t('top')}
-          <input
-            type="radio"
-            value={CommentSortType.Top}
-            checked={this.state.commentSort === CommentSortType.Top}
-            onChange={linkEvent(this, this.handleCommentSortChange)}
-          />
-        </label>
-        <label
-          className={`btn btn-sm btn-secondary pointer ${
-            this.state.commentSort === CommentSortType.New && 'active'
-          }`}
-        >
-          {i18n.t('new')}
-          <input
-            type="radio"
-            value={CommentSortType.New}
-            checked={this.state.commentSort === CommentSortType.New}
-            onChange={linkEvent(this, this.handleCommentSortChange)}
-          />
-        </label>
-        <label
-          className={`btn btn-sm btn-secondary pointer ${
-            this.state.commentSort === CommentSortType.Old && 'active'
-          }`}
-        >
-          {i18n.t('old')}
-          <input
-            type="radio"
-            value={CommentSortType.Old}
-            checked={this.state.commentSort === CommentSortType.Old}
-            onChange={linkEvent(this, this.handleCommentSortChange)}
-          />
-        </label>
-      </div>
+      <>
+        <div class="btn-group btn-group-toggle mr-3 mb-2">
+          <label
+            className={`btn btn-sm btn-secondary pointer ${
+              this.state.commentSort === CommentSortType.Hot && 'active'
+            }`}
+          >
+            {i18n.t('hot')}
+            <input
+              type="radio"
+              value={CommentSortType.Hot}
+              checked={this.state.commentSort === CommentSortType.Hot}
+              onChange={linkEvent(this, this.handleCommentSortChange)}
+            />
+          </label>
+          <label
+            className={`btn btn-sm btn-secondary pointer ${
+              this.state.commentSort === CommentSortType.Top && 'active'
+            }`}
+          >
+            {i18n.t('top')}
+            <input
+              type="radio"
+              value={CommentSortType.Top}
+              checked={this.state.commentSort === CommentSortType.Top}
+              onChange={linkEvent(this, this.handleCommentSortChange)}
+            />
+          </label>
+          <label
+            className={`btn btn-sm btn-secondary pointer ${
+              this.state.commentSort === CommentSortType.New && 'active'
+            }`}
+          >
+            {i18n.t('new')}
+            <input
+              type="radio"
+              value={CommentSortType.New}
+              checked={this.state.commentSort === CommentSortType.New}
+              onChange={linkEvent(this, this.handleCommentSortChange)}
+            />
+          </label>
+          <label
+            className={`btn btn-sm btn-secondary pointer ${
+              this.state.commentSort === CommentSortType.Old && 'active'
+            }`}
+          >
+            {i18n.t('old')}
+            <input
+              type="radio"
+              value={CommentSortType.Old}
+              checked={this.state.commentSort === CommentSortType.Old}
+              onChange={linkEvent(this, this.handleCommentSortChange)}
+            />
+          </label>
+        </div>
+        <div class="btn-group btn-group-toggle mb-2">
+          <label
+            className={`btn btn-sm btn-secondary pointer ${
+              this.state.commentViewType === CommentViewType.Chat && 'active'
+            }`}
+          >
+            {i18n.t('chat')}
+            <input
+              type="radio"
+              value={CommentViewType.Chat}
+              checked={this.state.commentViewType === CommentViewType.Chat}
+              onChange={linkEvent(this, this.handleCommentViewTypeChange)}
+            />
+          </label>
+        </div>
+      </>
     );
   }
 
-  newComments() {
+  commentsFlat() {
     return (
-      <div class="d-none d-md-block new-comments mb-3 card border-secondary">
-        <div class="card-body small">
-          <h6>{i18n.t('recent_comments')}</h6>
-          <CommentNodes
-            nodes={commentsToFlatNodes(this.state.comments)}
-            noIndent
-            locked={this.state.post.locked}
-            moderators={this.state.moderators}
-            admins={this.state.siteRes.admins}
-            postCreatorId={this.state.post.creator_id}
-            showContext
-            enableDownvotes={this.state.siteRes.site.enable_downvotes}
-          />
-        </div>
+      <div>
+        <CommentNodes
+          nodes={commentsToFlatNodes(this.state.comments)}
+          noIndent
+          locked={this.state.post.locked}
+          moderators={this.state.moderators}
+          admins={this.state.siteRes.admins}
+          postCreatorId={this.state.post.creator_id}
+          showContext
+          enableDownvotes={this.state.siteRes.site.enable_downvotes}
+          sort={this.state.commentSort}
+        />
       </div>
     );
   }
@@ -315,6 +333,13 @@ export class Post extends Component<any, PostState> {
 
   handleCommentSortChange(i: Post, event: any) {
     i.state.commentSort = Number(event.target.value);
+    i.state.commentViewType = CommentViewType.Tree;
+    i.setState(i.state);
+  }
+
+  handleCommentViewTypeChange(i: Post, event: any) {
+    i.state.commentViewType = Number(event.target.value);
+    i.state.commentSort = CommentSortType.New;
     i.setState(i.state);
   }
 
index 2588528a73d798583599b553330a30171f0bf4b5..d1d99cee86e106ae47f6f08177065cdb66a0e75b 100644 (file)
@@ -28,6 +28,7 @@ import {
   createCommentLikeRes,
   createPostLikeFindRes,
   commentsToFlatNodes,
+  getPageFromProps,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { UserListing } from './user-listing';
@@ -44,15 +45,31 @@ interface SearchState {
   searchResponse: SearchResponse;
   loading: boolean;
   site: Site;
+  searchText: string;
+}
+
+interface SearchProps {
+  q: string;
+  type_: SearchType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  q?: string;
+  type_?: string;
+  sort?: string;
+  page?: number;
 }
 
 export class Search extends Component<any, SearchState> {
   private subscription: Subscription;
   private emptyState: SearchState = {
-    q: this.getSearchQueryFromProps(this.props),
-    type_: this.getSearchTypeFromProps(this.props),
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    q: Search.getSearchQueryFromProps(this.props),
+    type_: Search.getSearchTypeFromProps(this.props),
+    sort: Search.getSortTypeFromProps(this.props),
+    page: getPageFromProps(this.props),
+    searchText: Search.getSearchQueryFromProps(this.props),
     searchResponse: {
       type_: null,
       posts: [],
@@ -77,26 +94,22 @@ export class Search extends Component<any, SearchState> {
     },
   };
 
-  getSearchQueryFromProps(props: any): string {
+  static getSearchQueryFromProps(props: any): string {
     return props.match.params.q ? props.match.params.q : '';
   }
 
-  getSearchTypeFromProps(props: any): SearchType {
+  static getSearchTypeFromProps(props: any): SearchType {
     return props.match.params.type
       ? routeSearchTypeToEnum(props.match.params.type)
       : SearchType.All;
   }
 
-  getSortTypeFromProps(props: any): SortType {
+  static getSortTypeFromProps(props: any): SortType {
     return props.match.params.sort
       ? routeSortTypeToEnum(props.match.params.sort)
       : SortType.TopAll;
   }
 
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   constructor(props: any, context: any) {
     super(props, context);
 
@@ -122,17 +135,23 @@ export class Search extends Component<any, SearchState> {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): SearchProps {
+    return {
+      q: Search.getSearchQueryFromProps(props),
+      type_: Search.getSearchTypeFromProps(props),
+      sort: Search.getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: SearchState) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.q !== this.state.q ||
+      lastState.type_ !== this.state.type_ ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state.q = this.getSearchQueryFromProps(nextProps);
-      this.state.type_ = this.getSearchTypeFromProps(nextProps);
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
-      this.setState(this.state);
+      this.setState({ loading: true, searchText: this.state.q });
       this.search();
     }
   }
@@ -148,7 +167,7 @@ export class Search extends Component<any, SearchState> {
         {this.state.type_ == SearchType.Posts && this.posts()}
         {this.state.type_ == SearchType.Communities && this.communities()}
         {this.state.type_ == SearchType.Users && this.users()}
-        {this.noResults()}
+        {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
         {this.paginator()}
       </div>
     );
@@ -163,7 +182,7 @@ export class Search extends Component<any, SearchState> {
         <input
           type="text"
           class="form-control mr-2"
-          value={this.state.q}
+          value={this.state.searchText}
           placeholder={`${i18n.t('search')}...`}
           onInput={linkEvent(this, this.handleQChange)}
           required
@@ -275,6 +294,7 @@ export class Search extends Component<any, SearchState> {
               {i.type_ == 'users' && (
                 <div>
                   <span>
+                    @
                     <UserListing
                       user={{
                         name: (i.data as UserView).name,
@@ -282,9 +302,9 @@ export class Search extends Component<any, SearchState> {
                       }}
                     />
                   </span>
-                  <span>{` - ${
-                    (i.data as UserView).comment_score
-                  } comment karma`}</span>
+                  <span>{` - ${i18n.t('number_of_comments', {
+                    count: (i.data as UserView).number_of_comments,
+                  })}`}</span>
                 </div>
               )}
             </div>
@@ -359,12 +379,17 @@ export class Search extends Component<any, SearchState> {
           <div class="row">
             <div class="col-12">
               <span>
-                <Link
-                  className="text-info"
-                  to={`/u/${user.name}`}
-                >{`/u/${user.name}`}</Link>
+                @
+                <UserListing
+                  user={{
+                    name: user.name,
+                    avatar: user.avatar,
+                  }}
+                />
               </span>
-              <span>{` - ${user.comment_score} comment karma`}</span>
+              <span>{` - ${i18n.t('number_of_comments', {
+                count: user.number_of_comments,
+              })}`}</span>
             </div>
           </div>
         ))}
@@ -383,41 +408,35 @@ export class Search extends Component<any, SearchState> {
             {i18n.t('prev')}
           </button>
         )}
-        <button
-          class="btn btn-sm btn-secondary"
-          onClick={linkEvent(this, this.nextPage)}
-        >
-          {i18n.t('next')}
-        </button>
+
+        {this.resultsCount() > 0 && (
+          <button
+            class="btn btn-sm btn-secondary"
+            onClick={linkEvent(this, this.nextPage)}
+          >
+            {i18n.t('next')}
+          </button>
+        )}
       </div>
     );
   }
 
-  noResults() {
+  resultsCount(): number {
     let res = this.state.searchResponse;
     return (
-      <div>
-        {res &&
-          res.posts.length == 0 &&
-          res.comments.length == 0 &&
-          res.communities.length == 0 &&
-          res.users.length == 0 && <span>{i18n.t('no_results')}</span>}
-      </div>
+      res.posts.length +
+      res.comments.length +
+      res.communities.length +
+      res.users.length
     );
   }
 
   nextPage(i: Search) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.search();
+    i.updateUrl({ page: i.state.page + 1 });
   }
 
   prevPage(i: Search) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.search();
+    i.updateUrl({ page: i.state.page - 1 });
   }
 
   search() {
@@ -435,37 +454,39 @@ export class Search extends Component<any, SearchState> {
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.setState(this.state);
-    this.updateUrl();
+    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
   }
 
   handleTypeChange(i: Search, event: any) {
-    i.state.type_ = Number(event.target.value);
-    i.state.page = 1;
-    i.setState(i.state);
-    i.updateUrl();
+    i.updateUrl({
+      type_: SearchType[Number(event.target.value)].toLowerCase(),
+      page: 1,
+    });
   }
 
   handleSearchSubmit(i: Search, event: any) {
     event.preventDefault();
-    i.state.loading = true;
-    i.search();
-    i.setState(i.state);
-    i.updateUrl();
+    i.updateUrl({
+      q: i.state.searchText,
+      type_: SearchType[i.state.type_].toLowerCase(),
+      sort: SortType[i.state.sort].toLowerCase(),
+      page: i.state.page,
+    });
   }
 
   handleQChange(i: Search, event: any) {
-    i.state.q = event.target.value;
-    i.setState(i.state);
+    i.setState({ searchText: event.target.value });
   }
 
-  updateUrl() {
-    let typeStr = SearchType[this.state.type_].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const qStr = paramUpdates.q || this.state.q;
+    const typeStr =
+      paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
+    const sortStr =
+      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const page = paramUpdates.page || this.state.page;
     this.props.history.push(
-      `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
index 05abdb20afeccb5f812c9e1e85a4cc8cd596fc41..33d6581991dc4375541b5496520d9bd583f56693 100644 (file)
@@ -23,6 +23,12 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
     this.state = this.emptyState;
   }
 
+  static getDerivedStateFromProps(props: any): SortSelectState {
+    return {
+      sort: props.sort,
+    };
+  }
+
   render() {
     return (
       <>
@@ -59,8 +65,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
   }
 
   handleSortChange(i: SortSelect, event: any) {
-    i.state.sort = Number(event.target.value);
-    i.setState(i.state);
-    i.props.onChange(i.state.sort);
+    i.props.onChange(event.target.value);
   }
 }
index 77d7a086043783147c5ff16001c8a31461ffea74..3386dbe59da38d9a68defebfd456f5101321e529 100644 (file)
@@ -168,6 +168,9 @@ export class Symbols extends Component<any, any> {
           <symbol id="icon-spinner" viewBox="0 0 32 32">
             <path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
           </symbol>
+          <symbol id="icon-cake" viewBox="0 0 24 24">
+            <path d="M 23.296875 22.394531 L 22.082031 22.394531 L 22.082031 17.007812 C 22.453125 16.699219 22.664062 16.261719 22.664062 15.796875 L 22.664062 13.984375 C 22.664062 12.996094 21.785156 12.191406 20.703125 12.191406 L 19.785156 12.191406 L 19.785156 7.785156 C 19.785156 7.050781 19.1875 6.449219 18.449219 6.449219 L 18.367188 6.449219 L 18.367188 5.96875 C 19.199219 5.675781 19.796875 4.882812 19.796875 3.957031 C 19.796875 3.644531 19.703125 3.117188 18.996094 1.800781 C 18.632812 1.121094 18.273438 0.550781 18.257812 0.527344 C 18.128906 0.320312 17.90625 0.199219 17.664062 0.199219 C 17.421875 0.199219 17.199219 0.320312 17.070312 0.527344 C 17.054688 0.550781 16.695312 1.121094 16.332031 1.800781 C 15.621094 3.117188 15.53125 3.644531 15.53125 3.957031 C 15.53125 4.882812 16.128906 5.675781 16.960938 5.96875 L 16.960938 6.449219 L 16.878906 6.449219 C 16.140625 6.449219 15.542969 7.050781 15.542969 7.785156 L 15.542969 12.191406 L 14.121094 12.191406 L 14.121094 7.785156 C 14.121094 7.050781 13.523438 6.449219 12.785156 6.449219 L 12.703125 6.449219 L 12.703125 5.96875 C 13.535156 5.675781 14.132812 4.882812 14.132812 3.957031 C 14.132812 3.644531 14.039062 3.117188 13.332031 1.800781 C 12.96875 1.121094 12.609375 0.550781 12.59375 0.527344 C 12.464844 0.320312 12.242188 0.199219 12 0.199219 C 11.757812 0.199219 11.535156 0.320312 11.40625 0.527344 C 11.390625 0.550781 11.03125 1.121094 10.667969 1.800781 C 9.960938 3.117188 9.867188 3.644531 9.867188 3.957031 C 9.867188 4.882812 10.464844 5.675781 11.296875 5.96875 L 11.296875 6.449219 L 11.214844 6.449219 C 10.476562 6.449219 9.878906 7.050781 9.878906 7.785156 L 9.878906 12.191406 L 8.457031 12.191406 L 8.457031 7.785156 C 8.457031 7.050781 7.859375 6.449219 7.121094 6.449219 L 7.039062 6.449219 L 7.039062 5.96875 C 7.871094 5.675781 8.46875 4.882812 8.46875 3.957031 C 8.46875 3.644531 8.378906 3.117188 7.667969 1.800781 C 7.304688 1.121094 6.945312 0.550781 6.929688 0.527344 C 6.800781 0.320312 6.578125 0.199219 6.335938 0.199219 C 6.09375 0.199219 5.871094 0.320312 5.742188 0.527344 C 5.726562 0.550781 5.367188 1.121094 5.003906 1.800781 C 4.296875 3.117188 4.203125 3.644531 4.203125 3.957031 C 4.203125 4.882812 4.800781 5.675781 5.632812 5.96875 L 5.632812 6.449219 L 5.550781 6.449219 C 4.8125 6.449219 4.214844 7.050781 4.214844 7.785156 L 4.214844 12.191406 L 3.296875 12.191406 C 2.214844 12.191406 1.335938 12.996094 1.335938 13.984375 L 1.335938 15.796875 C 1.335938 16.261719 1.546875 16.699219 1.917969 17.007812 L 1.917969 22.394531 L 0.703125 22.394531 C 0.316406 22.394531 0 22.710938 0 23.097656 C 0 23.488281 0.316406 23.800781 0.703125 23.800781 L 23.296875 23.800781 C 23.683594 23.800781 24 23.488281 24 23.097656 C 24 22.710938 23.683594 22.394531 23.296875 22.394531 Z M 16.9375 3.957031 C 16.941406 3.730469 17.246094 3.054688 17.664062 2.289062 C 18.082031 3.054688 18.382812 3.730469 18.390625 3.957031 C 18.390625 4.355469 18.0625 4.679688 17.664062 4.679688 C 17.265625 4.679688 16.9375 4.355469 16.9375 3.957031 Z M 16.949219 7.855469 L 18.378906 7.855469 L 18.378906 12.1875 L 16.949219 12.1875 Z M 11.273438 3.957031 C 11.277344 3.730469 11.582031 3.054688 12 2.289062 C 12.417969 3.054688 12.722656 3.730469 12.726562 3.957031 C 12.726562 4.355469 12.398438 4.679688 12 4.679688 C 11.601562 4.679688 11.273438 4.355469 11.273438 3.957031 Z M 11.285156 7.855469 L 12.714844 7.855469 L 12.714844 12.1875 L 11.285156 12.1875 Z M 5.609375 3.957031 C 5.613281 3.730469 5.917969 3.054688 6.335938 2.289062 C 6.753906 3.054688 7.058594 3.730469 7.0625 3.957031 C 7.0625 4.355469 6.734375 4.679688 6.335938 4.679688 C 5.9375 4.679688 5.609375 4.355469 5.609375 3.957031 Z M 5.621094 7.855469 L 7.050781 7.855469 L 7.050781 12.1875 L 5.621094 12.1875 Z M 20.675781 22.394531 L 3.324219 22.394531 L 3.324219 17.414062 C 3.433594 17.398438 3.546875 17.378906 3.652344 17.347656 L 5.429688 16.820312 C 6.453125 16.515625 7.582031 16.515625 8.609375 16.820312 L 10.011719 17.234375 C 10.652344 17.425781 11.324219 17.519531 12 17.519531 C 12.675781 17.519531 13.347656 17.425781 13.988281 17.234375 L 15.390625 16.820312 C 16.417969 16.515625 17.546875 16.515625 18.570312 16.820312 L 20.347656 17.347656 C 20.453125 17.378906 20.5625 17.398438 20.675781 17.414062 Z M 21.257812 15.796875 C 21.257812 15.855469 21.210938 15.902344 21.171875 15.933594 C 21.082031 16 20.925781 16.050781 20.746094 15.996094 L 18.972656 15.472656 C 17.6875 15.09375 16.273438 15.09375 14.992188 15.472656 L 13.589844 15.886719 C 12.566406 16.191406 11.433594 16.191406 10.410156 15.886719 L 9.007812 15.472656 C 8.367188 15.28125 7.691406 15.1875 7.019531 15.1875 C 6.34375 15.1875 5.671875 15.28125 5.027344 15.472656 L 3.253906 15.996094 C 3.074219 16.050781 2.917969 16 2.828125 15.933594 C 2.789062 15.902344 2.742188 15.855469 2.742188 15.796875 L 2.742188 13.984375 C 2.742188 13.800781 2.96875 13.597656 3.296875 13.597656 L 20.703125 13.597656 C 21.03125 13.597656 21.257812 13.800781 21.257812 13.984375 Z M 21.257812 15.796875 " />
+          </symbol>
         </defs>
       </svg>
     );
diff --git a/ui/src/components/user-details.tsx b/ui/src/components/user-details.tsx
new file mode 100644 (file)
index 0000000..e4b4b24
--- /dev/null
@@ -0,0 +1,307 @@
+import { Component, linkEvent } from 'inferno';
+import { WebSocketService, UserService } from '../services';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take, last } from 'rxjs/operators';
+import { i18n } from '../i18next';
+import {
+  UserOperation,
+  Post,
+  Comment,
+  CommunityUser,
+  SortType,
+  UserDetailsResponse,
+  UserView,
+  WebSocketJsonResponse,
+  UserDetailsView,
+  CommentResponse,
+  BanUserResponse,
+  PostResponse,
+  AddAdminResponse,
+} from '../interfaces';
+import {
+  wsJsonToRes,
+  toast,
+  commentsToFlatNodes,
+  setupTippy,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+} from '../utils';
+import { PostListing } from './post-listing';
+import { CommentNodes } from './comment-nodes';
+
+interface UserDetailsProps {
+  username?: string;
+  user_id?: number;
+  page: number;
+  limit: number;
+  sort: string;
+  enableDownvotes: boolean;
+  enableNsfw: boolean;
+  view: UserDetailsView;
+  onPageChange(page: number): number | any;
+}
+
+interface UserDetailsState {
+  follows: Array<CommunityUser>;
+  moderates: Array<CommunityUser>;
+  comments: Array<Comment>;
+  posts: Array<Post>;
+  saved?: Array<Post>;
+  admins: Array<UserView>;
+}
+
+export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
+  private subscription: Subscription;
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = {
+      follows: [],
+      moderates: [],
+      comments: [],
+      posts: [],
+      saved: [],
+      admins: [],
+    };
+
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        msg => this.parseMessage(msg),
+        err => console.error(err),
+        () => console.log('complete')
+      );
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  componentDidMount() {
+    this.fetchUserData();
+  }
+
+  componentDidUpdate(lastProps: UserDetailsProps) {
+    for (const key of Object.keys(lastProps)) {
+      if (lastProps[key] !== this.props[key]) {
+        this.fetchUserData();
+        break;
+      }
+    }
+    setupTippy();
+  }
+
+  fetchUserData() {
+    WebSocketService.Instance.getUserDetails({
+      user_id: this.props.user_id,
+      username: this.props.username,
+      sort: this.props.sort,
+      saved_only: this.props.view === UserDetailsView.Saved,
+      page: this.props.page,
+      limit: this.props.limit,
+    });
+  }
+
+  render() {
+    return (
+      <div>
+        {this.viewSelector(this.props.view)}
+        {this.paginator()}
+      </div>
+    );
+  }
+
+  viewSelector(view: UserDetailsView) {
+    if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
+      return this.overview();
+    }
+    if (view === UserDetailsView.Comments) {
+      return this.comments();
+    }
+    if (view === UserDetailsView.Posts) {
+      return this.posts();
+    }
+  }
+
+  overview() {
+    const comments = this.state.comments.map((c: Comment) => {
+      return { type: 'comments', data: c };
+    });
+    const posts = this.state.posts.map((p: Post) => {
+      return { type: 'posts', data: p };
+    });
+
+    const combined: Array<{ type: string; data: Comment | Post }> = [
+      ...comments,
+      ...posts,
+    ];
+
+    // Sort it
+    if (SortType[this.props.sort] === SortType.New) {
+      combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
+    } else {
+      combined.sort((a, b) => b.data.score - a.data.score);
+    }
+
+    return (
+      <div>
+        {combined.map(i => (
+          <div>
+            {i.type === 'posts' ? (
+              <PostListing
+                post={i.data as Post}
+                admins={this.state.admins}
+                showCommunity
+                enableDownvotes={this.props.enableDownvotes}
+                enableNsfw={this.props.enableNsfw}
+              />
+            ) : (
+              <CommentNodes
+                nodes={[{ comment: i.data as Comment }]}
+                admins={this.state.admins}
+                noIndent
+                showContext
+                enableDownvotes={this.props.enableDownvotes}
+              />
+            )}
+          </div>
+        ))}
+      </div>
+    );
+  }
+
+  comments() {
+    return (
+      <div>
+        <CommentNodes
+          nodes={commentsToFlatNodes(this.state.comments)}
+          admins={this.state.admins}
+          noIndent
+          showContext
+          enableDownvotes={this.props.enableDownvotes}
+        />
+      </div>
+    );
+  }
+
+  posts() {
+    return (
+      <div>
+        {this.state.posts.map(post => (
+          <PostListing
+            post={post}
+            admins={this.state.admins}
+            showCommunity
+            enableDownvotes={this.props.enableDownvotes}
+            enableNsfw={this.props.enableNsfw}
+          />
+        ))}
+      </div>
+    );
+  }
+
+  paginator() {
+    return (
+      <div class="my-2">
+        {this.props.page > 1 && (
+          <button
+            class="btn btn-sm btn-secondary mr-1"
+            onClick={linkEvent(this, this.prevPage)}
+          >
+            {i18n.t('prev')}
+          </button>
+        )}
+        {this.state.comments.length + this.state.posts.length > 0 && (
+          <button
+            class="btn btn-sm btn-secondary"
+            onClick={linkEvent(this, this.nextPage)}
+          >
+            {i18n.t('next')}
+          </button>
+        )}
+      </div>
+    );
+  }
+
+  nextPage(i: UserDetails) {
+    i.props.onPageChange(i.props.page + 1);
+  }
+
+  prevPage(i: UserDetails) {
+    i.props.onPageChange(i.props.page - 1);
+  }
+
+  parseMessage(msg: WebSocketJsonResponse) {
+    const res = wsJsonToRes(msg);
+
+    if (msg.error) {
+      toast(i18n.t(msg.error), 'danger');
+      if (msg.error == 'couldnt_find_that_username_or_email') {
+        this.context.router.history.push('/');
+      }
+      return;
+    } else if (msg.reconnect) {
+      this.fetchUserData();
+    } else if (res.op == UserOperation.GetUserDetails) {
+      const data = res.data as UserDetailsResponse;
+      this.setState({
+        comments: data.comments,
+        follows: data.follows,
+        moderates: data.moderates,
+        posts: data.posts,
+        admins: data.admins,
+      });
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      const data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.comments);
+      this.setState({
+        comments: this.state.comments,
+      });
+    } else if (res.op == UserOperation.EditComment) {
+      const data = res.data as CommentResponse;
+      editCommentRes(data, this.state.comments);
+      this.setState({
+        comments: this.state.comments,
+      });
+    } else if (res.op == UserOperation.CreateComment) {
+      const data = res.data as CommentResponse;
+      if (
+        UserService.Instance.user &&
+        data.comment.creator_id == UserService.Instance.user.id
+      ) {
+        toast(i18n.t('reply_sent'));
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      const data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.comments);
+      this.setState({
+        comments: this.state.comments,
+      });
+    } else if (res.op == UserOperation.CreatePostLike) {
+      const data = res.data as PostResponse;
+      createPostLikeFindRes(data, this.state.posts);
+      this.setState({
+        posts: this.state.posts,
+      });
+    } else if (res.op == UserOperation.BanUser) {
+      const data = res.data as BanUserResponse;
+      this.state.comments
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
+      this.state.posts
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
+      this.setState({
+        posts: this.state.posts,
+        comments: this.state.comments,
+      });
+    } else if (res.op == UserOperation.AddAdmin) {
+      const data = res.data as AddAdminResponse;
+      this.setState({
+        admins: data.admins,
+      });
+    }
+  }
+}
index 0e150b9420d3468d61cadd01ea229ab2dca01c54..58475d3e94127996cdc516d898745cf6b5e4ccec 100644 (file)
@@ -1,7 +1,13 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { UserView } from '../interfaces';
-import { pictrsAvatarThumbnail, showAvatars, hostname } from '../utils';
+import {
+  pictrsAvatarThumbnail,
+  showAvatars,
+  hostname,
+  isCakeDay,
+} from '../utils';
+import { CakeDay } from './cake-day';
 
 interface UserOther {
   name: string;
@@ -9,6 +15,7 @@ interface UserOther {
   avatar?: string;
   local?: boolean;
   actor_id?: string;
+  published?: string;
 }
 
 interface UserListingProps {
@@ -35,17 +42,21 @@ export class UserListing extends Component<UserListingProps, any> {
     }
 
     return (
-      <Link className="text-body font-weight-bold" to={link}>
-        {user.avatar && showAvatars() && (
-          <img
-            height="32"
-            width="32"
-            src={pictrsAvatarThumbnail(user.avatar)}
-            class="rounded-circle mr-2"
-          />
-        )}
-        <span>{name_}</span>
-      </Link>
+      <>
+        <Link className="text-body font-weight-bold" to={link}>
+          {user.avatar && showAvatars() && (
+            <img
+              height="32"
+              width="32"
+              src={pictrsAvatarThumbnail(user.avatar)}
+              class="rounded-circle mr-2"
+            />
+          )}
+          <span>{name_}</span>
+        </Link>
+
+        {isCakeDay(user.published) && <CakeDay creatorName={name_} />}
+      </>
     );
   }
 }
index 078ce89c851f14e99bc4223e7006b3c5fc1d84d5..945206c1de67202c3166516cd4541fd01d83f44d 100644 (file)
@@ -4,24 +4,18 @@ import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
   UserOperation,
-  Post,
-  Comment,
   CommunityUser,
-  GetUserDetailsForm,
   SortType,
   ListingType,
-  UserDetailsResponse,
   UserView,
-  CommentResponse,
   UserSettingsForm,
   LoginResponse,
-  BanUserResponse,
-  AddAdminResponse,
   DeleteAccountForm,
-  PostResponse,
   WebSocketJsonResponse,
   GetSiteResponse,
   Site,
+  UserDetailsView,
+  UserDetailsResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
@@ -34,27 +28,15 @@ import {
   languages,
   showAvatars,
   toast,
-  editCommentRes,
-  saveCommentRes,
-  createCommentLikeRes,
-  createPostLikeFindRes,
-  commentsToFlatNodes,
   setupTippy,
 } from '../utils';
-import { PostListing } from './post-listing';
 import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
 import { ListingTypeSelect } from './listing-type-select';
-import { CommentNodes } from './comment-nodes';
 import { MomentTime } from './moment-time';
 import { i18n } from '../i18next';
-
-enum View {
-  Overview,
-  Comments,
-  Posts,
-  Saved,
-}
+import moment from 'moment';
+import { UserDetails } from './user-details';
 
 interface UserState {
   user: UserView;
@@ -62,11 +44,7 @@ interface UserState {
   username: string;
   follows: Array<CommunityUser>;
   moderates: Array<CommunityUser>;
-  comments: Array<Comment>;
-  posts: Array<Post>;
-  saved?: Array<Post>;
-  admins: Array<UserView>;
-  view: View;
+  view: UserDetailsView;
   sort: SortType;
   page: number;
   loading: boolean;
@@ -79,6 +57,20 @@ interface UserState {
   site: Site;
 }
 
+interface UserProps {
+  view: UserDetailsView;
+  sort: SortType;
+  page: number;
+  user_id: number | null;
+  username: string;
+}
+
+interface UrlParams {
+  view?: string;
+  sort?: string;
+  page?: number;
+}
+
 export class User extends Component<any, UserState> {
   private subscription: Subscription;
   private emptyState: UserState = {
@@ -101,14 +93,11 @@ export class User extends Component<any, UserState> {
     username: null,
     follows: [],
     moderates: [],
-    comments: [],
-    posts: [],
-    admins: [],
-    loading: true,
+    loading: false,
     avatarLoading: false,
-    view: this.getViewFromProps(this.props),
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    view: User.getViewFromProps(this.props.match.view),
+    sort: User.getSortTypeFromProps(this.props.match.sort),
+    page: User.getPageFromProps(this.props.match.page),
     userSettingsForm: {
       show_nsfw: null,
       theme: null,
@@ -152,8 +141,9 @@ export class User extends Component<any, UserState> {
     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
       this
     );
+    this.handlePageChange = this.handlePageChange.bind(this);
 
-    this.state.user_id = Number(this.props.match.params.id);
+    this.state.user_id = Number(this.props.match.params.id) || null;
     this.state.username = this.props.match.params.username;
 
     this.subscription = WebSocketService.Instance.subject
@@ -164,7 +154,6 @@ export class User extends Component<any, UserState> {
         () => console.log('complete')
       );
 
-    this.refetch();
     WebSocketService.Instance.getSite();
   }
 
@@ -175,38 +164,32 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  getViewFromProps(props: any): View {
-    return props.match.params.view
-      ? View[capitalizeFirstLetter(props.match.params.view)]
-      : View.Overview;
+  static getViewFromProps(view: any): UserDetailsView {
+    return view
+      ? UserDetailsView[capitalizeFirstLetter(view)]
+      : UserDetailsView.Overview;
   }
 
-  getSortTypeFromProps(props: any): SortType {
-    return props.match.params.sort
-      ? routeSortTypeToEnum(props.match.params.sort)
-      : SortType.New;
+  static getSortTypeFromProps(sort: any): SortType {
+    return sort ? routeSortTypeToEnum(sort) : SortType.New;
   }
 
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
+  static getPageFromProps(page: any): number {
+    return page ? Number(page) : 1;
   }
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
-    if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
-    ) {
-      this.state.view = this.getViewFromProps(nextProps);
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
-      this.setState(this.state);
-      this.refetch();
-    }
+  static getDerivedStateFromProps(props: any): UserProps {
+    return {
+      view: this.getViewFromProps(props.match.params.view),
+      sort: this.getSortTypeFromProps(props.match.params.sort),
+      page: this.getPageFromProps(props.match.params.page),
+      user_id: Number(props.match.params.id) || null,
+      username: props.match.params.username,
+    };
   }
 
   componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
@@ -218,6 +201,8 @@ export class User extends Component<any, UserState> {
       // Couldnt get a refresh working. This does for now.
       location.reload();
     }
+    document.title = `/u/${this.state.username} - ${this.state.site.name}`;
+    setupTippy();
   }
 
   render() {
@@ -241,14 +226,20 @@ export class User extends Component<any, UserState> {
                     class="rounded-circle mr-2"
                   />
                 )}
-                <span>/u/{this.state.user.name}</span>
+                <span>/u/{this.state.username}</span>
               </h5>
               {this.selects()}
-              {this.state.view == View.Overview && this.overview()}
-              {this.state.view == View.Comments && this.comments()}
-              {this.state.view == View.Posts && this.posts()}
-              {this.state.view == View.Saved && this.overview()}
-              {this.paginator()}
+              <UserDetails
+                user_id={this.state.user_id}
+                username={this.state.username}
+                sort={SortType[this.state.sort]}
+                page={this.state.page}
+                limit={fetchLimit}
+                enableDownvotes={this.state.site.enable_downvotes}
+                enableNsfw={this.state.site.enable_nsfw}
+                view={this.state.view}
+                onPageChange={this.handlePageChange}
+              />
             </div>
             <div class="col-12 col-md-4">
               {this.userInfo()}
@@ -267,52 +258,52 @@ export class User extends Component<any, UserState> {
       <div class="btn-group btn-group-toggle">
         <label
           className={`btn btn-sm btn-secondary pointer btn-outline-light
-            ${this.state.view == View.Overview && 'active'}
+            ${this.state.view == UserDetailsView.Overview && 'active'}
           `}
         >
           <input
             type="radio"
-            value={View.Overview}
-            checked={this.state.view == View.Overview}
+            value={UserDetailsView.Overview}
+            checked={this.state.view === UserDetailsView.Overview}
             onChange={linkEvent(this, this.handleViewChange)}
           />
           {i18n.t('overview')}
         </label>
         <label
           className={`btn btn-sm btn-secondary pointer btn-outline-light
-            ${this.state.view == View.Comments && 'active'}
+            ${this.state.view == UserDetailsView.Comments && 'active'}
           `}
         >
           <input
             type="radio"
-            value={View.Comments}
-            checked={this.state.view == View.Comments}
+            value={UserDetailsView.Comments}
+            checked={this.state.view == UserDetailsView.Comments}
             onChange={linkEvent(this, this.handleViewChange)}
           />
           {i18n.t('comments')}
         </label>
         <label
           className={`btn btn-sm btn-secondary pointer btn-outline-light
-            ${this.state.view == View.Posts && 'active'}
+            ${this.state.view == UserDetailsView.Posts && 'active'}
           `}
         >
           <input
             type="radio"
-            value={View.Posts}
-            checked={this.state.view == View.Posts}
+            value={UserDetailsView.Posts}
+            checked={this.state.view == UserDetailsView.Posts}
             onChange={linkEvent(this, this.handleViewChange)}
           />
           {i18n.t('posts')}
         </label>
         <label
           className={`btn btn-sm btn-secondary pointer btn-outline-light
-            ${this.state.view == View.Saved && 'active'}
+            ${this.state.view == UserDetailsView.Saved && 'active'}
           `}
         >
           <input
             type="radio"
-            value={View.Saved}
-            checked={this.state.view == View.Saved}
+            value={UserDetailsView.Saved}
+            checked={this.state.view == UserDetailsView.Saved}
             onChange={linkEvent(this, this.handleViewChange)}
           />
           {i18n.t('saved')}
@@ -346,82 +337,6 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  overview() {
-    let combined: Array<{ type_: string; data: Comment | Post }> = [];
-    let comments = this.state.comments.map(e => {
-      return { type_: 'comments', data: e };
-    });
-    let posts = this.state.posts.map(e => {
-      return { type_: 'posts', data: e };
-    });
-
-    combined.push(...comments);
-    combined.push(...posts);
-
-    // Sort it
-    if (this.state.sort == SortType.New) {
-      combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
-    } else {
-      combined.sort((a, b) => b.data.score - a.data.score);
-    }
-
-    return (
-      <div>
-        {combined.map(i => (
-          <div>
-            {i.type_ == 'posts' ? (
-              <PostListing
-                post={i.data as Post}
-                admins={this.state.admins}
-                showCommunity
-                enableDownvotes={this.state.site.enable_downvotes}
-                enableNsfw={this.state.site.enable_nsfw}
-              />
-            ) : (
-              <CommentNodes
-                nodes={[{ comment: i.data as Comment }]}
-                admins={this.state.admins}
-                noIndent
-                showContext
-                enableDownvotes={this.state.site.enable_downvotes}
-              />
-            )}
-          </div>
-        ))}
-      </div>
-    );
-  }
-
-  comments() {
-    return (
-      <div>
-        <CommentNodes
-          nodes={commentsToFlatNodes(this.state.comments)}
-          admins={this.state.admins}
-          noIndent
-          showContext
-          enableDownvotes={this.state.site.enable_downvotes}
-        />
-      </div>
-    );
-  }
-
-  posts() {
-    return (
-      <div>
-        {this.state.posts.map(post => (
-          <PostListing
-            post={post}
-            admins={this.state.admins}
-            showCommunity
-            enableDownvotes={this.state.site.enable_downvotes}
-            enableNsfw={this.state.site.enable_nsfw}
-          />
-        ))}
-      </div>
-    );
-  }
-
   userInfo() {
     let user = this.state.user;
     return (
@@ -440,6 +355,15 @@ export class User extends Component<any, UserState> {
                 )}
               </ul>
             </h5>
+            <div className="d-flex align-items-center mb-2">
+              <svg class="icon">
+                <use xlinkHref="#icon-cake"></use>
+              </svg>
+              <span className="ml-2">
+                {i18n.t('cake_day_title')}{' '}
+                {moment.utc(user.published).local().format('MMM DD, YYYY')}
+              </span>
+            </div>
             <div>
               {i18n.t('joined')} <MomentTime data={user} showAgo />
             </div>
@@ -525,7 +449,7 @@ export class User extends Component<any, UserState> {
                     htmlFor="file-upload"
                     class="pointer ml-4 text-muted small font-weight-bold"
                   >
-                    {!this.state.userSettingsForm.avatar ? (
+                    {!this.checkSettingsAvatar ? (
                       <span class="btn btn-sm btn-secondary">
                         {i18n.t('upload_avatar')}
                       </span>
@@ -549,6 +473,18 @@ export class User extends Component<any, UserState> {
                   />
                 </form>
               </div>
+              {this.checkSettingsAvatar && (
+                <div class="form-group">
+                  <button
+                    class="btn btn-secondary btn-block"
+                    onClick={linkEvent(this, this.removeAvatar)}
+                  >
+                    {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
+                      'avatar'
+                    )}`}
+                  </button>
+                </div>
+              )}
               <div class="form-group">
                 <label>{i18n.t('language')}</label>
                 <select
@@ -872,75 +808,30 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  paginator() {
-    return (
-      <div class="my-2">
-        {this.state.page > 1 && (
-          <button
-            class="btn btn-sm btn-secondary mr-1"
-            onClick={linkEvent(this, this.prevPage)}
-          >
-            {i18n.t('prev')}
-          </button>
-        )}
-        <button
-          class="btn btn-sm btn-secondary"
-          onClick={linkEvent(this, this.nextPage)}
-        >
-          {i18n.t('next')}
-        </button>
-      </div>
-    );
-  }
-
-  updateUrl() {
-    let viewStr = View[this.state.view].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const page = paramUpdates.page || this.state.page;
+    const viewStr =
+      paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
+    const sortStr =
+      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
     this.props.history.push(
-      `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
+      `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
     );
   }
 
-  nextPage(i: User) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.refetch();
-  }
-
-  prevPage(i: User) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.refetch();
-  }
-
-  refetch() {
-    let form: GetUserDetailsForm = {
-      user_id: this.state.user_id,
-      username: this.state.username,
-      sort: SortType[this.state.sort],
-      saved_only: this.state.view == View.Saved,
-      page: this.state.page,
-      limit: fetchLimit,
-    };
-    WebSocketService.Instance.getUserDetails(form);
+  handlePageChange(page: number) {
+    this.updateUrl({ page });
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.setState(this.state);
-    this.updateUrl();
-    this.refetch();
+    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
   }
 
   handleViewChange(i: User, event: any) {
-    i.state.view = Number(event.target.value);
-    i.state.page = 1;
-    i.setState(i.state);
-    i.updateUrl();
-    i.refetch();
+    i.updateUrl({
+      view: UserDetailsView[Number(event.target.value)].toLowerCase(),
+      page: 1,
+    });
   }
 
   handleUserSettingsShowNsfwChange(i: User, event: any) {
@@ -1061,6 +952,22 @@ export class User extends Component<any, UserState> {
       });
   }
 
+  removeAvatar(i: User, event: any) {
+    event.preventDefault();
+    i.state.userSettingsLoading = true;
+    i.state.userSettingsForm.avatar = '';
+    i.setState(i.state);
+
+    WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
+  }
+
+  get checkSettingsAvatar(): boolean {
+    return (
+      this.state.userSettingsForm.avatar &&
+      this.state.userSettingsForm.avatar != ''
+    );
+  }
+
   handleUserSettingsSubmit(i: User, event: any) {
     event.preventDefault();
     i.state.userSettingsLoading = true;
@@ -1094,103 +1001,66 @@ export class User extends Component<any, UserState> {
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
-    console.log(msg);
-    let res = wsJsonToRes(msg);
+    const res = wsJsonToRes(msg);
     if (msg.error) {
       toast(i18n.t(msg.error), 'danger');
-      this.state.deleteAccountLoading = false;
-      this.state.avatarLoading = false;
-      this.state.userSettingsLoading = false;
       if (msg.error == 'couldnt_find_that_username_or_email') {
         this.context.router.history.push('/');
       }
-      this.setState(this.state);
+      this.setState({
+        deleteAccountLoading: false,
+        avatarLoading: false,
+        userSettingsLoading: false,
+      });
       return;
-    } else if (msg.reconnect) {
-      this.refetch();
     } else if (res.op == UserOperation.GetUserDetails) {
-      let data = res.data as UserDetailsResponse;
-      this.state.user = data.user;
-      this.state.comments = data.comments;
-      this.state.follows = data.follows;
-      this.state.moderates = data.moderates;
-      this.state.posts = data.posts;
-      this.state.admins = data.admins;
-      this.state.loading = false;
-      if (this.isCurrentUser) {
-        this.state.userSettingsForm.show_nsfw =
-          UserService.Instance.user.show_nsfw;
-        this.state.userSettingsForm.theme = UserService.Instance.user.theme
-          ? UserService.Instance.user.theme
-          : 'darkly';
-        this.state.userSettingsForm.default_sort_type =
-          UserService.Instance.user.default_sort_type;
-        this.state.userSettingsForm.default_listing_type =
-          UserService.Instance.user.default_listing_type;
-        this.state.userSettingsForm.lang = UserService.Instance.user.lang;
-        this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
-        this.state.userSettingsForm.email = this.state.user.email;
-        this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
-        this.state.userSettingsForm.show_avatars =
-          UserService.Instance.user.show_avatars;
-        this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
-      }
-      document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
-      window.scrollTo(0, 0);
-      this.setState(this.state);
-      setupTippy();
-    } else if (res.op == UserOperation.EditComment) {
-      let data = res.data as CommentResponse;
-      editCommentRes(data, this.state.comments);
-      this.setState(this.state);
-    } else if (res.op == UserOperation.CreateComment) {
-      let data = res.data as CommentResponse;
-      if (
-        UserService.Instance.user &&
-        data.comment.creator_id == UserService.Instance.user.id
-      ) {
-        toast(i18n.t('reply_sent'));
+      // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
+      // and set the parent state if it is not set or differs
+      const data = res.data as UserDetailsResponse;
+
+      if (this.state.user.id !== data.user.id) {
+        this.state.user = data.user;
+        this.state.follows = data.follows;
+        this.state.moderates = data.moderates;
+
+        if (this.isCurrentUser) {
+          this.state.userSettingsForm.show_nsfw =
+            UserService.Instance.user.show_nsfw;
+          this.state.userSettingsForm.theme = UserService.Instance.user.theme
+            ? UserService.Instance.user.theme
+            : 'darkly';
+          this.state.userSettingsForm.default_sort_type =
+            UserService.Instance.user.default_sort_type;
+          this.state.userSettingsForm.default_listing_type =
+            UserService.Instance.user.default_listing_type;
+          this.state.userSettingsForm.lang = UserService.Instance.user.lang;
+          this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+          this.state.userSettingsForm.email = this.state.user.email;
+          this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
+          this.state.userSettingsForm.show_avatars =
+            UserService.Instance.user.show_avatars;
+          this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
+        }
+        this.setState(this.state);
       }
-    } else if (res.op == UserOperation.SaveComment) {
-      let data = res.data as CommentResponse;
-      saveCommentRes(data, this.state.comments);
-      this.setState(this.state);
-    } else if (res.op == UserOperation.CreateCommentLike) {
-      let data = res.data as CommentResponse;
-      createCommentLikeRes(data, this.state.comments);
-      this.setState(this.state);
-    } else if (res.op == UserOperation.CreatePostLike) {
-      let data = res.data as PostResponse;
-      createPostLikeFindRes(data, this.state.posts);
-      this.setState(this.state);
-    } else if (res.op == UserOperation.BanUser) {
-      let data = res.data as BanUserResponse;
-      this.state.comments
-        .filter(c => c.creator_id == data.user.id)
-        .forEach(c => (c.banned = data.banned));
-      this.state.posts
-        .filter(c => c.creator_id == data.user.id)
-        .forEach(c => (c.banned = data.banned));
-      this.setState(this.state);
-    } else if (res.op == UserOperation.AddAdmin) {
-      let data = res.data as AddAdminResponse;
-      this.state.admins = data.admins;
-      this.setState(this.state);
     } else if (res.op == UserOperation.SaveUserSettings) {
-      let data = res.data as LoginResponse;
-      this.state = this.emptyState;
-      this.state.userSettingsLoading = false;
-      this.setState(this.state);
+      const data = res.data as LoginResponse;
       UserService.Instance.login(data);
+      this.setState({
+        userSettingsLoading: false,
+      });
+      window.scrollTo(0, 0);
     } else if (res.op == UserOperation.DeleteAccount) {
-      this.state.deleteAccountLoading = false;
-      this.state.deleteAccountShowConfirm = false;
-      this.setState(this.state);
+      this.setState({
+        deleteAccountLoading: false,
+        deleteAccountShowConfirm: false,
+      });
       this.context.router.history.push('/');
     } else if (res.op == UserOperation.GetSite) {
-      let data = res.data as GetSiteResponse;
-      this.state.site = data.site;
-      this.setState(this.state);
+      const data = res.data as GetSiteResponse;
+      this.setState({
+        site: data.site,
+      });
     }
   }
 }
index 0e1a181b207462717abfa470980b2a599efbe890..2c915196a72ed16fefa1d66df3398a5ca82e6004 100644 (file)
@@ -16,7 +16,7 @@
     <!-- Styles -->
     <link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
     <link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
-    <link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" />
+    <link rel="stylesheet" type="text/css" href="/static/assets/css/choices.min.css" />
     <link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" />
     <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/litely.min.css" id="default-light" media="(prefers-color-scheme: light)" />
     <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="default-dark" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" />
index 7e29319f9a666a0029ab35444b32118f6add1b01..dc860e0684acb03b3a2b9635a272772ee6eb83ab 100644 (file)
@@ -54,6 +54,11 @@ export enum CommentSortType {
   Old,
 }
 
+export enum CommentViewType {
+  Tree,
+  Chat,
+}
+
 export enum ListingType {
   All,
   Subscribed,
@@ -183,6 +188,7 @@ export interface Post {
   creator_actor_id: string;
   creator_local: boolean;
   creator_name: string;
+  creator_published: string;
   creator_avatar?: string;
   community_actor_id: string;
   community_local: boolean;
@@ -210,6 +216,7 @@ export interface Comment {
   local: boolean;
   creator_id: number;
   post_id: number;
+  post_name: string;
   parent_id?: number;
   content: string;
   removed: boolean;
@@ -227,6 +234,7 @@ export interface Comment {
   creator_local: boolean;
   creator_name: string;
   creator_avatar?: string;
+  creator_published: string;
   score: number;
   upvotes: number;
   downvotes: number;
@@ -929,3 +937,10 @@ export interface WebSocketJsonResponse {
   error?: string;
   reconnect?: boolean;
 }
+
+export enum UserDetailsView {
+  Overview,
+  Comments,
+  Posts,
+  Saved,
+}
index 3b0777944687cbc3040be9ad495da3641a06c32e..2bede77704aeee5553a48df68453a2ac810ea8ad 100644 (file)
@@ -51,11 +51,10 @@ import Tribute from 'tributejs/src/Tribute.js';
 import markdown_it from 'markdown-it';
 import markdownitEmoji from 'markdown-it-emoji/light';
 import markdown_it_container from 'markdown-it-container';
-import twemoji from 'twemoji';
 import emojiShortName from 'emoji-short-name';
 import Toastify from 'toastify-js';
 import tippy from 'tippy.js';
-import EmojiButton from '@joeattardi/emoji-button';
+import moment from 'moment';
 
 export const repoUrl = 'https://github.com/LemmyNet/lemmy';
 export const helpGuideUrl = '/docs/about_guide.html';
@@ -114,14 +113,6 @@ export const themes = [
   'litely',
 ];
 
-export const emojiPicker = new EmojiButton({
-  // Use the emojiShortName from native
-  style: 'twemoji',
-  theme: 'dark',
-  position: 'auto-start',
-  // TODO i18n
-});
-
 const DEFAULT_ALPHABET =
   'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 
@@ -178,10 +169,6 @@ export const md = new markdown_it({
     defs: objectFlip(emojiShortName),
   });
 
-md.renderer.rules.emoji = function (token, idx) {
-  return twemoji.parse(token[idx].content);
-};
-
 export function hotRankComment(comment: Comment): number {
   return hotRank(comment.score, comment.published);
 }
@@ -501,6 +488,19 @@ export function showAvatars(): boolean {
   );
 }
 
+export function isCakeDay(published: string): boolean {
+  // moment(undefined) or moment.utc(undefined) returns the current date/time
+  // moment(null) or moment.utc(null) returns null
+  const userCreationDate = moment.utc(published || null).local();
+  const currentDate = moment(new Date());
+
+  return (
+    userCreationDate.date() === currentDate.date() &&
+    userCreationDate.month() === currentDate.month() &&
+    userCreationDate.year() !== currentDate.year()
+  );
+}
+
 // Converts to image thumbnail
 export function pictrsImage(hash: string, thumbnail: boolean = false): string {
   let root = `/pictrs/image`;
@@ -590,8 +590,7 @@ export function setupTribute(): Tribute {
         trigger: ':',
         menuItemTemplate: (item: any) => {
           let shortName = `:${item.original.key}:`;
-          let twemojiIcon = twemoji.parse(item.original.val);
-          return `${twemojiIcon} ${shortName}`;
+          return `${item.original.val} ${shortName}`;
         },
         selectTemplate: (item: any) => {
           return `:${item.original.key}:`;
@@ -988,3 +987,12 @@ function canUseWebP() {
   // // very old browser like IE 8, canvas not supported
   // return false;
 }
+
+export function validTitle(title?: string): boolean {
+  // Initial title is null, minimum length is taken care of by textarea's minLength={3}
+  if (title === null || title.length < 3) return true;
+
+  const regex = new RegExp(/.*\S.*/, 'g');
+
+  return regex.test(title);
+}
index bba9525b1ed1b34822780dd342f60f89ab2d46d7..bfbd20a89d646e5a15fe38af33a9f4524f3d7182 100644 (file)
@@ -1 +1 @@
-export const version: string = 'v0.7.13';
+export const version: string = 'v0.7.21';
index 62b11ce4a7fa4ab2dd2c4c207525733b18d1a980..90c4a9959cb58a82560511ac2ebd465cbde28154 100644 (file)
     "no": "no",
     "powered_by": "Powered by",
     "landing_0":
-      "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+      "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "Not logged in.",
     "logged_in": "Logged in.",
+    "must_login": "You must <1>log in or register</1> to comment.",
     "site_saved": "Site Saved.",
     "community_ban": "You have been banned from this community.",
     "site_ban": "You have been banned from the site",
       "Couldn't find that username or email.",
     "password_incorrect": "Password incorrect.",
     "passwords_dont_match": "Passwords do not match.",
+    "no_password_reset": "You will not be able to reset your password without an email.",
     "invalid_username": "Invalid username.",
     "admin_already_created": "Sorry, there's already an admin.",
     "user_already_exists": "User already exists.",
     "action": "Action",
     "emoji_picker": "Emoji Picker",
     "block_leaving": "Are you sure you want to leave?",
-    "what_is": "What is"
+    "what_is": "What is",
+    "cake_day_title": "Cake day:",
+    "cake_day_info": "It's {{ creator_name }}'s cake day today!",
+    "invalid_post_title": "Invalid post title"
 }
index e74d5839080f2baca8b18d817c64b570bac15e28..0563880dab60c3529f28873f48db2290a82933d7 100644 (file)
 {
-    "post": "Argitaratu",
-    "remove_post": "Argitalpena Ezabatu",
-    "no_posts": "Argitalpenik gabe.",
-    "create_a_post": "Argitalpen bat sortu",
-    "number_of_posts": "Argitalpen {{count}}",
-    "number_of_posts_plural": "{{count}} Argitalpen",
+    "post": "bidali",
+    "remove_post": "Ezabatu bidalketa",
+    "no_posts": "Ez dago bidalketarik.",
+    "create_a_post": "Sortu bidalketa bat",
+    "number_of_posts": "Bidalketa {{count}}",
+    "number_of_posts_plural": "{{count}} bidalketa",
     "users": "Erabiltzaileak",
-    "send_message": "Mezua Bidali",
-    "message": "Mezu",
-    "edit": "Editatu",
-    "reply": "Erantzun",
+    "send_message": "Bidali mezua",
+    "message": "Mezua",
+    "edit": "editatu",
+    "reply": "erantzun",
     "more": "gehiago",
-    "upload_image": "Irudia igo",
+    "upload_image": "igo irudia",
     "link": "esteka",
     "remove": "ezabatu",
-    "mark_as_unread": "irakurri gabe",
+    "mark_as_unread": "markatu ez irakurrita",
     "delete": "ezabatu",
-    "delete_account": "Ezabatu Kontua",
-    "ban": "kaleratu",
-    "ban_from_site": "kaleratu gunetik",
-    "unban": "onartu",
+    "delete_account": "Ezabatu kontua",
+    "ban": "debekatu",
+    "ban_from_site": "debekatu gunean",
+    "unban": "kendu debekua",
     "save": "gorde",
     "create": "sortu",
     "creator": "sortzailea",
-    "username": "Erabiltzailearen izena",
+    "username": "Erabiltzaile-izena",
     "name": "Izena",
     "title": "Izenburua",
     "both": "Biak",
     "saved": "Gordeta",
-    "week": "Astea",
-    "month": "Hilabetea",
-    "year": "Urtea",
-    "all": "Dena",
+    "week": "Astekoak",
+    "month": "Hilabetekoak",
+    "year": "Urtekoak",
+    "all": "Guztiak",
     "api": "API",
     "unread": "Irakurri gabe",
     "replies": "Erantzunak",
     "search": "Bilatu",
-    "sign_up": "Erregistratu",
+    "sign_up": "Eman izena",
     "messages": "Mezuak",
     "password": "Pasahitza",
-    "password_change": "Pasahitza Aldatu",
-    "new_password": "Pasahitz Berria",
-    "email": "Posta elektronikoa",
+    "password_change": "Aldatu pasahitza",
+    "new_password": "Pasahitz berria",
+    "email": "Eposta",
     "language": "Hizkuntza",
     "url": "URL",
-    "chat": "Txat",
+    "chat": "Txata",
     "your_site": "zure gunea",
-    "nsfw": "NSFW",
-    "block_leaving": "Ziur zaude atera nahi duzula?",
+    "nsfw": "NSFW (eduki hunkigarria)",
+    "block_leaving": "Ziur al zaude atera nahi duzula?",
     "bitcoin": "Bitcoin",
     "ethereum": "Ethereum",
     "monero": "Monero",
     "yes": "bai",
     "no": "ez",
-    "couldnt_find_post": "Ezin izan da post-a aurkitu.",
-    "couldnt_save_post": "Ezin izan da argitalpena gorde.",
-    "site_already_exists": "Gunea jada existitzen da.",
+    "couldnt_find_post": "Ezin izan da bidalketarik aurkitu.",
+    "couldnt_save_post": "Ezin izan da bidalketa gorde.",
+    "site_already_exists": "Gunea dagoeneko existitzen da.",
     "action": "Ekintza",
     "time": "Denbora",
     "number_of_points": "Puntu {{count}}",
-    "number_of_points_plural": "{{count}} Puntu",
+    "number_of_points_plural": "{{count}} puntu",
     "number_of_users": "Erabiltzaile {{count}}",
-    "number_of_users_plural": "{{count}} Erabiltzaile",
-    "number_of_subscribers": "Jarraitzaile {{count}}",
-    "number_of_subscribers_plural": "{{count}} Jarraitzaile",
+    "number_of_users_plural": "{{count}} erabiltzaile",
+    "number_of_subscribers": "Harpidetu {{count}}",
+    "number_of_subscribers_plural": "{{count}} harpidetu",
     "invalid_community_name": "Izen baliogabea.",
     "click_to_delete_picture": "Egin klik irudia ezabatzeko.",
     "picture_deleted": "Irudia ezabatuta.",
-    "send_secure_message": "Mezu Segurua Bidali",
-    "preview": "Aurretiko bista",
-    "avatar": "Profilaren argazkia",
-    "upload_avatar": "Profileko argazki bat igo",
-    "show_avatars": "Profilen argazkia erakutsi",
+    "send_secure_message": "Bidali mezu segurua",
+    "preview": "Aurrebista",
+    "avatar": "Avatarra",
+    "upload_avatar": "Igo avatarra",
+    "show_avatars": "Erakutsi avatarrak",
     "show_context": "Erakutsi testuingurua",
     "formatting_help": "formatuaren laguntza",
-    "sorting_help": "sailkatzeko laguntza",
-    "view_source": "Iturria ikusi",
+    "sorting_help": "ordenatzeko laguntza",
+    "view_source": "Ikusi iturria",
     "unlock": "desblokeatu",
     "lock": "blokeatu",
     "sticky": "finkatuta",
     "unsticky": "finkatu gabe",
     "archive_link": "artxiboko esteka",
-    "mod": "Moderatzailea",
-    "mods": "Moderatzaileak",
+    "mod": "moderatzailea",
+    "mods": "moderatzaileak",
     "moderates": "Moderatuak",
-    "settings": "Konfigurazioa",
-    "admin_settings": "Administrazio-doikuntzak",
-    "site_config": "Gunearen Konfigurazioa",
+    "settings": "Ezarpenak",
+    "admin_settings": "Administrazio ezarpenak",
+    "site_config": "Gunearen konfigurazioa",
     "remove_as_mod": "ezabatu moderatzaile gisa",
-    "modlog": "Moderatzailearen erregistroa",
-    "appoint_as_mod": "moderatzaile bezala izendatu",
+    "modlog": "Moderazio loga",
+    "appoint_as_mod": "izendatu moderatzaile gisa",
     "admin": "administratzailea",
     "admins": "administratzaileak",
     "remove_as_admin": "ezabatu administratzaile gisa",
-    "appoint_as_admin": "administratzaile bezala izendatu",
+    "appoint_as_admin": "izendatu administratzaile gisa",
     "removed": "moderatzaileak ezabatua",
     "locked": "blokeatuta",
     "number_online": "Erabiltzaile {{count}} konektatuta",
-    "number_online_plural": "{{count}} Erabiltzaile konektatutak",
+    "number_online_plural": "{{count}} erabiltzaile konektatuta",
     "subscribed": "Harpidetuta",
     "prev": "Aurrekoa",
-    "create_community": "Komunitate bat sortu",
-    "create_post": "Sortu argitalpena",
-    "posts": "Argitalpenak",
-    "related_posts": "Argitalpen horiek zerikusia izan dezakete",
+    "create_community": "Sortu komunitatea",
+    "create_post": "Sortu bidalketa",
+    "posts": "Bidalketak",
+    "related_posts": "Bidalketa hauek zerikusia izan dezakete",
     "cross_posts": "Esteka hau ere hemen argitaratu da:",
-    "comments": "Iradokizunak",
-    "number_of_comments": "Iradokizun {{count}}",
-    "number_of_comments_plural": "{{count}} Iradokizun",
-    "remove_comment": "Iradokizunak Ezabatu",
+    "comments": "Iruzkinak",
+    "number_of_comments": "Iruzkin {{count}}",
+    "number_of_comments_plural": "{{count}} iruzkin",
+    "remove_comment": "Ezabatu iruzkina",
     "communities": "Komunitateak",
-    "create_a_community": "Komunitate bat sortu",
-    "cross_post": "Argitalpen gurutzatua",
-    "cross_posted_to": "Argitalpen-gurutzatua: ",
+    "create_a_community": "Sortu komunitate bat",
+    "cross_post": "bidalketa gurutzatua",
+    "cross_posted_to": "bidalketa gurutzatua: ",
     "next": "Hurrengoa",
-    "remove_community": "Komunitatea ezabatu",
-    "subscribed_to_communities": "<1>komunitateetan</1> harpidetuta",
-    "trending_communities": "<1>komunitateen</1> joerak",
-    "list_of_communities": "Komunitateen zerrenda",
-    "community_reqs": "Letra xehez, azpimarratuta eta espaziorik gabe.",
-    "create_private_message": "Mezu pribatu bat sortu",
+    "remove_community": "Ezabatu komunitatea",
+    "subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
+    "trending_communities": "<1>Komunitateen</1> joerak",
+    "list_of_communities": "Komunitate-zerrenda",
+    "community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
+    "create_private_message": "Sortu mezu pribatua",
     "cancel": "Ezeztatu",
     "stickied": "finkatuta",
-    "reason": "Arrazoi",
-    "mark_as_read": "markatu irakurrita bezala",
+    "reason": "Arrazoia",
+    "mark_as_read": "markatu irakurrita gisa",
     "deleted": "sortzaileak ezabatua",
-    "delete_account_confirm": "Ohartarazpena: horrek etengabe ezabatuko ditu zure datu guztiak. Idatzi zure pasahitza baieztatzeko.",
+    "delete_account_confirm": "Abisua: honek zure datu guztiak betirako ezabatu ditu. Sartu zure pasahitza baieztatzeko.",
     "restore": "leheneratu",
-    "unban_from_site": "gunetik debekua kentzea",
-    "banned": "kaleratuta",
-    "banned_users": "Kaleratutako Erabiltzaileak",
+    "unban_from_site": "kendu debekua gunean",
+    "banned": "debekatuta",
+    "banned_users": "Debekatutako erabiltzaileak",
     "unsave": "ez gorde",
-    "email_or_username": "e-Posta edo Erabiltzailea",
+    "email_or_username": "Eposta edo erabiltzaile-izena",
     "category": "Kategoria",
-    "subscribers": "Jarraitzaileak",
-    "unsubscribe": "Ez-harpidetu",
+    "subscribers": "Harpidetuak",
+    "unsubscribe": "Ezabatu harpidetza",
     "subscribe": "Harpidetu",
     "sidebar": "Alboko barra",
-    "sort_type": "Sailkapen mota",
+    "sort_type": "Ordena-mota",
     "hot": "Pil-pilean",
-    "new": "Berri",
-    "old": "Zahar",
-    "top_day": "Eguneko hoberenak",
-    "top": "Hoberena",
+    "new": "Berriak",
+    "old": "Zaharrak",
+    "top_day": "Gaur pil-pilean",
+    "top": "Bozkatuenak",
     "docs": "Dokumentazioa",
-    "inbox": "Mezuen sarrera",
-    "inbox_for": "Sarrera-erretilua <1>{{user}}</1> -rentzat",
-    "mark_all_as_read": "markatu dena irakurrita bezala",
+    "inbox": "Sarrera-erretilua",
+    "inbox_for": "<1>{{user}}</1>(r)en sarrera-erretilua",
+    "mark_all_as_read": "markatu guztiak irakurrita gisa",
     "type": "Mota",
-    "number_of_communities": "{{count}} Komunitate",
-    "number_of_communities_plural": "{{count}} Komunitateak",
+    "number_of_communities": "Komunitate {{count}}",
+    "number_of_communities_plural": "{{count}} komunitate",
     "mentions": "Aipamenak",
-    "reply_sent": "Bidalitako erantzuna",
-    "message_sent": "Bidalitako mezua",
-    "overview": "Laburpen",
+    "reply_sent": "Erantzuna bidali da",
+    "message_sent": "Mezua bidali da",
+    "overview": "Laburpena",
     "view": "Ikusi",
-    "logout": "Irten",
-    "login_sign_up": "Sartu / Kontua Sortu",
-    "login": "Sartu",
-    "notifications_error": "Mahaigaineko jakinarazpenak ez daude eskuragarri zure web-nabigatzailean. Probatu Firefox edo Chromerekin.",
+    "logout": "Itxi saioa",
+    "login_sign_up": "Hasi saioa / Eman izena",
+    "login": "Hasi saioa",
+    "notifications_error": "Mahaigaineko jakinarazpenak ez daude eskuragarri zure web-nabigatzailean. Probatu Firefoxekin edo Chromekin.",
     "unread_messages": "Irakurri gabeko mezuak",
-    "verify_password": "Pasahitza Balioztatu",
-    "old_password": "Aurreko Pasahitza",
+    "verify_password": "Balioztatu pasahitza",
+    "old_password": "Aurreko pasahitza",
     "forgot_password": "pasahitza ahaztu dut",
-    "reset_password_mail_sent": "Mezu elektroniko bat bidali pasahitza berrezartzeko.",
-    "no_email_setup": "Zerbitzari honek ez du posta elektronikoa behar bezala konfiguratu.",
-    "matrix_user_id": "Matrix Erabiltzailea",
-    "private_message_disclaimer": "Ohartarazpena: Lemmy-en dauden mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1> -en mezu seguruetarako.",
-    "send_notifications_to_email": "Bidali jakinarazpenak posta elektronikora",
-    "optional": "Ez-ohikoa",
-    "browser_default": "Nabigatzaile Lehenetsia",
-    "downvotes_disabled": "Puntuazio negatiboak desgaituta daude",
-    "enable_downvotes": "Kontrako botoak gaitu",
-    "upvote": "Aldeko botoa eman",
-    "downvote": "Kontrako botoa eman",
-    "number_of_downvotes": "Kontrako boto {{count}}",
-    "number_of_downvotes_plural": "{{count}} Kontrako botoak",
-    "number_of_upvotes": "Aldeko boto {{count}}",
-    "number_of_upvotes_plural": "{{count}} Aldeko botoak",
-    "open_registration": "Erregistro Irekia",
-    "registration_closed": "Erregistroa itxita",
-    "enable_nsfw": "NSFW gaitu",
+    "reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
+    "no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
+    "matrix_user_id": "Matrix erabiltzailea",
+    "private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1>en mezu seguruak trukatzeko.",
+    "send_notifications_to_email": "Bidali jakinarazpenak epostara",
+    "optional": "Hautazkoa",
+    "browser_default": "Nabigatzaileko lehenetsia",
+    "downvotes_disabled": "Kontrako bozkak desgaituta",
+    "enable_downvotes": "Gaitu kontrako bozkak",
+    "upvote": "Alde bozkatu",
+    "downvote": "Kontra bozkatu",
+    "number_of_downvotes": "Kontrako bozka {{count}}",
+    "number_of_downvotes_plural": "{{count}} kontrako bozka",
+    "number_of_upvotes": "Aldeko bozka {{count}}",
+    "number_of_upvotes_plural": "{{count}} aldeko bozka",
+    "open_registration": "Izen-ematea irekia",
+    "registration_closed": "Izen-ematea itxira",
+    "enable_nsfw": "Gaitu NSFW (eduki hunkigarria)",
     "body": "Gorputza",
-    "copy_suggested_title": "Kopiatu iradokitako izenburua: {{title}}",
+    "copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
     "community": "Komunitatea",
     "expand_here": "Hedatu hemen",
     "subscribe_to_communities": "Harpidetu zaitez <1>komunitate</1> batzuetara.",
-    "recent_comments": "Duela gutxiko Iruzkinak",
+    "recent_comments": "Iruzkin berrienak",
     "select_a_community": "Aukeratu komunitate bat",
-    "no_results": "Emaitzik gabe.",
-    "setup": "Instalazioa",
-    "lemmy_instance_setup": "Lemmy Instantziaren Konfigurazioa",
-    "setup_admin": "Gunearen Administratzailea Konfiguratu",
+    "no_results": "Ez dago emaitzarik.",
+    "setup": "Ezarpenak",
+    "lemmy_instance_setup": "Lemmy instantziaren ezarpena",
+    "setup_admin": "Ezarri gunearen administratzailea",
     "modified": "aldatuta",
-    "show_nsfw": "Erakutsi NSFW edukia",
-    "expires": "Iraungitzen",
-    "theme": "Gaia",
+    "show_nsfw": "Erakutsi eduki hunkigarria (NSFW)",
+    "expires": "Noiz iraungitzen da:",
+    "theme": "Itxura",
     "sponsors": "Babesleak",
-    "sponsors_of_lemmy": "Lemmyren Babesleak",
-    "sponsor_message": "Lemmy software librea da, <1>kode-irekia<1/>, publizitaterik, monetizaziorik edo arrisku kapitalik gabea, inoiz ez. Zuen dohainek zuzenean laguntzen dute proiektuaren lanaldi osoko garapena. Eskerrik asko honako pertsona hauei:",
-    "support_on_patreon": "Patreon-en lagundu",
-    "support_on_liberapay": "Liberpay-en lagundu",
-    "support_on_open_collective": "OpenCollective-n lagundu",
-    "donate_to_lemmy": "Dohaintza bat eman Lemmyri",
-    "donate": "Dohaintza bat egin",
-    "general_sponsors": "Lemmyri 10 eta 39 dolar artean emateko konpromisoa hartu zutenak dira Babesle Nagusiak.",
-    "silver_sponsors": "Zilarrezko Babesleak dira Lemmyri 40 dolar eman zizkiotenak.",
-    "crypto": "Kripto",
+    "sponsors_of_lemmy": "Lemmyren babesleak",
+    "sponsor_message": "Lemmy software librea da, <1>kode irekia</1>, publizitaterik, monetizaziorik eta arrisku kapitalik gabea, inoiz ez. Zuen dohaintzek zuzenean laguntzen dute proiektuaren lanaldi osoko garapena. Eskerrik asko honako pertsona hauei:",
+    "support_on_patreon": "Patreon bitartez lagundu",
+    "support_on_liberapay": "Liberpay bitartez lagundu",
+    "support_on_open_collective": "OpenCollective bitartez lagundu",
+    "donate_to_lemmy": "Egin dohaintza bat Lemmyri",
+    "donate": "Dohaintza egin",
+    "general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman zizkiotenak dira.",
+    "silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman zizkiotenak dira.",
+    "crypto": "Kriptomonetak",
     "code": "Kodea",
     "joined": "Batuta",
     "by": "egilea",
-    "to": "norentzako",
-    "from": "hemendik",
+    "to": "nori",
+    "from": "nork",
     "transfer_community": "transferentzia-komunitatea",
     "transfer_site": "transferentzia-gunea",
-    "are_you_sure": "Ziur ahal zaude?",
+    "are_you_sure": "ziur al zaude?",
     "powered_by": "Egilea",
-    "landing": "Lemmy <1>lotura-agregatzailea</1> /reddit alternatiboa da, eta <2>fedibertsoan</2> lan egiteko erabiltzen da. <3></3>Autohospedagarria da, iruzkin-hari eguneratuak ditu, eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide orrian dago. <5></5><6>Beta bertsio goiztiarra</6> da hau, eta ezaugarri asko hautsita edo desagertuta daude gaur egun. <7></7>Ezaugarri berriak iradokitzea edo akatsak jakinaraztea <8>hemen.</8><9></9> Rust, <11>Actix</11>, <12>Inferno</12>, <13>Typescriptekin egina</13>.",
-    "logged_in": "Konektatuta zaude.",
-    "not_logged_in": "Ez zaude konektatuta.",
-    "site_saved": "Gunea Gordeta.",
+    "landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina.",
+    "logged_in": "Saioa hasi duzu.",
+    "not_logged_in": "Ez duzu saiorik hasi.",
+    "site_saved": "Gunea gorde da.",
     "community_ban": "Komunitate honetan sartzea debekatu dizute.",
     "site_ban": "Gune honetan sartzea debekatu dizute",
-    "couldnt_create_comment": "Ezin izan da iruzkinik egin.",
-    "couldnt_like_comment": "Ezin izan zaio iruzkin bati like bat eman.",
-    "couldnt_update_comment": "Ezin izan zen iruzkina eguneratu.",
-    "couldnt_save_comment": "Ezin izan zen iruzkina gorde.",
-    "couldnt_get_comments": "Ezin izan ziren iruzkinak lortu.",
-    "no_comment_edit_allowed": "Ezin da iruzkina editatu.",
-    "no_post_edit_allowed": "Ezin da post-a editatu.",
-    "no_community_edit_allowed": "Ezin da komunitatea editatu.",
-    "couldnt_find_community": "Ezin izan da komunitatea aurkitu.",
-    "couldnt_update_community": "Ezin izan zen komunitatea eguneratu.",
-    "community_already_exists": "Komunitatea existitzen da jada.",
-    "community_moderator_already_exists": "Komunitatearen moderatzailea existitzen da dagoeneko.",
-    "community_follower_already_exists": "Jarraitzaileen komunitatea existitzen da dagoeneko.",
-    "community_user_already_banned": "Komunitatearen erabiltzaile hau debekatuta dago jada.",
-    "couldnt_create_post": "Argitalpena ezin izan da sortu.",
-    "post_title_too_long": "Argitalpenaren izenburua luzeegia da.",
-    "couldnt_like_post": "Ezin izan zaio like bat eman postari.",
-    "couldnt_get_posts": "Ezin izan ziren postak lortu",
-    "couldnt_update_post": "Ezin izan zen post-a eguneratu",
+    "couldnt_create_comment": "Ezin izan da iruzkina sortu.",
+    "couldnt_like_comment": "Ezin izan da iruzkinari datsegit eman.",
+    "couldnt_update_comment": "Ezin izan da iruzkina eguneratu.",
+    "couldnt_save_comment": "Ezin izan da iruzkina gorde.",
+    "couldnt_get_comments": "Ezin izan da iruzkinik lortu.",
+    "no_comment_edit_allowed": "Ezin duzu iruzkina editatu.",
+    "no_post_edit_allowed": "Ezin duzu bidalketa editatu.",
+    "no_community_edit_allowed": "Ezin duzu komunitatea editatu.",
+    "couldnt_find_community": "Ezin izan da komunitaterik aurkitu.",
+    "couldnt_update_community": "Ezin izan da komunitatea eguneratu.",
+    "community_already_exists": "Komunitate hori dagoeneko existitzen da.",
+    "community_moderator_already_exists": "Komunitateko moderatzaile hori dagoeneko existitzen da.",
+    "community_follower_already_exists": "Komunitateko jarraitzaile hori dagoeneko existitzen da.",
+    "community_user_already_banned": "Komunitateko erabiltzaile hau dagoeneko debekatuta dago.",
+    "couldnt_create_post": "Ezin izan da bidalketa sortu.",
+    "post_title_too_long": "Bidalketaren izenburua luzeegia da.",
+    "couldnt_like_post": "Ezin izan da bidalketari datsegit eman.",
+    "couldnt_get_posts": "Ezin izan da bidalketa lortu",
+    "couldnt_update_post": "Ezin izan da bidalketa eguneratu",
     "no_slurs": "Irainik gabe.",
-    "not_an_admin": "Ez da administratzailea.",
-    "couldnt_update_site": "Ezinezkoa lekua berritzea.",
-    "couldnt_find_that_username_or_email": "Ezin izan da aurkitu erabiltzaile-izen edo helbide elektroniko hori.",
-    "password_incorrect": "Pasahitz desegokia.",
+    "not_an_admin": "Ez zara administratzailea.",
+    "couldnt_update_site": "Ezin izan da gunea eguneratu.",
+    "couldnt_find_that_username_or_email": "Ezin izan da aurkitu erabiltzaile-izen edo eposta hori.",
+    "password_incorrect": "Pasahitz okerra.",
     "passwords_dont_match": "Pasahitzak ez dira berdinak.",
-    "admin_already_created": "Barkatu, badago administratzaile bat dagoeneko.",
-    "user_already_exists": "Erabiltzailea existitzen da dagoeneko.",
-    "email_already_exists": "Posta helbide hau beste norbaitek erabiltzen du.",
-    "couldnt_update_user": "Ezin izan zen erabiltzailea eguneratu.",
-    "system_err_login": "Sistemaren errorea. Saiatu saioa ixten eta berriro sartzen.",
-    "couldnt_create_private_message": "Ezinezkoa izan da mezu pribatua sortzea.",
-    "no_private_message_edit_allowed": "Ezin da mezu pribaturik editatu.",
-    "couldnt_update_private_message": "Ezinezkoa izan da mezu pribatua berritzea.",
-    "emoji_picker": "Emoji Hautagailua",
-    "invalid_username": "Erabiltzaile-izen baliogabea."
+    "admin_already_created": "Barkatu, dagoeneko badago administratzaile bat.",
+    "user_already_exists": "Erabiltzaile hori dagoeneko existitzen da.",
+    "email_already_exists": "Eposta hori dagoeneko existitzen da.",
+    "couldnt_update_user": "Ezin izan da erabiltzailea eguneratu.",
+    "system_err_login": "Sistemaren errorea. Saiatu saioa ixten eta berriz hasten.",
+    "couldnt_create_private_message": "Ezin izan da mezu pribatu hori sortu.",
+    "no_private_message_edit_allowed": "Ezin duzu mezu pribaturik editatu.",
+    "couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
+    "emoji_picker": "Emoji hautagailua",
+    "invalid_username": "Erabiltzaile-izen baliogabea.",
+    "what_is": "Zer da"
 }
index cf8c0ea69317496d7ee5b00418d4d6df93b2bb55..0ff126dec24b3eebf6a3bad9a77f6d29a13eba16 100644 (file)
     "click_to_delete_picture": "Clicca per eliminare la foto.",
     "picture_deleted": "Foto eliminata.",
     "select_a_community": "Seleziona una comunità",
-    "invalid_username": "Nome utente non valido."
+    "invalid_username": "Nome utente non valido.",
+    "what_is": "Cos'è"
 }
index 0967ef424bce6791893e9a57bb952f80fd536e93..12a388842907a9e0433b49f2382a35488a4acd57 100644 (file)
@@ -1 +1,73 @@
-{}
+{
+    "password": "Lozinka",
+    "verify_password": "Potvrdite Loziku",
+    "old_password": "Stara Lozinka",
+    "reset_password_mail_sent": "Poslali smo Vam email za promenu lozinke.",
+    "private_message_disclaimer": "Napomena: Privatne poruke poslate putem Lemmy-a nisu osigurane. Molimo vas napravite nalog na <1>Riot.im</1> za bezbednu razmenu poruka.",
+    "browser_default": "Podrazumevano od strane pretraživača",
+    "number_of_upvotes_0": "{{count}} Gore-glas",
+    "number_of_upvotes_1": "{{count}} Gore-glasova",
+    "number_of_upvotes_2": "{{count}} Gore-glasa",
+    "subscribe_to_communities": "Pretplatite se nekim <1>zajednicama</1>.",
+    "show_nsfw": "Prikaži NSFW sadržaj",
+    "sponsor_message": "Lemmy je besplatan, <1>softver otvorenog koda</1>, bez reklamiranja, monetizovanja ili preduzetničkog kapitala, ikada. Vaše donacije direktno podržavaju aktivni razvoj projekta.Zahvaljujemo se sledećim ljudima:",
+    "donate_to_lemmy": "Donirajte Lemmy-u",
+    "general_sponsors": "Generalni Sponzori su oni koji su donirali između $10 i $39 Lemmy-u.",
+    "powered_by": "Sajt pokreće",
+    "forgot_password": "zaboravljena lozinka",
+    "password_change": "Promena Lozinke",
+    "new_password": "Nova Lozinka",
+    "no_email_setup": "Ovaj server nije pravilno namestio Vaš email.",
+    "email": "Email",
+    "matrix_user_id": "Korisnik Matrixa",
+    "send_notifications_to_email": "Primajte notifikacie na Vaš Email",
+    "optional": "Opcionalno",
+    "expires": "Ističe",
+    "language": "Jezik",
+    "downvotes_disabled": "Onemogućite negativne glasove",
+    "enable_downvotes": "Dozvolite negativne glasove",
+    "upvote": "Gore-glas",
+    "downvote": "Dole-glas",
+    "number_of_downvotes_0": "{{count}} Dole-glas",
+    "number_of_downvotes_1": "{{count}} Dole-glasova",
+    "number_of_downvotes_2": "{{count}} Dole-glasa",
+    "open_registration": "Otvorena Registracija",
+    "registration_closed": "Zatvorena registracija",
+    "enable_nsfw": "Dozvolite NSFW sadržaj",
+    "url": "URL",
+    "body": "Sadržaj",
+    "copy_suggested_title": "kopirajte predloženi naslov: {{title}}",
+    "community": "Zajednica",
+    "expand_here": "Proširite ovde",
+    "chat": "Ćaskanje",
+    "recent_comments": "Nedavni Komentari",
+    "no_results": "Nema rezultata.",
+    "setup": "Instalacioni proces",
+    "lemmy_instance_setup": "Instaliranje Lemmy Instance",
+    "setup_admin": "Napravite Administratorski Nalog",
+    "your_site": "Vaš sajt",
+    "modified": "izmenjeno",
+    "nsfw": "NSFW",
+    "theme": "Tema",
+    "sponsors": "Sponzori",
+    "sponsors_of_lemmy": "Sponzori Lemmy-a",
+    "support_on_patreon": "Podržite nas na Patreonu",
+    "support_on_liberapay": "Podržite nas na Liberpay-u",
+    "support_on_open_collective": "Podržite nas na OpenCollective",
+    "donate": "Donirajte",
+    "silver_sponsors": "Srebrni Sponzori su oni koji su donirali $40 Lemmy-u.",
+    "crypto": "Kripto",
+    "bitcoin": "Bitcoin",
+    "ethereum": "Ethereum",
+    "monero": "Monero",
+    "code": "Kod",
+    "joined": "Pridružio/la",
+    "by": "od",
+    "to": "do",
+    "from": "od",
+    "transfer_community": "transferujte zajednicu",
+    "transfer_site": "transferujte sajt",
+    "are_you_sure": "da li ste sigurni?",
+    "yes": "da",
+    "no": "ne"
+}
index 076083fb9878bd2f2d4c84f44c9f59553e45b5b6..c39f1dc4b058e3c83f1b87b19f2bd2eea4b5d840 100644 (file)
     exec-sh "^0.3.2"
     minimist "^1.2.0"
 
-"@fortawesome/fontawesome-common-types@^0.2.28":
-  version "0.2.28"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
-  integrity sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg==
-
-"@fortawesome/fontawesome-svg-core@^1.2.22":
-  version "1.2.28"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz#e5b8c8814ef375f01f5d7c132d3c3a2f83a3abf9"
-  integrity sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg==
-  dependencies:
-    "@fortawesome/fontawesome-common-types" "^0.2.28"
-
-"@fortawesome/free-regular-svg-icons@^5.10.2":
-  version "5.13.0"
-  resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz#925a13d8bdda0678f71551828cac80ab47b8150c"
-  integrity sha512-70FAyiS5j+ANYD4dh9NGowTorNDnyvQHHpCM7FpnF7GxtDjBUCKdrFqCPzesEIpNDFNd+La3vex+jDk4nnUfpA==
-  dependencies:
-    "@fortawesome/fontawesome-common-types" "^0.2.28"
-
-"@fortawesome/free-solid-svg-icons@^5.10.2":
-  version "5.13.0"
-  resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
-  integrity sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg==
-  dependencies:
-    "@fortawesome/fontawesome-common-types" "^0.2.28"
-
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
-"@joeattardi/emoji-button@^2.12.1":
-  version "2.12.1"
-  resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-2.12.1.tgz#190df7c00721e04742ed6f8852db828798a4cf98"
-  integrity sha512-rUuCXIcv4mRFK2IUKarYJN6J667wtH234smb1aQILzRf3/ycOoa6yUwnnvjxZeXMsPhuTnz15ndMOP2DhO5nNw==
-  dependencies:
-    "@fortawesome/fontawesome-svg-core" "^1.2.22"
-    "@fortawesome/free-regular-svg-icons" "^5.10.2"
-    "@fortawesome/free-solid-svg-icons" "^5.10.2"
-    "@popperjs/core" "^2.0.0"
-    focus-trap "^5.1.0"
-    tiny-emitter "^2.1.0"
-    tslib "^1.10.0"
-    twemoji "^12.1.5"
-
-"@popperjs/core@^2.0.0":
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.2.3.tgz#0ae22b5650ab0b8fe508047245b66e71fc59e983"
-  integrity sha512-68EQPzEZRrpFavFX40V2+80eqzQIhgza2AGTXW+i8laxSA4It+Y13rmZInrAYoIujp8YO7YJPbvgOesDZcIulQ==
-
 "@popperjs/core@^2.2.0":
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.3.2.tgz#1e56eb99bccddbda6a3e29aa4f3660f5b23edc43"
@@ -1457,6 +1412,15 @@ chardet@^0.7.0:
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
+choices.js@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
+  integrity sha512-JgpeDY0Tmg7tqY6jaW/druSklJSt7W68tXFJIw0GSGWmO37SDAL8o60eICNGbzIODjj02VNNtf5h6TgoHDtCsA==
+  dependencies:
+    deepmerge "^4.2.0"
+    fuse.js "^3.4.5"
+    redux "^4.0.4"
+
 chokidar@^1.6.1:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -1779,7 +1743,7 @@ deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
-deepmerge@^4.2.2:
+deepmerge@^4.2.0, deepmerge@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
@@ -2660,14 +2624,6 @@ fliplog@^0.3.13:
   dependencies:
     chain-able "^1.0.1"
 
-focus-trap@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
-  integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
-  dependencies:
-    tabbable "^4.0.0"
-    xtend "^4.0.1"
-
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -2729,15 +2685,6 @@ fs-extra@^7.0.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-extra@^8.0.1:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
-  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
-  dependencies:
-    graceful-fs "^4.2.0"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2812,6 +2759,11 @@ fuse-concat-with-sourcemaps@^1.0.5:
   dependencies:
     source-map "^0.6.1"
 
+fuse.js@^3.4.5:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
+  integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
+
 gensync@^1.0.0-beta.1:
   version "1.0.0-beta.1"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@@ -2909,7 +2861,7 @@ globals@^12.1.0:
   dependencies:
     type-fest "^0.8.1"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
@@ -4085,15 +4037,6 @@ jsonfile@^4.0.0:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsonfile@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
-  integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
-  dependencies:
-    universalify "^0.1.2"
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4535,11 +4478,6 @@ mkdirp@^0.5.1:
   dependencies:
     minimist "0.0.8"
 
-mobius1-selectr@^2.4.13:
-  version "2.4.13"
-  resolved "https://registry.yarnpkg.com/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz#0019dfd9f984840d6e40f70683ab3ec78ce3b5df"
-  integrity sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw==
-
 moment@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -5303,6 +5241,14 @@ reconnecting-websocket@^4.4.0:
   resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
   integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
 
+redux@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+  dependencies:
+    loose-envify "^1.4.0"
+    symbol-observable "^1.2.0"
+
 regenerate-unicode-properties@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -6154,7 +6100,7 @@ supports-hyperlinks@^2.0.0:
     has-flag "^4.0.0"
     supports-color "^7.0.0"
 
-symbol-observable@^1.1.0:
+symbol-observable@^1.1.0, symbol-observable@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -6164,11 +6110,6 @@ symbol-tree@^3.2.2:
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
   integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
 
-tabbable@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
-  integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
-
 table@^5.2.3:
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@@ -6220,11 +6161,6 @@ through@^2.3.6:
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
-tiny-emitter@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
-  integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
-
 tiny-invariant@^1.0.2:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
@@ -6376,11 +6312,6 @@ ts-transform-inferno@^4.0.3:
   resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.3.tgz#2cc0eb125abdaff24b8298106a618ab7c6319edc"
   integrity sha512-Pcg0PVQwJ7Fpv4+3R9obFNsrNKQyLbmUqsjeG7T7r4/4UTgIl0MSwurexjtuGpCp2iv2X/i9ffKPAfAOyYJ9og==
 
-tslib@^1.10.0:
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
-  integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
-
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@@ -6405,21 +6336,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-twemoji-parser@12.1.3:
-  version "12.1.3"
-  resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-12.1.3.tgz#916c0153e77bd5f1011e7a99cbeacf52e43c9371"
-  integrity sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q==
-
-twemoji@^12.1.2, twemoji@^12.1.5:
-  version "12.1.5"
-  resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-12.1.5.tgz#a961fb65a1afcb1f729ad7e59391f9fe969820b9"
-  integrity sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==
-  dependencies:
-    fs-extra "^8.0.1"
-    jsonfile "^5.0.0"
-    twemoji-parser "12.1.3"
-    universalify "^0.1.2"
-
 type-check@~0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -6505,7 +6421,7 @@ union-value@^1.0.0:
     is-extendable "^0.1.1"
     set-value "^2.0.1"
 
-universalify@^0.1.0, universalify@^0.1.2:
+universalify@^0.1.0:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
@@ -6761,11 +6677,6 @@ xregexp@^4.3.0:
   dependencies:
     "@babel/runtime-corejs3" "^7.8.3"
 
-xtend@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
-  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
 y18n@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"