Add Daily Digest notification preferences with custom time support
- Add dailyDigest and dailyDigestHour fields to Kotlin NotificationPreference model - Update NotificationPreferencesViewModel to support new fields - Add Daily Summary toggle with time picker to Android NotificationPreferencesScreen - Add Daily Summary toggle with time picker to iOS NotificationPreferencesView - Add localized strings for Daily Summary in all 10 languages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -547,6 +547,8 @@ enum L10n {
|
||||
static var propertySharedDescription: String { String(localized: "profile_property_shared_description") }
|
||||
static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") }
|
||||
static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") }
|
||||
static var dailyDigest: String { String(localized: "profile_daily_digest") }
|
||||
static var dailyDigestDescription: String { String(localized: "profile_daily_digest_description") }
|
||||
static var otherNotifications: String { String(localized: "profile_other_notifications") }
|
||||
|
||||
// Email Notifications
|
||||
|
||||
@@ -20543,6 +20543,136 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile_daily_digest" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tägliche Zusammenfassung"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Daily Summary"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Resumen diario"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Résumé quotidien"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Riepilogo giornaliero"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "デイリーサマリー"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "일일 요약"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Dagelijkse samenvatting"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Resumo diário"
|
||||
}
|
||||
},
|
||||
"zh" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "每日摘要"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile_daily_digest_description" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tägliche Übersicht über fällige und überfällige Aufgaben"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Daily overview of tasks due and overdue"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Resumen diario de tareas pendientes y vencidas"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aperçu quotidien des tâches à faire et en retard"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Panoramica giornaliera delle attività in scadenza e scadute"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "期限が近いタスクと期限切れのタスクの毎日の概要"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "마감 예정 및 지연된 작업의 일일 개요"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Dagelijks overzicht van taken die binnenkort verlopen en achterstallig zijn"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Resumo diário de tarefas a vencer e atrasadas"
|
||||
}
|
||||
},
|
||||
"zh" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "即将到期和逾期任务的每日概览"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties_add_button" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -217,6 +217,40 @@ struct NotificationPreferencesView: View {
|
||||
.onChange(of: viewModel.warrantyExpiring) { _, newValue in
|
||||
viewModel.updatePreference(warrantyExpiring: newValue)
|
||||
}
|
||||
|
||||
Toggle(isOn: $viewModel.dailyDigest) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.Profile.dailyDigest)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text(L10n.Profile.dailyDigestDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.clipboard.fill")
|
||||
.foregroundColor(Color.appSecondary)
|
||||
}
|
||||
}
|
||||
.tint(Color.appPrimary)
|
||||
.onChange(of: viewModel.dailyDigest) { _, newValue in
|
||||
viewModel.updatePreference(dailyDigest: newValue)
|
||||
}
|
||||
|
||||
// Time picker for Daily Digest
|
||||
if viewModel.dailyDigest {
|
||||
NotificationTimePickerRow(
|
||||
isEnabled: $viewModel.dailyDigestTimeEnabled,
|
||||
selectedHour: $viewModel.dailyDigestHour,
|
||||
onEnableCustomTime: {
|
||||
viewModel.enableCustomTime(for: .dailyDigest)
|
||||
},
|
||||
onTimeChange: { hour in
|
||||
viewModel.updateCustomTime(hour, for: .dailyDigest)
|
||||
},
|
||||
formatHour: viewModel.formatHour
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.Profile.otherNotifications)
|
||||
}
|
||||
@@ -278,17 +312,20 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
@Published var taskAssigned: Bool = true
|
||||
@Published var residenceShared: Bool = true
|
||||
@Published var warrantyExpiring: Bool = true
|
||||
@Published var dailyDigest: Bool = true
|
||||
@Published var emailTaskCompleted: Bool = true
|
||||
|
||||
// Custom notification times (local hours, 0-23)
|
||||
@Published var taskDueSoonHour: Int? = nil
|
||||
@Published var taskOverdueHour: Int? = nil
|
||||
@Published var warrantyExpiringHour: Int? = nil
|
||||
@Published var dailyDigestHour: Int? = nil
|
||||
|
||||
// Track if user has enabled custom times
|
||||
@Published var taskDueSoonTimeEnabled: Bool = false
|
||||
@Published var taskOverdueTimeEnabled: Bool = false
|
||||
@Published var warrantyExpiringTimeEnabled: Bool = false
|
||||
@Published var dailyDigestTimeEnabled: Bool = false
|
||||
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@@ -298,6 +335,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
private let defaultTaskDueSoonLocalHour = 14 // 2 PM local
|
||||
private let defaultTaskOverdueLocalHour = 9 // 9 AM local
|
||||
private let defaultWarrantyExpiringLocalHour = 10 // 10 AM local
|
||||
private let defaultDailyDigestLocalHour = 8 // 8 AM local
|
||||
|
||||
func loadPreferences() {
|
||||
isLoading = true
|
||||
@@ -314,6 +352,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
self.taskAssigned = prefs.taskAssigned
|
||||
self.residenceShared = prefs.residenceShared
|
||||
self.warrantyExpiring = prefs.warrantyExpiring
|
||||
self.dailyDigest = prefs.dailyDigest
|
||||
self.emailTaskCompleted = prefs.emailTaskCompleted
|
||||
|
||||
// Load custom notification times (convert from UTC to local)
|
||||
@@ -329,6 +368,10 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
self.warrantyExpiringHour = DateUtils.utcHourToLocal(Int(utcHour))
|
||||
self.warrantyExpiringTimeEnabled = true
|
||||
}
|
||||
if let utcHour = prefs.dailyDigestHour?.intValue {
|
||||
self.dailyDigestHour = DateUtils.utcHourToLocal(Int(utcHour))
|
||||
self.dailyDigestTimeEnabled = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
self.errorMessage = nil
|
||||
@@ -350,10 +393,12 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
taskAssigned: Bool? = nil,
|
||||
residenceShared: Bool? = nil,
|
||||
warrantyExpiring: Bool? = nil,
|
||||
dailyDigest: Bool? = nil,
|
||||
emailTaskCompleted: Bool? = nil,
|
||||
taskDueSoonHour: Int? = nil,
|
||||
taskOverdueHour: Int? = nil,
|
||||
warrantyExpiringHour: Int? = nil
|
||||
warrantyExpiringHour: Int? = nil,
|
||||
dailyDigestHour: Int? = nil
|
||||
) {
|
||||
isSaving = true
|
||||
|
||||
@@ -363,6 +408,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
let taskDueSoonUtc = taskDueSoonHour.map { DateUtils.localHourToUtc($0) }
|
||||
let taskOverdueUtc = taskOverdueHour.map { DateUtils.localHourToUtc($0) }
|
||||
let warrantyExpiringUtc = warrantyExpiringHour.map { DateUtils.localHourToUtc($0) }
|
||||
let dailyDigestUtc = dailyDigestHour.map { DateUtils.localHourToUtc($0) }
|
||||
|
||||
let request = UpdateNotificationPreferencesRequest(
|
||||
taskDueSoon: taskDueSoon.map { KotlinBoolean(bool: $0) },
|
||||
@@ -371,10 +417,12 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
taskAssigned: taskAssigned.map { KotlinBoolean(bool: $0) },
|
||||
residenceShared: residenceShared.map { KotlinBoolean(bool: $0) },
|
||||
warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) },
|
||||
dailyDigest: dailyDigest.map { KotlinBoolean(bool: $0) },
|
||||
emailTaskCompleted: emailTaskCompleted.map { KotlinBoolean(bool: $0) },
|
||||
taskDueSoonHour: taskDueSoonUtc.map { KotlinInt(int: Int32($0)) },
|
||||
taskOverdueHour: taskOverdueUtc.map { KotlinInt(int: Int32($0)) },
|
||||
warrantyExpiringHour: warrantyExpiringUtc.map { KotlinInt(int: Int32($0)) }
|
||||
warrantyExpiringHour: warrantyExpiringUtc.map { KotlinInt(int: Int32($0)) },
|
||||
dailyDigestHour: dailyDigestUtc.map { KotlinInt(int: Int32($0)) }
|
||||
)
|
||||
let result = try await APILayer.shared.updateNotificationPreferences(request: request)
|
||||
|
||||
@@ -406,6 +454,10 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
warrantyExpiringHour = defaultWarrantyExpiringLocalHour
|
||||
warrantyExpiringTimeEnabled = true
|
||||
updatePreference(warrantyExpiringHour: defaultWarrantyExpiringLocalHour)
|
||||
case .dailyDigest:
|
||||
dailyDigestHour = defaultDailyDigestLocalHour
|
||||
dailyDigestTimeEnabled = true
|
||||
updatePreference(dailyDigestHour: defaultDailyDigestLocalHour)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +473,9 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
case .warrantyExpiring:
|
||||
warrantyExpiringHour = hour
|
||||
updatePreference(warrantyExpiringHour: hour)
|
||||
case .dailyDigest:
|
||||
dailyDigestHour = hour
|
||||
updatePreference(dailyDigestHour: hour)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +483,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
|
||||
case taskDueSoon
|
||||
case taskOverdue
|
||||
case warrantyExpiring
|
||||
case dailyDigest
|
||||
}
|
||||
|
||||
// Format hour to display string
|
||||
|
||||
Reference in New Issue
Block a user