Skip to content

Commit 18f6fe9

Browse files
Copilotgfauredev
andcommitted
fix: notifications, session notes display, and add resume session button
Agent-Logs-Url: https://github.com/gfauredev/LogOut/sessions/0da072c0-a66a-488a-9fae-ea480a0065c5 Co-authored-by: gfauredev <19304085+gfauredev@users.noreply.github.com>
1 parent cb26fad commit 18f6fe9

12 files changed

Lines changed: 222 additions & 130 deletions

File tree

assets/en.ftl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ start-new-workout = Start New Workout
1111
session-repeat-title = Start a new session based on this one
1212
session-repeat-weekday-title = Repeat same-weekday session
1313
session-delete-title = Delete session
14+
session-resume-last-title = Resume last session
1415
session-show-more = +{ $count } more
1516
hold-to-delete-hint = Hold for 3s to delete
1617
session-delete-confirm = Delete this session?
@@ -145,7 +146,7 @@ congratulations = 🎉 Great workout! Session complete!
145146
notif-permission-blocked = ⚠️ Notifications blocked
146147
notif-permission-enable = ⚠️ Tap here to enable notifications
147148
notif-duration-title = Duration reached
148-
notif-duration-body = Target exercise duration reached!
149+
notif-duration-body = All Time High duration reached!
149150
notif-rest-title = Rest over
150151
notif-rest-body = Time to start your next set!
151152

assets/es.ftl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ start-new-workout = Nuevo entrenamiento
1111
session-repeat-title = Iniciar nueva sesión basada en esta
1212
session-repeat-weekday-title = Repetir la sesión del mismo día de la semana
1313
session-delete-title = Eliminar sesión
14+
session-resume-last-title = Reanudar la última sesión
1415
session-show-more = +{ $count } más
1516
hold-to-delete-hint = Mantener 3s para eliminar
1617
session-delete-confirm = ¿Eliminar esta sesión?
@@ -144,7 +145,7 @@ congratulations = 🎉 ¡Buen entrenamiento! ¡Sesión completada!
144145
notif-permission-blocked = ⚠️ Notificaciones bloqueadas
145146
notif-permission-enable = ⚠️ Pulsa aquí para activar las notificaciones
146147
notif-duration-title = Duración alcanzada
147-
notif-duration-body = ¡Duración objetivo del ejercicio alcanzada!
148+
notif-duration-body = ¡Duración récord personal del ejercicio alcanzada!
148149
notif-rest-title = Descanso terminado
149150
notif-rest-body = ¡Es hora de tu próxima serie!
150151

assets/fr.ftl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ start-new-workout = Nouvelle séance
1111
session-repeat-title = Démarrer une nouvelle séance basée sur celle-ci
1212
session-repeat-weekday-title = Répéter la séance du même jour de la semaine
1313
session-delete-title = Supprimer la séance
14+
session-resume-last-title = Reprendre la dernière séance
1415
session-show-more = +{ $count } autres
1516
hold-to-delete-hint = Maintenir 3s pour supprimer
1617
session-delete-confirm = Supprimer cette séance ?
@@ -148,7 +149,7 @@ congratulations = 🎉 Beau travail ! Séance terminée !
148149
notif-permission-blocked = ⚠️ Notifications bloquées
149150
notif-permission-enable = ⚠️ Appuie ici pour activer les notifications
150151
notif-duration-title = Durée atteinte
151-
notif-duration-body = Durée cible de l'exercice atteinte !
152+
notif-duration-body = Durée record personnel de l'exercice atteinte !
152153
notif-rest-title = Repos terminé
153154
notif-rest-body = C'est l'heure de ta prochaine série !
154155

src/components/active_session/completed_exercises.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub fn CompletedExercisesSection(
6363
}
6464
{
6565
rsx! {
66-
for (idx , log) in session.read().exercise_logs.iter().enumerate().rev() {
66+
for (idx, log) in session.read().exercise_logs.iter().enumerate().rev() {
6767
CompletedExerciseLog {
6868
key: "{idx}",
6969
idx,

src/components/active_session/mod.rs

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use dioxus::prelude::*;
1212
use dioxus_i18n::prelude::i18n;
1313
use dioxus_i18n::t;
1414
use futures_channel::mpsc::UnboundedReceiver;
15+
#[cfg(target_arch = "wasm32")]
16+
use gloo_timers::future::TimeoutFuture;
1517
use std::sync::Arc;
1618

1719
mod completed_exercises;
@@ -129,21 +131,6 @@ pub fn SessionView() -> Element {
129131
let pending_ids = use_memo(move || session.read().pending_exercise_ids.clone());
130132
let lang_str = use_memo(move || i18n().language().to_string());
131133
let mut notes_input = use_signal(|| session.read().notes.clone());
132-
// Initialise the uncontrolled textarea on first mount. Because we never
133-
// bind `value:` to the textarea, the DOM starts blank even when the
134-
// session already has notes (e.g. after reopening the app). This hook
135-
// fires once and sets the initial DOM value via JavaScript.
136-
let initial_notes = session.peek().notes.clone();
137-
use_hook(move || {
138-
if !initial_notes.is_empty() {
139-
let val_js = serde_json::to_string(&initial_notes).unwrap_or_default();
140-
spawn(async move {
141-
document::eval(&format!(
142-
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
143-
));
144-
});
145-
}
146-
});
147134
// Track the session ID so we can distinguish between:
148135
// (a) the debounce saving the user's own input for the *same* session
149136
// → do NOT touch the DOM (would reset cursor on Android)
@@ -384,7 +371,7 @@ pub fn SessionView() -> Element {
384371
}
385372
if !active_filters.read().is_empty() {
386373
div { class: "filter-chips",
387-
for (i , filter) in active_filters.read().iter().enumerate() {
374+
for (i, filter) in active_filters.read().iter().enumerate() {
388375
button {
389376
class: "filter-chip active",
390377
title: t!("session-filter-remove"),
@@ -459,9 +446,20 @@ pub fn SessionView() -> Element {
459446
// Dioxus to overwrite the DOM value on every re-render, which
460447
// resets the cursor position to the end on Android's WebView
461448
// (especially visible when typing fast with the IME).
462-
// Instead we drive the initial / external value via eval in the
463-
// `use_effect` above, and keep `notes_input` in sync via
464-
// `oninput` so the effect can detect external changes.
449+
// Instead we set the initial DOM value via `onmounted` (fires
450+
// once the element is in the DOM) and keep `notes_input` in
451+
// sync via `oninput` so the effect can detect session changes.
452+
onmounted: move |_| {
453+
let notes = notes_input.peek().clone();
454+
if !notes.is_empty() {
455+
let val_js = serde_json::to_string(&notes).unwrap_or_default();
456+
document::eval(
457+
&format!(
458+
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};",
459+
),
460+
);
461+
}
462+
},
465463
oninput: move |evt| {
466464
let text = evt.value();
467465
notes_input.set(text.clone());
@@ -480,6 +478,98 @@ pub fn GlobalSessionHeader() -> Element {
480478
let rest_duration = use_context::<RestDurationSignal>().0;
481479
let mut rest_input_value = use_signal(|| DEFAULT_REST_SECONDS.to_string());
482480
let mut congratulations = use_context::<crate::CongratulationsSignal>().0;
481+
482+
// A memo that captures the (rest_start_time, rest_duration) pair so the
483+
// notification effect only re-fires when the rest period actually changes.
484+
let rest_key = use_memo(move || {
485+
let rd = *rest_duration.read();
486+
session()
487+
.and_then(|s| s.rest_start_time)
488+
.map(|start| (start, rd))
489+
});
490+
491+
// Track how many rest-exceeded intervals have fired for the current rest
492+
// period. Reset to 0 each time a new rest period begins.
493+
let mut rest_bell_count = use_signal(|| 0u64);
494+
495+
// Pre-localise the notification strings in the reactive context so they
496+
// can be moved into async closures without requiring i18n context access.
497+
let rest_notif_title = use_memo(move || t!("notif-rest-title").to_string());
498+
let rest_notif_body = use_memo(move || t!("notif-rest-body").to_string());
499+
500+
// Schedule a precise one-shot rest-over notification whenever a new rest
501+
// period begins. Fires ~250 ms early to compensate for jitter.
502+
use_effect(move || {
503+
let Some((start, duration)) = rest_key() else {
504+
return;
505+
};
506+
if duration == 0 {
507+
return;
508+
}
509+
// Reset the exceeded-interval counter for the new rest period.
510+
rest_bell_count.set(0);
511+
512+
let title = rest_notif_title.peek().clone();
513+
let body = rest_notif_body.peek().clone();
514+
let fire_at_secs = start + duration;
515+
516+
#[cfg(not(target_arch = "wasm32"))]
517+
{
518+
let now = crate::models::get_current_timestamp();
519+
if fire_at_secs > now {
520+
let delay_ms = ((fire_at_secs - now) * 1_000)
521+
.saturating_sub(crate::components::session_timers::NOTIF_EARLY_MS);
522+
tokio::spawn(async move {
523+
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
524+
crate::services::notifications::send_notification(&title, &body, "logout-rest");
525+
});
526+
} else {
527+
crate::services::notifications::send_notification(&title, &body, "logout-rest");
528+
}
529+
}
530+
#[cfg(target_arch = "wasm32")]
531+
{
532+
let now = crate::models::get_current_timestamp();
533+
let delay_ms = if fire_at_secs > now {
534+
((fire_at_secs - now) * 1_000)
535+
.saturating_sub(crate::components::session_timers::NOTIF_EARLY_MS)
536+
.min(u32::MAX as u64) as u32
537+
} else {
538+
0
539+
};
540+
wasm_bindgen_futures::spawn_local(async move {
541+
gloo_timers::future::TimeoutFuture::new(delay_ms).await;
542+
crate::services::notifications::send_notification(&title, &body, "logout-rest");
543+
});
544+
}
545+
});
546+
547+
// Tick-based coroutine: fires a notification for every completed exceeded
548+
// interval (2nd, 3rd, … ring) so the user keeps being reminded.
549+
use_coroutine(move |_: UnboundedReceiver<()>| async move {
550+
loop {
551+
crate::utils::sleep_ms(1_000).await;
552+
let Some((start, duration)) = *rest_key.peek() else {
553+
continue;
554+
};
555+
if duration == 0 {
556+
continue;
557+
}
558+
let now = crate::models::get_current_timestamp();
559+
let elapsed = now.saturating_sub(start);
560+
let intervals = elapsed / duration;
561+
let prev = *rest_bell_count.peek();
562+
if intervals > prev {
563+
rest_bell_count.set(intervals);
564+
crate::services::notifications::send_notification(
565+
&rest_notif_title.peek(),
566+
&rest_notif_body.peek(),
567+
"logout-rest",
568+
);
569+
}
570+
}
571+
});
572+
483573
use_effect(move || {
484574
if *show_rest.read() {
485575
rest_input_value.set(rest_duration.read().to_string());

src/components/active_session/pending_exercises.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub fn PendingExercisesSection(
5454
if resolved.len() > 1 {
5555
details {
5656
summary { {t!("pending-more", count : (resolved.len() - 1).to_string())} }
57-
for (id , name , category) in resolved.iter().skip(1).cloned() {
57+
for (id, name, category) in resolved.iter().skip(1).cloned() {
5858
{
5959
let id2 = id.clone();
6060
rsx! {

src/components/analytics/chart.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ pub fn ChartView(data: SeriesData, colors: Vec<&'static str>) -> Element {
312312
}
313313
}
314314
}
315-
for (slot_idx , _ , metric , points) in data.iter() {
315+
for (slot_idx, _, metric, points) in data.iter() {
316316
{
317317
let mi = metric.to_index();
318318
if let Some((_, scale, _, _)) = axis_data[mi] {
@@ -350,7 +350,7 @@ pub fn ChartView(data: SeriesData, colors: Vec<&'static str>) -> Element {
350350
stroke_linecap: "round",
351351
opacity: "0.7",
352352
}
353-
for (x , y) in points.iter() {
353+
for (x, y) in points.iter() {
354354
circle {
355355
cx: "{scale_x(*x)}",
356356
cy: "{y_svg(y * scale, mi)}",
@@ -454,7 +454,7 @@ pub fn ChartView(data: SeriesData, colors: Vec<&'static str>) -> Element {
454454
}
455455
if !cursor_values.is_empty() {
456456
div { class: "cursor-values",
457-
for (slot_idx , name , value , unit) in cursor_values.iter() {
457+
for (slot_idx, name, value, unit) in cursor_values.iter() {
458458
div { class: "cursor-value-row",
459459
span {
460460
class: "cursor-swatch",

src/components/analytics/selector.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub fn MetricSelector(
5858
pairs[i].1 = if value.is_empty() { None } else { Some(value) };
5959
},
6060
option { value: "", {t!("analytics-select-exercise")} }
61-
for (id , name) in exercises_for_slot.iter() {
61+
for (id, name) in exercises_for_slot.iter() {
6262
option { value: "{id}", "{name}" }
6363
}
6464
}

src/components/exercise_form_fields.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ pub fn ExerciseFormFields(
372372
}
373373
if !instructions_list.read().is_empty() {
374374
ol {
375-
for (idx , instruction) in instructions_list.read().iter().enumerate() {
375+
for (idx, instruction) in instructions_list.read().iter().enumerate() {
376376
li { key: "{idx}",
377377
span { "{instruction}" }
378378
button {
@@ -399,7 +399,7 @@ pub fn ExerciseFormFields(
399399
{image_upload_widget}
400400
if !images_list.read().is_empty() {
401401
ul { class: "tags",
402-
for (idx , url) in images_list.read().iter().enumerate() {
402+
for (idx, url) in images_list.read().iter().enumerate() {
403403
li { key: "{idx}",
404404
button {
405405
class: "del label",

src/components/exercises.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ pub fn Exercises() -> Element {
240240
}
241241
if !active_filters.read().is_empty() {
242242
div { class: "filter-chips",
243-
for (i , filter) in active_filters.read().iter().enumerate() {
243+
for (i, filter) in active_filters.read().iter().enumerate() {
244244
button {
245245
class: "filter-chip active",
246246
title: t!("filter-remove"),
@@ -280,7 +280,7 @@ pub fn Exercises() -> Element {
280280
}
281281
}
282282
main { class: "exercises",
283-
for (exercise , is_custom , show_instructions) in visible_items() {
283+
for (exercise, is_custom, show_instructions) in visible_items() {
284284
ExerciseCard {
285285
key: "{exercise.id}",
286286
exercise,

0 commit comments

Comments
 (0)