forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
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}