Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions Strand/Liquid/LiquidTodayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct LiquidTodayView: View {
// cheap heart-rate fingerprint and memoises, so the widget, this shell and the other Today view all
// share one computation rather than scoring the day three times.
@State private var hostedStressHours: [DaytimeStress.HourPoint] = []
@State private var hostedStressActivityMaskedHours = 0

// sheets / expanders
@State private var guideSection: ScoreSection?
Expand Down Expand Up @@ -807,6 +808,12 @@ struct LiquidTodayView: View {
.foregroundStyle(StrandPalette.textTertiary)
.frame(maxWidth: .infinity, minHeight: 60, alignment: .center)
}
if let maskedCaption = stressActivityMaskedHoursCaption(hostedStressActivityMaskedHours) {
Text(maskedCaption)
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
case .asleepDuration: AsleepDurationCard(data: AsleepDurationData.build(days: repo.days))
Expand Down Expand Up @@ -1806,9 +1813,14 @@ struct LiquidTodayView: View {
}

// #2040: and today's stress, on the same "only when hosted" rule.
hostedStressHours = HostedCardPrefs.decodeEnabled(hostedCardsRaw).contains(.stressToday)
? (await StressDayCurve.today(repo: repo)?.result.timeline ?? [])
: []
if HostedCardPrefs.decodeEnabled(hostedCardsRaw).contains(.stressToday) {
let result = await StressDayCurve.today(repo: repo)?.result
hostedStressHours = result?.timeline ?? []
hostedStressActivityMaskedHours = result?.activityMaskedHours ?? 0
} else {
hostedStressHours = []
hostedStressActivityMaskedHours = 0
}

// First load done — bring the hero gauges + sky to life now the launch churn has settled.
if !dataLoaded { withAnimation(.easeIn(duration: 0.4)) { dataLoaded = true } }
Expand Down
18 changes: 18 additions & 0 deletions Strand/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{
"sourceLanguage": "en",
"strings": {
"%lld hours excluded — you were moving.": { "localizations": {
"de": {"stringUnit": {"state": "translated", "value": "%lld Stunden ausgeschlossen — du warst in Bewegung."}}, "es": {"stringUnit": {"state": "translated", "value": "Se excluyeron %lld horas porque estabas en movimiento."}}, "fr": {"stringUnit": {"state": "translated", "value": "%lld heures exclues — vous étiez en mouvement."}}, "it": {"stringUnit": {"state": "translated", "value": "%lld ore escluse — eri in movimento."}}, "pl": {"stringUnit": {"state": "translated", "value": "Wykryty ruch — wykluczone godziny: %lld."}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "Foram excluídas %lld horas — estiveste em movimento."}}, "ru": {"stringUnit": {"state": "translated", "value": "Движение: исключённые часы — %lld."}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "因你当时在活动,已排除 %lld 小时。"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "因你當時在活動,已排除 %lld 小時。"}}
} },
"1 hour excluded — you were moving.": { "localizations": {
"de": {"stringUnit": {"state": "translated", "value": "1 Stunde ausgeschlossen — du warst in Bewegung."}}, "es": {"stringUnit": {"state": "translated", "value": "Se excluyó 1 hora porque estabas en movimiento."}}, "fr": {"stringUnit": {"state": "translated", "value": "1 heure exclue — vous étiez en mouvement."}}, "it": {"stringUnit": {"state": "translated", "value": "1 ora esclusa — eri in movimento."}}, "pl": {"stringUnit": {"state": "translated", "value": "Wykluczono 1 godzinę — wykryto ruch."}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "Foi excluída 1 hora — estiveste em movimento."}}, "ru": {"stringUnit": {"state": "translated", "value": "Исключён 1 час — вы двигались."}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "因你当时在活动,已排除 1 小时。"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "因你當時在活動,已排除 1 小時。"}}
} },
"Autonomic load across your waking day": { "localizations": {
"de": {"stringUnit": {"state": "translated", "value": "Autonome Belastung im Verlauf deines wachen Tages"}}, "es": {"stringUnit": {"state": "translated", "value": "Carga autonómica durante las horas de vigilia"}}, "fr": {"stringUnit": {"state": "translated", "value": "Charge autonome au fil de votre journée éveillée"}}, "it": {"stringUnit": {"state": "translated", "value": "Carico autonomico durante le ore di veglia"}}, "pl": {"stringUnit": {"state": "translated", "value": "Obciążenie autonomiczne w ciągu dnia"}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "Carga autonómica ao longo do teu dia acordado"}}, "ru": {"stringUnit": {"state": "translated", "value": "Вегетативная нагрузка в течение дня бодрствования"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "清醒时段的自主神经负荷"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "清醒時段的自主神經負荷"}}
} },
"The line is each waking hour's 0-3 proxy, scored against your personal daytime baseline (how your own days usually run). The bar below splits your day into calm, moderate and high stress time.": { "localizations": {
"de": {"stringUnit": {"state": "translated", "value": "Die Linie zeigt den 0–3-Näherungswert jeder Wachstunde, bewertet anhand deiner persönlichen Tages-Baseline (wie deine Tage normalerweise verlaufen). Der Balken darunter teilt deinen Tag in ruhige, mäßige und hohe Stresszeiten."}}, "es": {"stringUnit": {"state": "translated", "value": "La línea muestra el indicador de 0 a 3 de cada hora de vigilia, comparado con tu referencia diurna personal (cómo suelen transcurrir tus días). La barra inferior divide el día en periodos de estrés bajo, moderado y alto."}}, "fr": {"stringUnit": {"state": "translated", "value": "La ligne représente l'indice 0–3 de chaque heure éveillée, évalué par rapport à votre référence diurne personnelle (le déroulement habituel de vos journées). La barre ci-dessous répartit votre journée en périodes de stress faible, modéré et élevé."}}, "it": {"stringUnit": {"state": "translated", "value": "La linea mostra l'indicatore da 0 a 3 per ogni ora di veglia, valutato rispetto al tuo riferimento diurno personale (l'andamento abituale delle tue giornate). La barra sottostante divide la giornata in periodi di stress basso, moderato e alto."}}, "pl": {"stringUnit": {"state": "translated", "value": "Linia pokazuje wskaźnik 0–3 dla każdej godziny czuwania, oceniany względem Twojej osobistej dziennej linii bazowej (typowego przebiegu Twoich dni). Pasek poniżej dzieli dzień na okresy niskiego, umiarkowanego i wysokiego stresu."}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "A linha mostra o indicador de 0–3 de cada hora de vigília, avaliado em relação à tua linha de base diurna pessoal (como os teus dias costumam decorrer). A barra abaixo divide o teu dia em períodos de stress baixo, moderado e elevado."}}, "ru": {"stringUnit": {"state": "translated", "value": "Линия показывает показатель 0–3 за каждый час бодрствования относительно вашей личной дневной нормы (обычного хода ваших дней). Полоса ниже делит день на периоды низкого, умеренного и высокого стресса."}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "折线显示每个清醒小时的 0–3 指标,并以你的个人日间基线(你平时一天的状态)为基准评分。下方的条形将你的一天划分为低、中、高压力时段。"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "折線顯示每個清醒小時的 0–3 指標,並以你的個人日間基線(你平時一天的狀態)為基準評分。下方的長條將你的一天劃分為低、中、高壓力時段。"}}
} },
"Close coach settings": { "localizations": {
"de": {"stringUnit": {"state": "translated", "value": "Coach-Einstellungen schließen"}}, "en": {"stringUnit": {"state": "translated", "value": "Close coach settings"}}, "es": {"stringUnit": {"state": "translated", "value": "Cerrar los ajustes del coach"}}, "fr": {"stringUnit": {"state": "translated", "value": "Fermer les réglages du coach"}}, "it": {"stringUnit": {"state": "translated", "value": "Chiudi le impostazioni del coach"}}, "pl": {"stringUnit": {"state": "translated", "value": "Zamknij ustawienia trenera"}}, "pt-PT": {"stringUnit": {"state": "translated", "value": "Fechar as definições do coach"}}, "ru": {"stringUnit": {"state": "translated", "value": "Закрыть настройки тренера"}}, "zh-Hans": {"stringUnit": {"state": "translated", "value": "关闭教练设置"}}, "zh-Hant": {"stringUnit": {"state": "translated", "value": "關閉教練設定"}}
} },
Expand Down Expand Up @@ -160755,6 +160767,12 @@
"value": "La ligne représente l'indice 0–3 de chaque heure éveillée, noté par rapport à vos propres heures calmes d'aujourd'hui. La barre ci-dessous répartit votre journée en temps de stress calme, modéré et élevé."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "La linea mostra l'indicatore da 0 a 3 per ogni ora di veglia, valutato rispetto alle tue ore calme di oggi. La barra sottostante divide la giornata in periodi di stress basso, moderato e alto."
}
},
"pt-PT": {
"stringUnit": {
"state": "translated",
Expand Down
17 changes: 16 additions & 1 deletion Strand/Screens/StressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import StrandDesign
import StrandAnalytics
import WhoopStore

/// The shared explanation for an activity-masked gap on the Stress screen and hosted Today cards.
/// Whole-phrase singular/plural variants keep the sentence natural in every catalog locale.
func stressActivityMaskedHoursCaption(_ count: Int) -> String? {
guard count > 0 else { return nil }
return count == 1
? String(localized: "1 hour excluded — you were moving.")
: String(localized: "\(count) hours excluded — you were moving.")
}

// MARK: - Stress Monitor
//
// A clear, Whoop-style "Stress Monitor": one 0–3 number, a band (LOW/MEDIUM/HIGH),
Expand Down Expand Up @@ -64,7 +73,7 @@ struct StressView: View {
@State private var modelSignature: StressInputs?

var body: some View {
ScreenScaffold(title: "Stress", subtitle: "Autonomic load from HRV and resting heart rate",
ScreenScaffold(title: "Stress", subtitle: "Autonomic load across your waking day",
// PERF (scroll): lazy column — byte-identical layout (LazyVStack == eager VStack
// alignment/spacing/header). The content is one inner eager VStack, so the staggered
// section reveal is unchanged; this only defers building that stack until it scrolls in.
Expand Down Expand Up @@ -340,6 +349,12 @@ struct StressView: View {
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
if let maskedCaption = stressActivityMaskedHoursCaption(day.activityMaskedHours) {
Text(maskedCaption)
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion Strand/Screens/TodayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ struct TodayView: View {
// cheap heart-rate fingerprint, so a refresh that changed nothing costs one indexed COUNT and no
// rows, and the iOS widget shares the same computation rather than scoring the day twice.
@State private var hostedStressHours: [DaytimeStress.HourPoint] = []
@State private var hostedStressActivityMaskedHours = 0

// TODAY's in-progress Effort (NOOP 0–100 axis), recomputed over the day's HR (local-midnight→now)
// each load so the gauge tracks today as it accumulates rather than waiting on the heavy daily pass
Expand Down Expand Up @@ -2563,6 +2564,12 @@ struct TodayView: View {
.foregroundStyle(StrandPalette.textTertiary)
.frame(maxWidth: .infinity, minHeight: 60, alignment: .center)
}
if let maskedCaption = stressActivityMaskedHoursCaption(hostedStressActivityMaskedHours) {
Text(maskedCaption)
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
case .asleepDuration: AsleepDurationCard(data: AsleepDurationData.build(days: repo.days))
Expand Down Expand Up @@ -4654,11 +4661,14 @@ struct TodayView: View {
private func loadHostedStress() async {
guard HostedCardPrefs.decodeEnabled(hostedCardsRaw).contains(.stressToday) else {
hostedStressHours = []
hostedStressActivityMaskedHours = 0
return
}
// `timeline`, not `hours`: the half-step display series, so the curve tracks the day rather
// than stepping through it, matching the widget and the Android card.
hostedStressHours = await StressDayCurve.today(repo: repo)?.result.timeline ?? []
let result = await StressDayCurve.today(repo: repo)?.result
hostedStressHours = result?.timeline ?? []
hostedStressActivityMaskedHours = result?.activityMaskedHours ?? 0
}

private func loadHostedSleepModel() async {
Expand Down
2 changes: 1 addition & 1 deletion Tools/i18n_extra_locale_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ NOOPWatch/Localizable.xcstrings:zh-Hant 0
NOOPWatchComplications/Localizable.xcstrings:it 25
NOOPWatchComplications/Localizable.xcstrings:zh-Hans 0
NOOPWatchComplications/Localizable.xcstrings:zh-Hant 0
Strand/Resources/Localizable.xcstrings:it 179
Strand/Resources/Localizable.xcstrings:it 178
Strand/Resources/Localizable.xcstrings:ru 1
Strand/Resources/Localizable.xcstrings:zh-Hans 0
Strand/Resources/Localizable.xcstrings:zh-Hant 79
Expand Down
Loading
Loading