tangled
alpha
login
or
join now
serendipty01.dev
/
smokesignal
forked from
smokesignal.events/smokesignal
0
fork
atom
The smokesignal.events web application
0
fork
atom
overview
issues
pulls
pipelines
refactor: performance and usability improvements
Nick Gerakines
4 months ago
abd721b6
a0b521ea
+149
-90
4 changed files
expand all
collapse all
unified
split
src
http
handle_view_event.rs
storage
event.rs
templates
en-us
view_event.common.html
view_event_attendees.common.html
+39
-57
src/http/handle_view_event.rs
···
22
22
};
23
23
use crate::storage::event::event_exists;
24
24
use crate::storage::event::event_get;
25
25
-
use crate::storage::event::get_event_rsvp_counts;
26
25
use crate::storage::event::get_event_rsvps_with_validation;
26
26
+
use crate::storage::event::get_going_rsvp_count;
27
27
+
use crate::storage::event::get_recent_going_rsvps;
27
28
use crate::storage::event::get_user_rsvp_status_and_validation;
28
29
use crate::storage::event::rsvp_get_by_event_and_did;
29
30
use crate::storage::identity_profile::handle_for_did;
···
54
55
NSID.to_string()
55
56
}
56
57
57
57
-
/// Helper function to fetch the organizer's handle (which contains their time zone)
58
58
-
/// This is used to implement the time zone selection logic.
59
59
-
async fn fetch_organizer_handle(pool: &StoragePool, did: &str) -> Option<IdentityProfile> {
60
60
-
match handle_for_did(pool, did).await {
61
61
-
Ok(handle) => Some(handle),
62
62
-
Err(err) => {
63
63
-
tracing::warn!("Failed to fetch organizer handle: {}", err);
64
64
-
None
65
65
-
}
66
66
-
}
67
67
-
}
68
68
-
69
58
/// Helper function to extract metadata from an acceptance record
70
59
fn extract_acceptance_metadata(acceptance_record: serde_json::Value) -> Option<Vec<(String, String)>> {
71
60
match serde_json::from_value::<crate::atproto::lexicon::acceptance::Acceptance>(
···
91
80
}
92
81
93
82
/// Helper function to fetch recent going RSVPs with handles
94
94
-
/// Returns up to 3 most recent RSVPs with status "going"
83
83
+
/// Returns up to 3 most recent RSVPs with status "going", ordered by updated_at DESC
95
84
async fn fetch_recent_going_rsvps(
96
85
pool: &StoragePool,
97
86
aturi: &str,
98
87
) -> Vec<RsvpDisplay> {
99
99
-
// Get recent going RSVPs (limited to 3 for display)
100
100
-
let rsvps = get_event_rsvps_with_validation(pool, aturi, Some("going"))
88
88
+
// Get recent going RSVPs (limited to 3, ordered by updated_at DESC)
89
89
+
let rsvps = get_recent_going_rsvps(pool, aturi, 3)
101
90
.await
102
91
.unwrap_or_default();
103
92
104
104
-
// Extract DIDs for batch lookup (take first 3)
105
105
-
let dids: Vec<String> = rsvps.iter().take(3).map(|(did, _, _)| did.clone()).collect();
93
93
+
// Extract DIDs for batch lookup
94
94
+
let dids: Vec<String> = rsvps.iter().map(|(did, _, _)| did.clone()).collect();
106
95
107
96
// Perform batch lookup for handles
108
108
-
let handle_profiles = handles_by_did(pool, dids.clone())
97
97
+
let handle_profiles = handles_by_did(pool, dids)
109
98
.await
110
99
.unwrap_or_default();
111
100
···
410
399
411
400
let event_result = match &event_get_result {
412
401
Ok(event) => {
413
413
-
let organizer_handle = {
414
414
-
if ctx
415
415
-
.current_handle
416
416
-
.clone()
417
417
-
.is_some_and(|h| h.did == event.did)
418
418
-
{
419
419
-
ctx.current_handle.clone()
420
420
-
} else {
421
421
-
fetch_organizer_handle(&ctx.web_context.pool, &event.did).await
422
422
-
}
402
402
+
// Use current user's handle if viewing their own event (for timezone),
403
403
+
// otherwise use the organizer's profile (which we already have)
404
404
+
let organizer_handle = if ctx
405
405
+
.current_handle
406
406
+
.as_ref()
407
407
+
.is_some_and(|h| h.did == event.did)
408
408
+
{
409
409
+
ctx.current_handle.as_ref()
410
410
+
} else {
411
411
+
Some(&profile)
423
412
};
424
413
425
414
let facet_limits = crate::facets::FacetLimits {
···
431
420
432
421
EventView::try_from((
433
422
ctx.current_handle.as_ref(),
434
434
-
organizer_handle.as_ref(),
423
423
+
organizer_handle,
435
424
event,
436
425
&facet_limits,
437
426
))
···
475
464
let (
476
465
user_rsvp_result,
477
466
user_email_result,
478
478
-
counts_map,
467
467
+
going_count_result,
479
468
recent_going_rsvps,
469
469
+
organizer_profile_record,
480
470
) = tokio::join!(
481
471
// Get user's RSVP status if logged in
482
472
async {
···
503
493
Ok(None)
504
494
}
505
495
},
506
506
-
// Get counts for all RSVP statuses in a single query
496
496
+
// Get count of "going" RSVPs
497
497
+
get_going_rsvp_count(&ctx.web_context.pool, &aturi),
498
498
+
// Get recent going RSVPs for display
499
499
+
fetch_recent_going_rsvps(&ctx.web_context.pool, &aturi),
500
500
+
// Fetch organizer profile record for avatar display
507
501
async {
508
508
-
get_event_rsvp_counts(&ctx.web_context.pool, vec![aturi.clone()])
502
502
+
profile_get_by_did(&ctx.web_context.pool, &profile.did)
509
503
.await
510
510
-
.unwrap_or_default()
504
504
+
.ok()
505
505
+
.flatten()
511
506
},
512
512
-
// Get recent going RSVPs for display
513
513
-
fetch_recent_going_rsvps(&ctx.web_context.pool, &aturi),
514
507
);
515
508
516
509
// Process results from parallel operations
···
532
525
}
533
526
};
534
527
535
535
-
let going_count = counts_map
536
536
-
.get(&(aturi.clone(), "going".to_string()))
537
537
-
.copied()
538
538
-
.unwrap_or(0) as u32;
539
539
-
let interested_count = counts_map
540
540
-
.get(&(aturi.clone(), "interested".to_string()))
541
541
-
.copied()
542
542
-
.unwrap_or(0) as u32;
543
543
-
let notgoing_count = counts_map
544
544
-
.get(&(aturi.clone(), "notgoing".to_string()))
545
545
-
.copied()
546
546
-
.unwrap_or(0) as u32;
528
528
+
let going_count = match going_count_result {
529
529
+
Ok(count) => count as u32,
530
530
+
Err(err) => {
531
531
+
tracing::error!("Error getting going RSVP count: {:?}", err);
532
532
+
0
533
533
+
}
534
534
+
};
547
535
548
536
// Fetch acceptance data (depends on user_rsvp_status, so must come after)
549
537
let (pending_acceptance_aturi, pending_acceptance_metadata, validated_acceptance_metadata) =
···
557
545
)
558
546
.await;
559
547
560
560
-
// Set counts on event
548
548
+
// Set counts on event (only going_count is used in view_event template)
561
549
let mut event_with_counts = event;
562
550
event_with_counts.count_going = going_count;
563
563
-
event_with_counts.count_interested = interested_count;
564
564
-
event_with_counts.count_notgoing = notgoing_count;
551
551
+
event_with_counts.count_interested = 0;
552
552
+
event_with_counts.count_notgoing = 0;
565
553
566
554
// Check if private content should be displayed
567
555
let private_content = get_private_content_if_authorized(
···
572
560
user_rsvp_is_validated,
573
561
)
574
562
.await;
575
575
-
576
576
-
// Fetch organizer profile record for avatar display
577
577
-
let organizer_profile_record = profile_get_by_did(&ctx.web_context.pool, &profile.did)
578
578
-
.await
579
579
-
.ok()
580
580
-
.flatten();
581
563
582
564
Ok((
583
565
StatusCode::OK,
+69
src/storage/event.rs
···
585
585
Ok(rsvps)
586
586
}
587
587
588
588
+
/// Get recent going RSVPs for an event, ordered by updated_at DESC
589
589
+
/// Returns up to `limit` most recent RSVPs with status "going"
590
590
+
pub async fn get_recent_going_rsvps(
591
591
+
pool: &StoragePool,
592
592
+
event_aturi: &str,
593
593
+
limit: i64,
594
594
+
) -> Result<Vec<(String, String, Option<chrono::DateTime<chrono::Utc>>)>, StorageError> {
595
595
+
// Validate event_aturi is not empty
596
596
+
if event_aturi.trim().is_empty() {
597
597
+
return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol(
598
598
+
"Event URI cannot be empty".into(),
599
599
+
)));
600
600
+
}
601
601
+
602
602
+
let mut tx = pool
603
603
+
.begin()
604
604
+
.await
605
605
+
.map_err(StorageError::CannotBeginDatabaseTransaction)?;
606
606
+
607
607
+
let rsvps = sqlx::query_as::<_, (String, String, Option<chrono::DateTime<chrono::Utc>>)>(
608
608
+
"SELECT did, status, validated_at FROM rsvps WHERE event_aturi = $1 AND status = $2 ORDER BY updated_at DESC LIMIT $3",
609
609
+
)
610
610
+
.bind(event_aturi)
611
611
+
.bind("going")
612
612
+
.bind(limit)
613
613
+
.fetch_all(tx.as_mut())
614
614
+
.await
615
615
+
.map_err(StorageError::UnableToExecuteQuery)?;
616
616
+
617
617
+
tx.commit()
618
618
+
.await
619
619
+
.map_err(StorageError::CannotCommitDatabaseTransaction)?;
620
620
+
621
621
+
Ok(rsvps)
622
622
+
}
623
623
+
624
624
+
/// Get the count of RSVPs with status "going" for an event
625
625
+
pub async fn get_going_rsvp_count(
626
626
+
pool: &StoragePool,
627
627
+
event_aturi: &str,
628
628
+
) -> Result<i64, StorageError> {
629
629
+
// Validate event_aturi is not empty
630
630
+
if event_aturi.trim().is_empty() {
631
631
+
return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol(
632
632
+
"Event URI cannot be empty".into(),
633
633
+
)));
634
634
+
}
635
635
+
636
636
+
let mut tx = pool
637
637
+
.begin()
638
638
+
.await
639
639
+
.map_err(StorageError::CannotBeginDatabaseTransaction)?;
640
640
+
641
641
+
let count = sqlx::query_scalar::<_, i64>(
642
642
+
"SELECT COUNT(*) FROM rsvps WHERE event_aturi = $1 AND status = $2",
643
643
+
)
644
644
+
.bind(event_aturi)
645
645
+
.bind("going")
646
646
+
.fetch_one(tx.as_mut())
647
647
+
.await
648
648
+
.map_err(StorageError::UnableToExecuteQuery)?;
649
649
+
650
650
+
tx.commit()
651
651
+
.await
652
652
+
.map_err(StorageError::CannotCommitDatabaseTransaction)?;
653
653
+
654
654
+
Ok(count)
655
655
+
}
656
656
+
588
657
pub async fn get_user_rsvp(
589
658
pool: &StoragePool,
590
659
event_aturi: &str,
+27
-15
templates/en-us/view_event.common.html
···
1
1
{% macro verified_check(attendee) -%}
2
2
-
{%- if attendee.status == "going" -%}<span class="icon has-text-success" title="Going"><i class="fas fa-check-circle"></i></span>
3
3
-
{%- elif attendee.status == "interested" -%}<span class="icon has-text-info" title="Interested"><i class="fas fa-star"></i></span>
4
4
-
{%- elif attendee.status == "notgoing" -%}<span class="icon has-text-danger" title="Not Going"><i class="fas fa-times-circle"></i></span>{%- endif -%}
5
5
-
{%- if attendee.verified -%}<span class="icon has-text-link" title="The event organizer has signed this RSVP."><i class="fa fa-check-circle"></i></span>{%- endif -%}
2
2
+
{%- if attendee.verified -%}
3
3
+
<span class="icon"><i class="fa fa-certificate"></i></span>
4
4
+
{%- else -%}
5
5
+
{%- if attendee.status == "going" -%}
6
6
+
<span class="icon"><i class="fas fa-circle-check"></i></span>
7
7
+
{%- elif attendee.status == "interested" -%}
8
8
+
<span class="icon"><i class="fas fa-circle-question"></i></span>
9
9
+
{%- elif attendee.status == "notgoing" -%}
10
10
+
<span class="icon"><i class="fas fa-circle-xmark"></i></span>
11
11
+
{%- endif -%}
12
12
+
{%- endif -%}
6
13
{%- endmacro %}
7
14
<style>
8
15
/* ============================================
···
314
321
<!-- Attendance Summary -->
315
322
<div class="box">
316
323
{% set going_count = event.count_going | default(0) %}
324
324
+
{% set rsvp_count = recent_going_rsvps|length %}
317
325
{% if going_count == 0 %}
318
318
-
<p class="has-text-grey">No one is going yet.</p>
319
319
-
{% elif recent_going_rsvps and recent_going_rsvps|length > 0 %}
326
326
+
<p class="has-text-grey">No one is going to the event yet. <a href="{{ base }}/{{ handle_slug }}/{{ event_rkey }}/attendees" class="has-text-weight-semibold">View all attendees</a></p>
327
327
+
{% elif rsvp_count > 0 %}
320
328
<p>
321
321
-
{% set rsvp_count = recent_going_rsvps|length %}
322
322
-
{% if rsvp_count == 1 %}
323
323
-
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a> is going.
324
324
-
{% elif rsvp_count == 2 %}
325
325
-
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a> and {{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a> are going.
326
326
-
{% elif rsvp_count == 3 %}
327
327
-
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a>, {{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a>, and {{ verified_check(recent_going_rsvps[2]) }}<a href="{{ base }}/@{{ recent_going_rsvps[2].handle }}">@{{ recent_going_rsvps[2].handle }}</a> are going.
329
329
+
{% if going_count == 1 %}
330
330
+
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a> is going to the event.
331
331
+
{% elif going_count == 2 %}
332
332
+
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a> and
333
333
+
{{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a> are going to the event.
334
334
+
{% elif going_count == 3 %}
335
335
+
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a>,
336
336
+
{{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a>, and
337
337
+
{{ verified_check(recent_going_rsvps[2]) }}<a href="{{ base }}/@{{ recent_going_rsvps[2].handle }}">@{{ recent_going_rsvps[2].handle }}</a> are going to the event.
328
338
{% else %}
329
339
{% set others_count = going_count - 3 %}
330
330
-
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a>, {{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a>, {{ verified_check(recent_going_rsvps[2]) }}<a href="{{ base }}/@{{ recent_going_rsvps[2].handle }}">@{{ recent_going_rsvps[2].handle }}</a> and {{ others_count }} {% if others_count == 1 %}other{% else %}others{% endif %} are going.
340
340
+
{{ verified_check(recent_going_rsvps[0]) }}<a href="{{ base }}/@{{ recent_going_rsvps[0].handle }}">@{{ recent_going_rsvps[0].handle }}</a>,
341
341
+
{{ verified_check(recent_going_rsvps[1]) }}<a href="{{ base }}/@{{ recent_going_rsvps[1].handle }}">@{{ recent_going_rsvps[1].handle }}</a>,
342
342
+
{{ verified_check(recent_going_rsvps[2]) }}<a href="{{ base }}/@{{ recent_going_rsvps[2].handle }}">@{{ recent_going_rsvps[2].handle }}</a>, and {{ others_count }} other{% if others_count > 1 %}s{% endif %} are going to the event.
331
343
{% endif %}
332
344
<a href="{{ base }}/{{ handle_slug }}/{{ event_rkey }}/attendees" class="has-text-weight-semibold">View all attendees</a>
333
345
</p>
334
346
{% else %}
335
335
-
<p><strong>{{ going_count }} {% if going_count == 1 %}person{% else %}people{% endif %}</strong> {% if going_count == 1 %}is{% else %}are{% endif %} going. <a href="{{ base }}/{{ handle_slug }}/{{ event_rkey }}/attendees" class="has-text-weight-semibold">View all attendees</a></p>
347
347
+
<p><strong>{{ going_count }} {% if going_count == 1 %}person{% else %}people{% endif %}</strong> {% if going_count == 1 %}is{% else %}are{% endif %} going to the event. <a href="{{ base }}/{{ handle_slug }}/{{ event_rkey }}/attendees" class="has-text-weight-semibold">View all attendees</a></p>
336
348
{% endif %}
337
349
</div>
338
350
+14
-18
templates/en-us/view_event_attendees.common.html
···
1
1
+
{% macro verified_check(attendee) -%}
2
2
+
{%- if attendee.verified -%}
3
3
+
<span class="icon"><i class="fa fa-certificate"></i></span>
4
4
+
{%- else -%}
5
5
+
{%- if attendee.status == "going" -%}
6
6
+
<span class="icon"><i class="fas fa-circle-check"></i></span>
7
7
+
{%- elif attendee.status == "interested" -%}
8
8
+
<span class="icon"><i class="fas fa-circle-question"></i></span>
9
9
+
{%- elif attendee.status == "notgoing" -%}
10
10
+
<span class="icon"><i class="fas fa-circle-xmark"></i></span>
11
11
+
{%- endif -%}
12
12
+
{%- endif -%}
13
13
+
{%- endmacro %}
1
14
<section class="section">
2
15
<div class="container">
3
16
<div class="content">
···
10
23
<div class="grid is-col-min-12 has-text-centered">
11
24
{% for attendee in attendees %}
12
25
<span class="cell">
13
13
-
{% if attendee.status == "going" %}
14
14
-
<span class="icon has-text-success" title="Going">
15
15
-
<i class="fas fa-check-circle"></i>
16
16
-
</span>
17
17
-
{% elif attendee.status == "interested" %}
18
18
-
<span class="icon has-text-info" title="Interested">
19
19
-
<i class="fas fa-star"></i>
20
20
-
</span>
21
21
-
{% elif attendee.status == "notgoing" %}
22
22
-
<span class="icon has-text-danger" title="Not Going">
23
23
-
<i class="fas fa-times-circle"></i>
24
24
-
</span>
25
25
-
{% endif %}
26
26
-
{% if attendee.verified %}
27
27
-
<span class="icon has-text-link" title="The event organizer has signed this RSVP.">
28
28
-
<i class="fa fa-check-circle"></i>
29
29
-
</span>
30
30
-
{% endif %}
26
26
+
{{ verified_check(attendee) }}
31
27
<a href="{ base }}/{{ attendee.did }}">
32
28
{% if attendee.handle %}
33
29
@{{ attendee.handle }}