From 1ee1f29cdfcc372d43bad6595d86fbc8ed5c7363 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 9 Apr 2025 22:00:45 -0400 Subject: [PATCH] add: following timeline --- Cargo.lock | 8 ++-- crates/app/Cargo.toml | 2 +- crates/app/src/assets.rs | 2 + crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/html/macros.html | 8 ++++ .../src/public/html/timelines/following.html | 16 +++++++ .../app/src/public/html/timelines/home.html | 4 +- .../src/public/html/timelines/popular.html | 2 + crates/app/src/routes/pages/misc.rs | 40 ++++++++++++++++ crates/app/src/routes/pages/mod.rs | 1 + crates/core/Cargo.toml | 2 +- crates/core/src/database/posts.rs | 47 ++++++++++++++++++ crates/core/src/database/userfollows.rs | 48 +++++++++++++++++++ crates/l10n/Cargo.toml | 2 +- crates/shared/Cargo.toml | 2 +- 15 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 crates/app/src/public/html/timelines/following.html diff --git a/Cargo.lock b/Cargo.lock index f9ad075..eacc870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3155,7 +3155,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "1.0.0" +version = "1.0.1" dependencies = [ "axum", "axum-extra", @@ -3179,7 +3179,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "1.0.0" +version = "1.0.1" dependencies = [ "async-recursion", "bb8-postgres", @@ -3198,7 +3198,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "1.0.0" +version = "1.0.1" dependencies = [ "pathbufd", "serde", @@ -3207,7 +3207,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "1.0.0" +version = "1.0.1" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index e3adc21..153f670 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "1.0.0" +version = "1.0.1" edition = "2024" [features] diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 866c8b5..43c8fd1 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -54,6 +54,7 @@ pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/s pub const COMMUNITIES_MEMBERS: &str = include_str!("./public/html/communities/members.html"); pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.html"); +pub const TIMELINES_FOLLOWING: &str = include_str!("./public/html/timelines/following.html"); pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.html"); pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.html"); @@ -181,6 +182,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"communities/members.html"(crate::assets::COMMUNITIES_MEMBERS) --config=config); write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config); + write_template!(html_path->"timelines/following.html"(crate::assets::TIMELINES_FOLLOWING) --config=config); write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config); write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 1ef63e2..a7115ac 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -3,6 +3,7 @@ version = "1.0.0" [data] "general:link.home" = "Home" +"general:link.following" = "Following" "general:link.popular" = "Popular" "general:link.communities" = "Communities" "general:link.next" = "Next" diff --git a/crates/app/src/public/html/macros.html b/crates/app/src/public/html/macros.html index 64ab4cd..707f888 100644 --- a/crates/app/src/public/html/macros.html +++ b/crates/app/src/public/html/macros.html @@ -163,6 +163,14 @@ {{ text "general:link.home" }} + + {{ icon "earth" }} + {{ text "general:link.following" }} + + {{ icon "trending-up" }} {{ text "general:link.popular" }} diff --git a/crates/app/src/public/html/timelines/following.html b/crates/app/src/public/html/timelines/following.html new file mode 100644 index 0000000..ed810e3 --- /dev/null +++ b/crates/app/src/public/html/timelines/following.html @@ -0,0 +1,16 @@ +{% extends "root.html" %} {% block head %} +Following - {{ config.name }} +{% endblock %} {% block body %} {{ macros::nav() }} +
+ {{ macros::timelines_nav(selected="following") }} + + +
+ {% for post in list %} + {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} + {% endfor %} + + {{ components::pagination(page=page, items=list|length) }} +
+
+{% endblock %} diff --git a/crates/app/src/public/html/timelines/home.html b/crates/app/src/public/html/timelines/home.html index b1f76ab..cb3d8ed 100644 --- a/crates/app/src/public/html/timelines/home.html +++ b/crates/app/src/public/html/timelines/home.html @@ -5,7 +5,7 @@ {{ macros::timelines_nav(selected="home") }} - {% if list|length == 0 %} + {% if list|length == 0 and page == 0 %}
✨ Welcome to {{ config.name }}! @@ -21,6 +21,8 @@ {% for post in list %} {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% endfor %} + + {{ components::pagination(page=page, items=list|length) }}
{% endif %} diff --git a/crates/app/src/public/html/timelines/popular.html b/crates/app/src/public/html/timelines/popular.html index a163421..d6f4405 100644 --- a/crates/app/src/public/html/timelines/popular.html +++ b/crates/app/src/public/html/timelines/popular.html @@ -9,6 +9,8 @@ {% for post in list %} {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% endfor %} + + {{ components::pagination(page=page, items=list|length) }}
{% endblock %} diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 7105d66..f49e72a 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -58,9 +58,48 @@ pub async fn index_request( let mut context = initial_context(&data.0.0, lang, &Some(user)).await; context.insert("list", &list); + context.insert("page", &req.page); Html(data.1.render("timelines/home.html", &context).unwrap()) } +/// `/following` +pub async fn following_request( + jar: CookieJar, + Extension(data): Extension, + Query(req): Query, +) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + let list = match data + .0 + .get_posts_from_user_following(user.id, 12, req.page) + .await + { + Ok(l) => match data.0.fill_posts_with_community(l).await { + Ok(l) => l, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }; + + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &Some(user)).await; + + context.insert("list", &list); + context.insert("page", &req.page); + Ok(Html( + data.1.render("timelines/following.html", &context).unwrap(), + )) +} + /// `/popular` pub async fn popular_request( jar: CookieJar, @@ -82,6 +121,7 @@ pub async fn popular_request( let mut context = initial_context(&data.0.0, lang, &user).await; context.insert("list", &list); + context.insert("page", &req.page); Html(data.1.render("timelines/popular.html", &context).unwrap()) } diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 268092e..52eb6a1 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -18,6 +18,7 @@ pub fn routes() -> Router { Router::new() // misc .route("/", get(misc::index_request)) + .route("/following", get(misc::following_request)) .route("/popular", get(misc::popular_request)) .route("/notifs", get(misc::notifications_request)) .route("/doc/{*file_name}", get(misc::markdown_document_request)) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 3fb3bf5..cbc5c50 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "1.0.0" +version = "1.0.1" edition = "2024" [features] diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 17e97d8..fabe0ef 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -307,6 +307,53 @@ impl DataManager { Ok(res.unwrap()) } + /// Get posts from all users the given user is following. + /// + /// # Arguments + /// * `id` - the ID of the user + /// * `batch` - the limit of posts in each page + /// * `page` - the page number + pub async fn get_posts_from_user_following( + &self, + id: usize, + batch: usize, + page: usize, + ) -> Result> { + let following = self.get_userfollows_by_initiator_all(id).await?; + let mut following = following.iter(); + let first = match following.next() { + Some(f) => f, + None => return Ok(Vec::new()), + }; + + let mut query_string: String = String::new(); + + for user in following { + query_string.push_str(&format!(" OR owner = {}", user.receiver)); + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + &format!( + "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", + first.receiver + ), + &[&(batch as i64), &((page * batch) as i64)], + |x| { Self::get_post_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("post".to_string())); + } + + Ok(res.unwrap()) + } + /// Check if the given `uid` can post in the given `community`. pub async fn check_can_post(&self, community: &Community, uid: usize) -> bool { match community.write_access { diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs index 3465d1a..085be15 100644 --- a/crates/core/src/database/userfollows.rs +++ b/crates/core/src/database/userfollows.rs @@ -106,6 +106,30 @@ impl DataManager { Ok(res.unwrap()) } + /// Get users the given user is following. + /// + /// # Arguments + /// * `id` - the ID of the user + pub async fn get_userfollows_by_initiator_all(&self, id: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM userfollows WHERE initiator = $1", + &[&(id as i64)], + |x| { Self::get_userfollow_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("user follow".to_string())); + } + + Ok(res.unwrap()) + } + /// Get users following the given user. /// /// # Arguments @@ -137,6 +161,30 @@ impl DataManager { Ok(res.unwrap()) } + /// Get users following the given user. + /// + /// # Arguments + /// * `id` - the ID of the user + pub async fn get_userfollows_by_receiver_all(&self, id: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM userfollows WHERE receiver = $1", + &[&(id as i64)], + |x| { Self::get_userfollow_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("user follow".to_string())); + } + + Ok(res.unwrap()) + } + /// Complete a vector of just userfollows with their receiver as well. pub async fn fill_userfollows_with_receiver( &self, diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 784b9ab..6c64919 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "1.0.0" +version = "1.0.1" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 0813b84..450ac12 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "1.0.0" +version = "1.0.1" edition = "2024" authors.workspace = true repository.workspace = true