//! Page: home. Landing page for visitors; repo dashboard for signed-in users. //! //! The dashboard is a greeting, the contribution calendar, then two columns: the user's //! repositories and the unread inbox rows that need them. use std::time::Duration; use forge_core::{Notification, Repo, Visibility}; use time::OffsetDateTime; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::page; use topcoat::view::{View, component, view}; use crate::auth::{current_user, percent_encode}; use crate::i18n::misc::{self as m, K as MK}; use crate::i18n::orgs::{self as g, K}; use crate::i18n::{self, Lang}; use crate::pages::issue_parts::stamp_unix; use crate::pages::repo_settings::{notice_banner_for, take_notice}; use crate::state::AppState; use crate::views::heatmap::{self, Calendar, contribution_calendar}; use crate::views::{display_name, now_unix, relative_time, shell}; /// How far back the calendar reads contributions: 53 weeks plus the days of the current week. const CALENDAR_DAYS: u64 = 371; /// Unread inbox rows in the "Needs your attention" column. const ATTENTION_ROWS: u32 = 5; /// What the signed-in dashboard shows, loaded once per page view. struct Dashboard { name: String, hour: u8, repos: Vec, calendar: Calendar, attention: Vec, } /// One unread inbox row, with its strings ready for the view. struct AttentionRow { href: String, reference: String, title: String, kind: &'static str, relative: String, } #[page("/")] async fn home(cx: &Cx) -> Result { let state = app_context::(cx); let viewer = current_user(cx).await?; let lang = i18n::lang(cx, viewer.as_ref().map(|(u, _)| u)); let dash = match &viewer { Some((user, _)) => Some(load_dashboard(state, user, lang).await?), None => None, }; let has_users = viewer.is_none() && state.store.user_count().await? > 0; let title = if viewer.is_some() { g::t(lang, K::Dashboard) } else { g::t(lang, K::Home) }; let notice = take_notice(cx); Ok(view! { shell(title: title, viewer: viewer.as_ref(), notice_banner_for(lang: lang, title: notice) if let Some(dash) = dash { dashboard(dash: dash, lang: lang) } else { landing(has_users: has_users, lang: lang) }) }) } /// Reads the dashboard data for a signed-in user. async fn load_dashboard( state: &AppState, user: &forge_core::User, lang: Lang, ) -> Result { let repos = state.store.list_repos_by_owner(&user.id).await?; let now = OffsetDateTime::now_utc(); let today = ( i64::from(now.year()), i64::from(u8::from(now.month())), i64::from(now.day()), ); let since = forge_core::time::stamp_ago(Duration::from_secs(CALENDAR_DAYS * 86_400)); let counts = state.store.contribution_counts(&user.id, &since).await?; let calendar = heatmap::build(&counts, today); let now_secs = now_unix(); let attention = state .store .list_notifications(&user.id, true, None, ATTENTION_ROWS) .await? .iter() .map(|note| attention_row(lang, note, now_secs)) .collect(); Ok(Dashboard { name: display_name(lang, &user.username), hour: now.hour(), repos, calendar, attention, }) } #[component] async fn landing(has_users: bool, lang: Lang) -> Result { Ok(view! {

"forge"

(g::t(lang, K::LandingTagline))

}) } #[component] async fn dashboard(dash: Dashboard, lang: Lang) -> Result { let empty = dash.repos.is_empty(); let count = m::repositories_count(lang, dash.repos.len() as u64); let greeting = m::greeting(lang, dash.hour, &dash.name); let attention_empty = dash.attention.is_empty(); Ok(view! {

(greeting)

contribution_calendar(calendar: dash.calendar, lang: lang)

(g::t(lang, K::YourRepositories))

(count) (g::t(lang, K::NewRepository))
if empty {

(g::t(lang, K::NoReposYet))

} else {
for repo in dash.repos.into_iter() { }
(g::t(lang, K::Name)) (g::t(lang, K::Description))
(format!("{}/{}", repo.owner_name, repo.name)) if repo.visibility == Visibility::Private { (i18n::visibility_label(lang, repo.visibility)) } (repo.description)
}

(m::t(lang, MK::NeedsAttention))

if attention_empty {

(m::t(lang, MK::NothingNeedsYou))

} else { }

(m::t(lang, MK::ViewAllNotifications))

}) } /// The row for an unread inbox entry on the dashboard. fn attention_row(lang: Lang, note: &Notification, now: i64) -> AttentionRow { let kind = match (note.is_pull, note.issue_state.as_str()) { (false, "open") => m::t(lang, MK::KindOpenIssue), (false, _) => m::t(lang, MK::KindClosedIssue), (true, "open") => m::t(lang, MK::KindOpenPull), (true, _) => m::t(lang, MK::KindClosedPull), }; AttentionRow { href: format!("/notifications/{}/open", percent_encode(¬e.id)), reference: format!("{}/{} #{}", note.repo_owner, note.repo_name, note.number), title: note.title.clone(), kind, relative: relative_time(lang, stamp_unix(¬e.updated_at), now), } } /// CSS class for a visibility badge. pub fn badge_class(visibility: Visibility) -> &'static str { match visibility { Visibility::Public => "badge public", Visibility::Private => "badge private", } }