i18n+filtering fork - fluent-templates v2
at main 203 lines 5.9 kB view raw
1use anyhow::Result; 2use axum::extract::Path; 3use axum::response::IntoResponse; 4use axum_extra::extract::Query; 5use axum_htmx::{HxBoosted, HxRequest}; 6use chrono_tz::Tz; 7use http::StatusCode; 8use minijinja::context as template_context; 9use serde::{Deserialize, Serialize}; 10use std::fmt; 11 12use crate::{ 13 contextual_error, create_renderer, 14 http::{ 15 context::UserRequestContext, 16 errors::{CommonError, WebError}, 17 event_view::EventView, 18 pagination::{Pagination, PaginationView}, 19 tab_selector::{TabLink, TabSelector}, 20 utils::build_url, 21 }, 22 storage::{ 23 errors::StorageError, 24 event::{event_list_did_recently_updated, model::EventWithRole}, 25 handle::{handle_for_did, handle_for_handle}, 26 }, 27}; 28 29use super::event_view::hydrate_event_organizers; 30 31#[derive(Debug, Deserialize, Serialize, PartialEq)] 32pub enum ProfileTab { 33 RecentlyUpdated, 34} 35 36impl fmt::Display for ProfileTab { 37 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 38 match self { 39 ProfileTab::RecentlyUpdated => write!(f, "recentlyupdated"), 40 } 41 } 42} 43 44impl From<TabSelector> for ProfileTab { 45 fn from(_: TabSelector) -> Self { 46 ProfileTab::RecentlyUpdated 47 } 48} 49 50pub async fn handle_profile_view( 51 ctx: UserRequestContext, 52 HxRequest(hx_request): HxRequest, 53 HxBoosted(hx_boosted): HxBoosted, 54 Path(handle_slug): Path<String>, 55 pagination: Query<Pagination>, 56 tab_selector: Query<TabSelector>, 57) -> Result<impl IntoResponse, WebError> { 58 let renderer = create_renderer!(ctx.web_context.clone(), ctx.language.clone(), hx_boosted, hx_request); 59 60 if !handle_slug.starts_with("did:web:") 61 && !handle_slug.starts_with("did:plc:") 62 && !handle_slug.starts_with('@') 63 { 64 return contextual_error!( 65 renderer: renderer, 66 CommonError::InvalidHandleSlug, 67 template_context!{} 68 ); 69 } 70 71 let profile = { 72 if let Some(handle_slug) = handle_slug.strip_prefix('@') { 73 handle_for_handle(&ctx.web_context.pool, handle_slug).await 74 } else if handle_slug.starts_with("did:") { 75 handle_for_did(&ctx.web_context.pool, &handle_slug).await 76 } else { 77 Err(StorageError::HandleNotFound) 78 } 79 }; 80 81 if let Err(err) = profile { 82 return contextual_error!( 83 renderer: renderer, 84 err, 85 template_context!{} 86 ); 87 } 88 89 let profile = profile.unwrap(); 90 91 let is_self = ctx 92 .current_handle 93 .clone() 94 .is_some_and(|inner_current_entity| inner_current_entity.did == profile.did); 95 96 let _default_context = template_context! { 97 current_handle => ctx.current_handle, 98 language => ctx.language.to_string(), 99 canonical_url => format!("https://{}/{}", ctx.web_context.config.external_base, profile.did), 100 profile, 101 is_self, 102 }; 103 104 let _ = { 105 if let Some(current_handle) = ctx.current_handle.clone() { 106 current_handle.tz.parse::<Tz>().unwrap_or(Tz::UTC) 107 } else { 108 profile.tz.parse::<Tz>().unwrap_or(Tz::UTC) 109 } 110 }; 111 112 let (page, page_size) = pagination.clamped(); 113 let tab: ProfileTab = tab_selector.0.into(); 114 let tab_name = tab.to_string(); 115 116 let events = { 117 let tab_events: Result<Vec<EventWithRole>> = match tab { 118 ProfileTab::RecentlyUpdated => event_list_did_recently_updated( 119 &ctx.web_context.pool, 120 &profile.did, 121 page, 122 page_size, 123 ) 124 .await 125 .map_err(|err| err.into()), 126 }; 127 match tab_events { 128 Ok(values) => values, 129 Err(err) => { 130 return contextual_error!( 131 renderer: renderer, 132 err, 133 template_context!{} 134 ); 135 } 136 } 137 }; 138 139 let organizer_handlers = hydrate_event_organizers(&ctx.web_context.pool, &events).await?; 140 141 let mut events = events 142 .iter() 143 .filter_map(|event_view| { 144 let organizer_maybe = organizer_handlers.get(&event_view.event.did); 145 EventView::try_from_with_locale( 146 (ctx.current_handle.as_ref(), 147 organizer_maybe, 148 &event_view.event), 149 Some(&ctx.language.0), 150 ) 151 .ok() 152 }) 153 .collect::<Vec<EventView>>(); 154 155 if let Err(err) = 156 super::event_view::hydrate_event_rsvp_counts(&ctx.web_context.pool, &mut events).await 157 { 158 tracing::warn!("Failed to hydrate event counts: {}", err); 159 } 160 161 let params: Vec<(&str, &str)> = vec![("tab", &tab_name)]; 162 163 let pagination_view = PaginationView::new(page_size, events.len() as i64, page, params); 164 165 if events.len() > page_size as usize { 166 events.truncate(page_size as usize); 167 } 168 169 let tab_links = vec![TabLink { 170 name: "recentlyupdated".to_string(), 171 label: "Recently Updated".to_string(), 172 url: build_url( 173 &ctx.web_context.config.external_base, 174 &format!("/{}", handle_slug), 175 vec![Some(("tab", "upcoming"))], 176 ), 177 active: tab == ProfileTab::RecentlyUpdated, 178 }]; 179 180 let canonical_url = format!( 181 "https://{}/{}", 182 ctx.web_context.config.external_base, 183 handle_slug 184 ); 185 186 Ok(( 187 StatusCode::OK, 188 renderer.render_template( 189 "profile", 190 template_context! { 191 profile, 192 is_self, 193 tab => tab.to_string(), 194 tabs => tab_links, 195 events, 196 pagination => pagination_view, 197 }, 198 ctx.current_handle.as_ref(), 199 &canonical_url, 200 ), 201 ) 202 .into_response()) 203}