add: forum posts timeline

This commit is contained in:
trisua 2025-08-04 14:24:25 -04:00
parent 8c779b2f2e
commit d4ff681310
10 changed files with 159 additions and 17 deletions

View file

@ -103,6 +103,8 @@ pub const TIMELINES_ALL_QUESTIONS: &str =
include_str!("./public/html/timelines/all_questions.lisp"); include_str!("./public/html/timelines/all_questions.lisp");
pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.lisp"); pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.lisp");
pub const TIMELINES_SWISS_ARMY: &str = include_str!("./public/html/timelines/swiss_army.lisp"); pub const TIMELINES_SWISS_ARMY: &str = include_str!("./public/html/timelines/swiss_army.lisp");
pub const TIMELINES_ALL_FORUM_POSTS: &str =
include_str!("./public/html/timelines/all_forum_posts.lisp");
pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.lisp"); pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.lisp");
pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.lisp"); pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.lisp");
@ -336,6 +338,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"timelines/all_questions.html"(crate::assets::TIMELINES_ALL_QUESTIONS) --config=config --lisp plugins); write_template!(html_path->"timelines/all_questions.html"(crate::assets::TIMELINES_ALL_QUESTIONS) --config=config --lisp plugins);
write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config --lisp plugins); write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config --lisp plugins);
write_template!(html_path->"timelines/swiss_army.html"(crate::assets::TIMELINES_SWISS_ARMY) --config=config --lisp plugins); write_template!(html_path->"timelines/swiss_army.html"(crate::assets::TIMELINES_SWISS_ARMY) --config=config --lisp plugins);
write_template!(html_path->"timelines/all_forum_posts.html"(crate::assets::TIMELINES_ALL_FORUM_POSTS) --config=config --lisp plugins);
write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config --lisp plugins); write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config --lisp plugins);
write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config --lisp plugins); write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config --lisp plugins);

View file

@ -116,6 +116,7 @@ version = "1.0.0"
"communities:label.content" = "Content" "communities:label.content" = "Content"
"communities:label.title" = "Title" "communities:label.title" = "Title"
"communities:label.posts" = "Posts" "communities:label.posts" = "Posts"
"communities:label.forum_posts" = "Forum posts"
"communities:label.topics" = "Topics" "communities:label.topics" = "Topics"
"communities:label.questions" = "Questions" "communities:label.questions" = "Questions"
"communities:label.not_allowed_to_read" = "You're not allowed to view this community's posts" "communities:label.not_allowed_to_read" = "You're not allowed to view this community's posts"

View file

@ -2636,8 +2636,18 @@
(text "{%- endif %}") (text "{%- endif %}")
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro topic_post_display(post, owner, is_pinned=false) -%}") (text "{% macro topic_post_display(post, owner, is_pinned=false, community=false) -%}")
(tr (tr
(text "{% if community %}")
(td
(a
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
("class" "flex gap_1 items_center")
(text "{{ self::community_avatar(id=post.community, community=community) }}")
(span
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))))
(text "{%- endif %}")
(td (td
("class" "flex gap_1") ("class" "flex gap_1")
(a (a

View file

@ -165,7 +165,7 @@
(text "{%- endif %}"))) (text "{%- endif %}")))
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro timelines_nav(selected=\"\", posts=\"\", questions=\"\", secondary_selected=\"posts\") -%}") (text "{% macro timelines_nav(selected=\"\", posts=\"\", questions=\"\", secondary_selected=\"posts\", forum_posts=\"\") -%}")
(div (div
("class" "mobile_nav mobile") ("class" "mobile_nav mobile")
; primary nav ; primary nav
@ -184,7 +184,7 @@
(text "{% if posts and questions -%}") (text "{% if posts and questions -%}")
; secondary nav ; secondary nav
(text "{{ macros::timelines_secondary_nav(posts=posts, questions=questions, selected=secondary_selected) }}") (text "{{ macros::timelines_secondary_nav(posts=posts, questions=questions, selected=secondary_selected, forum_posts=forum_posts) }}")
(text "{%- endif %}")) (text "{%- endif %}"))
(div (div
@ -194,7 +194,7 @@
; secondary nav desktop only ; secondary nav desktop only
(text "{% if posts and questions -%}") (text "{% if posts and questions -%}")
(text "{{ macros::timelines_secondary_nav(posts=posts, questions=questions, selected=secondary_selected) }}") (text "{{ macros::timelines_secondary_nav(posts=posts, questions=questions, selected=secondary_selected, forum_posts=forum_posts) }}")
(text "{%- endif %}")) (text "{%- endif %}"))
(text "{%- endmacro %}") (text "{%- endmacro %}")
@ -252,7 +252,7 @@
(text "{%- endif %}"))) (text "{%- endif %}")))
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro timelines_secondary_nav(posts=\"\", questions=\"\", selected=\"posts\") -%} {% if user -%}") (text "{% macro timelines_secondary_nav(posts=\"\", questions=\"\", selected=\"posts\", forum_posts=\"\") -%} {% if user -%}")
(div (div
("class" "pillmenu w_full") ("class" "pillmenu w_full")
(a (a
@ -261,6 +261,14 @@
(icon (text "newspaper")) (icon (text "newspaper"))
(span (str (text "communities:label.posts")))) (span (str (text "communities:label.posts"))))
(text "{% if forum_posts|length > 0 -%}")
(a
("href" "{{ forum_posts }}")
("class" "{% if selected == 'forum_posts' -%}active{%- endif %}")
(icon (text "list-tree"))
(span (str (text "communities:label.forum_posts"))))
(text "{%- endif %}")
(a (a
("href" "{{ questions }}") ("href" "{{ questions }}")
("class" "{% if selected == 'questions' -%}active{%- endif %}") ("class" "{% if selected == 'questions' -%}active{%- endif %}")

View file

@ -1,11 +1,10 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Latest posts - {{ config.name }}")) (text "Latest posts - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")
(text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\") }} {% if not user -%}") (text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", forum_posts=\"/all/forum_posts\") }} {% if not user -%}")
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div
@ -32,11 +31,9 @@
("class" "card w_full flex flex_col gap_2") ("class" "card w_full flex flex_col gap_2")
("ui_ident" "io_data_load") ("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker")))) (div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}") (text "{% set paged = user and user.settings.paged_timelines %}")
(script (script
(text "setTimeout(() => { (text "setTimeout(() => {
trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=AllPosts&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=AllPosts&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
});")) });"))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -0,0 +1,24 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Latest forum posts - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex_col gap_2")
(text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", secondary_selected=\"forum_posts\", forum_posts=\"/all/forum_posts\") }}")
(div
("class" "card w_full flex flex_col gap_2")
(div
("class" "w_full")
("style" "overflow: auto")
(table
("class" "w_full")
(thead
(th (text "In"))
(th (text "Title"))
(th (text "Replies"))
(th (text "Score"))
(th (text "Created")))
(tbody
(text "{% for post in feed %} {{ components::topic_post_display(post=post[0], owner=post[1], community=post[2]) }} {% endfor %}"))))
(text "{{ components::pagination(page=page, items=feed|length) }}")))
(text "{% endblock %}")

View file

@ -1,13 +1,11 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Latest questions - {{ config.name }}")) (text "Latest questions - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")
(text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", secondary_selected=\"questions\") }}") (text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", secondary_selected=\"questions\", forum_posts=\"/all/forum_posts\") }}")
(div (div
("class" "card w_full flex flex_col gap_2") ("class" "card w_full flex flex_col gap_2")
(text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) (text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -47,8 +47,8 @@ pub async fn index_request(
// i'm only changing this for stripe // i'm only changing this for stripe
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &None).await; let mut context = initial_context(&data.0.0.0, lang, &None).await;
context.insert("page", &req.page); context.insert("page", &req.page);
Html(data.1.render("timelines/all.html", &context).unwrap()) Html(data.1.render("timelines/all.html", &context).unwrap())
}; };
} }
@ -79,6 +79,7 @@ pub async fn index_request(
context.insert("list", &list); context.insert("list", &list);
context.insert("page", &req.page); context.insert("page", &req.page);
Html(data.1.render("timelines/home.html", &context).unwrap()) Html(data.1.render("timelines/home.html", &context).unwrap())
} }
@ -93,8 +94,8 @@ pub async fn popular_request(
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await; let mut context = initial_context(&data.0.0.0, lang, &user).await;
context.insert("page", &req.page); context.insert("page", &req.page);
Html(data.1.render("timelines/popular.html", &context).unwrap()) Html(data.1.render("timelines/popular.html", &context).unwrap())
} }
@ -116,8 +117,8 @@ pub async fn following_request(
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("page", &req.page); context.insert("page", &req.page);
Ok(Html( Ok(Html(
data.1.render("timelines/following.html", &context).unwrap(), data.1.render("timelines/following.html", &context).unwrap(),
)) ))
@ -134,8 +135,8 @@ pub async fn all_request(
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await; let mut context = initial_context(&data.0.0.0, lang, &user).await;
context.insert("page", &req.page); context.insert("page", &req.page);
Html(data.1.render("timelines/all.html", &context).unwrap()) Html(data.1.render("timelines/all.html", &context).unwrap())
} }
@ -172,6 +173,7 @@ pub async fn index_questions_request(
context.insert("list", &list); context.insert("list", &list);
context.insert("page", &req.page); context.insert("page", &req.page);
Html( Html(
data.1 data.1
.render("timelines/home_questions.html", &context) .render("timelines/home_questions.html", &context)
@ -212,6 +214,7 @@ pub async fn popular_questions_request(
context.insert("list", &list); context.insert("list", &list);
context.insert("page", &req.page); context.insert("page", &req.page);
Html( Html(
data.1 data.1
.render("timelines/popular_questions.html", &context) .render("timelines/popular_questions.html", &context)
@ -254,6 +257,7 @@ pub async fn following_questions_request(
context.insert("list", &list); context.insert("list", &list);
context.insert("page", &req.page); context.insert("page", &req.page);
Ok(Html( Ok(Html(
data.1 data.1
.render("timelines/following_questions.html", &context) .render("timelines/following_questions.html", &context)
@ -271,7 +275,6 @@ pub async fn all_questions_request(
let user = get_user_from_token!(jar, data.0); let user = get_user_from_token!(jar, data.0);
let ignore_users = crate::ignore_users_gen!(user, data); let ignore_users = crate::ignore_users_gen!(user, data);
let list = match data.0.get_latest_global_questions(12, req.page).await { let list = match data.0.get_latest_global_questions(12, req.page).await {
Ok(l) => match data.0.fill_questions(l, &ignore_users).await { Ok(l) => match data.0.fill_questions(l, &ignore_users).await {
Ok(l) => l, Ok(l) => l,
@ -285,6 +288,7 @@ pub async fn all_questions_request(
context.insert("list", &list); context.insert("list", &list);
context.insert("page", &req.page); context.insert("page", &req.page);
Html( Html(
data.1 data.1
.render("timelines/all_questions.html", &context) .render("timelines/all_questions.html", &context)
@ -292,6 +296,50 @@ pub async fn all_questions_request(
) )
} }
/// `/all/forum_posts`
pub async fn all_forum_posts_request(
jar: CookieJar,
Extension(data): Extension<State>,
Query(req): Query<PaginatedQuery>,
) -> impl IntoResponse {
let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => {
return Html(render_error(Error::NotAllowed, &jar, &data, &None).await);
}
};
let ignore_users = crate::ignore_users_gen!(user!, data);
let list = match data
.0
.get_latest_forum_posts(48, req.page, &Some(user.clone()), req.before)
.await
{
Ok(l) => match data
.0
.fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone()))
.await
{
Ok(l) => l,
Err(e) => return Html(render_error(e, &jar, &data, &Some(user)).await),
},
Err(e) => return Html(render_error(e, &jar, &data, &Some(user)).await),
};
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("page", &req.page);
context.insert("feed", &list);
Html(
data.1
.render("timelines/all_forum_posts.html", &context)
.unwrap(),
)
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NotificationsProps { pub struct NotificationsProps {
#[serde(default)] #[serde(default)]

View file

@ -41,6 +41,8 @@ pub fn routes() -> Router {
get(misc::following_questions_request), get(misc::following_questions_request),
) )
.route("/all/questions", get(misc::all_questions_request)) .route("/all/questions", get(misc::all_questions_request))
// forum post timelines
.route("/all/forum_posts", get(misc::all_forum_posts_request))
// misc // misc
.route("/notifs", get(misc::notifications_request)) .route("/notifs", get(misc::notifications_request))
.route("/requests", get(misc::requests_request)) .route("/requests", get(misc::requests_request))

View file

@ -1606,6 +1606,57 @@ impl DataManager {
Ok(res.unwrap()) Ok(res.unwrap())
} }
/// Get forum posts from all communities, sorted by creation.
///
/// # Arguments
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_latest_forum_posts(
&self,
batch: usize,
page: usize,
as_user: &Option<User>,
before_time: usize,
) -> Result<Vec<Post>> {
// check if we should hide nsfw posts
let mut hide_nsfw: bool = true;
if let Some(ua) = as_user {
hide_nsfw = !ua.settings.show_nsfw;
}
// ...
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
&format!(
"SELECT * FROM posts WHERE replying_to = 0{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
if before_time > 0 {
format!(" AND created < {before_time}")
} else {
String::new()
},
if hide_nsfw {
" AND NOT context LIKE '%\"is_nsfw\":true%'"
} else {
""
}
),
&[&(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())
}
/// Get posts from all communities the given user is in. /// Get posts from all communities the given user is in.
/// ///
/// # Arguments /// # Arguments