ff2747eb7785ad4f822b379b83077153498504d9 / frogue / src / views / index.rs
Raw
use maud::{Markup, PreEscaped, Render, html};
use tracing::instrument;
use super::Page;
use super::components::Pagination;
use crate::activitypub::feed::FeedEntry;
use crate::registration::RegistrationMode;
pub const FEED_PAGE_SIZE: usize = 20;
/// The landing page for anonymous visitors: explains what Frogue is, shows
/// instance statistics, and indicates whether registration is open.
pub(super) struct Landing {
pub user_count: u64,
pub repository_count: u64,
pub registration_mode: RegistrationMode,
}
impl Render for Landing {
fn render(&self) -> Markup {
html! {
section class="landing-intro" {
h2 { "What is Frogue?" }
p {
"Frogue is a " em { "software forge" } ". It's a place to host and collaborate on software projects,"
"track issues, and review patch proposals. It speaks " a href="https://activitypub.rocks/" { "ActivityPub" }
" and " a href="https://forgefed.org/" { "ForgeFed" } ", so it can federate with other forges and ActivityPub clients."
}
}
section class="landing-stats" {
h2 { "This instance" }
ul {
li { strong { (self.user_count) } " users" }
li { strong { (self.repository_count) } " public repositories" }
}
}
section class="landing-registration" {
h2 { "Join" }
@match self.registration_mode {
RegistrationMode::Open => {
p {
"Registrations are open. "
a href="/register" { "Create an account" }
" or "
a href="/login" { "log in" }
"."
}
}
RegistrationMode::InviteOnly => {
p {
"Registrations are invite-only. Ask an existing user for an invite link, "
"then "
a href="/register" { "register" }
"."
}
}
}
}
}
}
}
/// The body of the index page: when a user is logged in and federation is
/// enabled, show their home feed. Otherwise show the landing page.
pub(super) struct Index {
pub entries: Vec<FeedEntry>,
pub public_url: String,
pub viewer_username: Option<String>,
pub page: u64,
pub landing: Landing,
}
impl Render for Index {
fn render(&self) -> Markup {
if self.viewer_username.is_none() {
// Anonymous visitor: show the landing page
self.landing.render()
} else if self.entries.is_empty() {
// Empty feed - show a message
html! {
p { "Your feed is empty. Follow some remote users or post a note!" }
}
} else {
// Logged in with feed entries
html! {
div class="feed" {
@for entry in &self.entries {
(render_feed_entry(entry, &self.public_url, &self.viewer_username))
}
}
(Pagination {
current_page: self.page,
has_next: self.entries.len() == FEED_PAGE_SIZE,
base_url: "/".to_string(),
})
}
}
}
}
/// Render a single feed entry.
fn render_feed_entry(
entry: &FeedEntry,
public_url: &str,
viewer_username: &Option<String>,
) -> Markup {
let author_handle = entry.author_handle();
let content = entry.content();
let permalink = match entry {
FeedEntry::Local {
uuid,
author_username,
..
} => format!("{public_url}/~{author_username}/notes/{uuid}"),
FeedEntry::Remote { object_url, .. } => object_url.clone(),
};
let timestamp = entry.timestamp();
let time_str = timestamp.format("%Y-%m-%d %H:%M UTC").to_string();
let (reacted, boosted, remote_note_id) = match entry {
FeedEntry::Local { reacted, .. } => (*reacted, false, None),
FeedEntry::Remote {
remote_note_id,
reacted,
boosted,
..
} => (*reacted, *boosted, Some(*remote_note_id)),
};
html! {
article class="feed-entry" {
header class="feed-entry-header" {
strong class="feed-entry-author" { (author_handle) }
time datetime=(timestamp.to_rfc3339()) { (time_str) }
}
div class="feed-entry-content" {
(PreEscaped(content))
}
footer class="feed-entry-footer" {
a href=(permalink) { "permalink" }
@if viewer_username.is_some() {
@if let Some(note_id) = remote_note_id {
@if reacted {
form method="post" action=(format!("/remote-notes/{}/unreact", note_id)) {
button type="submit" class="react-button reacted" { "❤️ Unreact" }
}
} @else {
form method="post" action=(format!("/remote-notes/{}/react", note_id)) {
button type="submit" class="react-button" { "❤️ React" }
}
}
@if boosted {
form method="post" action=(format!("/remote-notes/{}/unannounce", note_id)) {
button type="submit" class="react-button reacted" { "🔁 Unboost" }
}
} @else {
form method="post" action=(format!("/remote-notes/{}/announce", note_id)) {
button type="submit" class="react-button" { "🔁 Boost" }
}
}
}
}
}
}
}
}
/// The index page, as seen by `viewer` - nobody, when it is `None`.
/// When logged in and federation is enabled, shows the home feed.
/// When not logged in, shows the landing page with instance stats.
#[instrument(skip(entries))]
pub fn index(
viewer: Option<&str>,
entries: Vec<FeedEntry>,
public_url: &str,
page: u64,
user_count: u64,
repository_count: u64,
registration_mode: RegistrationMode,
) -> HttpResponse {
let viewer_username = viewer.map(str::to_string);
let title = if viewer.is_some() {
"Home - Frogue".to_string()
} else {
"Frogue".to_string()
};
Page {
title,
viewer: viewer_username.clone(),
extra_head: None,
body: Index {
entries,
public_url: public_url.to_string(),
viewer_username,
page,
landing: Landing {
user_count,
repository_count,
registration_mode,
},
},
}
.into_response()
}
use actix_web::HttpResponse;