diff --git a/docs/releases.md b/docs/releases.md index 2c6a8db2..527c52dd 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1985,10 +1985,15 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ### ntfy server v2.26.x (UNRELEASED) +**Features:** + +* Web app: Add "Date format" and "Time format" settings (Settings -> Appearance), with ISO 8601, day/month/year (slash or dot) and month/day/year date options and a 12-/24-hour clock option, and base the default format on your browser/system locale rather than the selected display language. When logged in, both settings sync across devices via your account ([#1647](https://github.com/binwiederhier/ntfy/issues/1647), thanks to [@wsw70](https://github.com/wsw70) for reporting) + **Bug fixes + maintenance:** * Web app: Smooth transitions and loading animation, remove flickering * Web app: `GET /account` now reads from the primary database instead of a read replica, so the account view no longer shows stale data right after a change when replicas lag behind +* Docs: Document the third-party HelmForge Helm chart as a Kubernetes installation option ([#1727](https://github.com/binwiederhier/ntfy/issues/1727), thanks to [@mberlofa](https://github.com/mberlofa)) ### ntfy Android v1.25.x (UNRELEASED) diff --git a/server/server_account.go b/server/server_account.go index 34d93d6b..4b80285a 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -107,6 +107,12 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis if u.Prefs.Language != nil { response.Language = *u.Prefs.Language } + if u.Prefs.DateFormat != nil { + response.DateFormat = *u.Prefs.DateFormat + } + if u.Prefs.TimeFormat != nil { + response.TimeFormat = *u.Prefs.TimeFormat + } if u.Prefs.Notification != nil { response.Notification = u.Prefs.Notification } @@ -373,6 +379,12 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ if newPrefs.Language != nil { prefs.Language = newPrefs.Language } + if newPrefs.DateFormat != nil { + prefs.DateFormat = newPrefs.DateFormat + } + if newPrefs.TimeFormat != nil { + prefs.TimeFormat = newPrefs.TimeFormat + } if newPrefs.Notification != nil { if prefs.Notification == nil { prefs.Notification = &user.NotificationPrefs{} diff --git a/server/server_account_test.go b/server/server_account_test.go index 3c4f1787..cee5c57b 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -207,12 +207,19 @@ func TestAccount_ChangeSettings(t *testing.T) { }) require.Equal(t, 200, rr.Code) + rr = request(t, s, "PATCH", "/v1/account/settings", `{"date_format": "iso8601", "time_format": "24h"}`, map[string]string{ + "Authorization": util.BearerAuth(token.Value), + }) + require.Equal(t, 200, rr.Code) + rr = request(t, s, "GET", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ "Authorization": util.BearerAuth(token.Value), }) require.Equal(t, 200, rr.Code) account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) require.Equal(t, "de", account.Language) + require.Equal(t, "iso8601", account.DateFormat) // Merged, not overwritten by previous PATCH + require.Equal(t, "24h", account.TimeFormat) require.Equal(t, util.Int(86400), account.Notification.DeleteAfter) require.Equal(t, util.String("juntos"), account.Notification.Sound) require.Nil(t, account.Notification.MinPriority) // Not set diff --git a/server/types.go b/server/types.go index 5b254c4d..5c5e65ec 100644 --- a/server/types.go +++ b/server/types.go @@ -312,6 +312,8 @@ type apiAccountResponse struct { SyncTopic string `json:"sync_topic,omitempty"` Provisioned bool `json:"provisioned,omitempty"` Language string `json:"language,omitempty"` + DateFormat string `json:"date_format,omitempty"` + TimeFormat string `json:"time_format,omitempty"` Notification *user.NotificationPrefs `json:"notification,omitempty"` Subscriptions []*user.Subscription `json:"subscriptions,omitempty"` Reservations []*apiAccountReservation `json:"reservations,omitempty"` diff --git a/user/types.go b/user/types.go index 344bfa5d..e416113a 100644 --- a/user/types.go +++ b/user/types.go @@ -97,6 +97,8 @@ type MagicLink struct { // Prefs represents a user's configuration settings type Prefs struct { Language *string `json:"language,omitempty"` + DateFormat *string `json:"date_format,omitempty"` + TimeFormat *string `json:"time_format,omitempty"` Notification *NotificationPrefs `json:"notification,omitempty"` Subscriptions []*Subscription `json:"subscriptions,omitempty"` } diff --git a/web/jsconfig.json b/web/jsconfig.json new file mode 100644 index 00000000..924531f5 --- /dev/null +++ b/web/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "checkJs": false + }, + "include": ["src"], + "exclude": ["node_modules", "build"] +} diff --git a/web/public/static/langs/ar.json b/web/public/static/langs/ar.json index d0c1e4b6..cb786842 100644 --- a/web/public/static/langs/ar.json +++ b/web/public/static/langs/ar.json @@ -361,7 +361,7 @@ "account_upgrade_dialog_tier_price_per_month": "شهر", "prefs_appearance_theme_title": "السمة", "subscribe_dialog_subscribe_use_another_background_info": "لن يتم استلام الاشعارات من الخوادم الخارجية عندما يكون تطبيق الويب مغلقاً", - "prefs_appearance_theme_system": "النظام (الافتراضي)", + "prefs_system_default": "النظام الافتراضي", "prefs_notifications_min_priority_low_and_higher": "أولوية منخفضة وأعلى", "prefs_notifications_min_priority_default_and_higher": "الأولوية الافتراضية وما فوقها", "prefs_notifications_min_priority_high_and_higher": "أولوية عالية وأعلى" diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 4e81bb75..f754cfae 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -400,7 +400,7 @@ "prefs_notifications_web_push_title": "Известия във фонов режим", "prefs_notifications_web_push_enabled_description": "Известията ще бъдат получавани даже и ако приложението за уеб не работи (чрез Web Push)", "prefs_appearance_theme_title": "Цветова тема", - "prefs_appearance_theme_system": "Системна (подразбирана)", + "prefs_system_default": "Системна подразбирана", "web_push_subscription_expiring_title": "Известията временно ще бъдат спрени", "web_push_subscription_expiring_body": "За да продължите да получавате известия, отворете ntfy", "action_bar_unmute_notifications": "Включване звука на известията", diff --git a/web/public/static/langs/cs.json b/web/public/static/langs/cs.json index dde71051..4590624d 100644 --- a/web/public/static/langs/cs.json +++ b/web/public/static/langs/cs.json @@ -398,7 +398,7 @@ "prefs_notifications_web_push_disabled_description": "Oznámení jsou přijímána, když je webová aplikace spuštěna (přes WebSocket)", "prefs_notifications_web_push_disabled": "Zakázáno", "prefs_appearance_theme_title": "Motiv", - "prefs_appearance_theme_system": "Systém (výchozí)", + "prefs_system_default": "Systém výchozí", "prefs_appearance_theme_dark": "Tmavý režim", "prefs_appearance_theme_light": "Světlý režim", "web_push_subscription_expiring_title": "Oznámení budou pozastavena", diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index a619bedb..8fd5a5ba 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -395,7 +395,7 @@ "prefs_notifications_web_push_enabled": "Aktiviert für {{server}}", "prefs_notifications_web_push_disabled": "Deaktiviert", "prefs_appearance_theme_title": "Thema", - "prefs_appearance_theme_system": "System (Standard)", + "prefs_system_default": "System Standard", "prefs_appearance_theme_dark": "Nachtmodus", "prefs_appearance_theme_light": "Tagmodus", "error_boundary_button_reload_ntfy": "ntfy neu laden", diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index e2fae504..b55568a0 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -412,9 +412,17 @@ "prefs_appearance_title": "Appearance", "prefs_appearance_language_title": "Language", "prefs_appearance_theme_title": "Theme", - "prefs_appearance_theme_system": "System (default)", + "prefs_system_default": "System default", "prefs_appearance_theme_dark": "Dark mode", "prefs_appearance_theme_light": "Light mode", + "prefs_appearance_date_format_title": "Date format", + "prefs_appearance_date_format_iso8601": "ISO 8601", + "prefs_appearance_date_format_dmy": "Day/Month/Year", + "prefs_appearance_date_format_dmy_dot": "Day.Month.Year", + "prefs_appearance_date_format_mdy": "Month/Day/Year", + "prefs_appearance_time_format_title": "Time format", + "prefs_appearance_time_format_12h": "12-hour", + "prefs_appearance_time_format_24h": "24-hour", "prefs_reservations_title": "Reserved topics", "prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.", "prefs_reservations_limit_reached": "You reached your reserved topics limit.", diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json index 26d787c7..fbad65fe 100644 --- a/web/public/static/langs/es.json +++ b/web/public/static/langs/es.json @@ -395,7 +395,7 @@ "prefs_notifications_web_push_enabled_description": "Las notificaciones se reciben incluso cuando la aplicación web no se está ejecutando (a través de Web Push)", "prefs_notifications_web_push_disabled": "Desactivado", "prefs_appearance_theme_title": "Tema", - "prefs_appearance_theme_system": "Sistema (por defecto)", + "prefs_system_default": "Sistema por defecto", "error_boundary_button_reload_ntfy": "Volver a cargar ntfy", "web_push_subscription_expiring_title": "Las notificaciones se pausarán", "prefs_notifications_web_push_disabled_description": "Las notificaciones se reciben cuando la aplicación web se está ejecutando (a través de WebSocket)", diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 19c0c704..3f0425b7 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -238,7 +238,7 @@ "account_basics_tier_title": "Kasutajakonto tüüp", "account_basics_tier_description": "Sinu kasutajakonto õigused", "account_delete_dialog_button_submit": "Kustuta kasutajakonto jäädavalt", - "prefs_appearance_theme_system": "Süsteemi kujundus", + "prefs_system_default": "Süsteemi kujundus", "prefs_appearance_theme_dark": "Tume kujundus", "prefs_appearance_theme_light": "Hele kujundus", "prefs_reservations_title": "Reserveeritud teemad", diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json index 4ddf2920..4128fdb5 100644 --- a/web/public/static/langs/fi.json +++ b/web/public/static/langs/fi.json @@ -390,7 +390,7 @@ "alert_notification_ios_install_required_title": "iOS-asennus vaaditaan", "publish_dialog_checkbox_markdown": "Muotoile Markdownina", "prefs_notifications_web_push_title": "Taustailmoitukset", - "prefs_appearance_theme_system": "Järjestelmä (oletus)", + "prefs_system_default": "Järjestelmä oletus", "alert_notification_permission_denied_description": "Ota ilmoitukset uudelleen käyttöön selaimessa", "prefs_appearance_theme_title": "Teema", "prefs_appearance_theme_light": "Vaalea tila", diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json index 42051387..a9850136 100644 --- a/web/public/static/langs/fr.json +++ b/web/public/static/langs/fr.json @@ -396,7 +396,7 @@ "prefs_notifications_web_push_enabled": "Activé pour {{server}}", "prefs_notifications_web_push_disabled": "Désactivé", "prefs_appearance_theme_title": "Thème", - "prefs_appearance_theme_system": "Système (défaut)", + "prefs_system_default": "Système défaut", "prefs_appearance_theme_dark": "Mode sombre", "prefs_appearance_theme_light": "Mode clair", "error_boundary_button_reload_ntfy": "Recharger ntfy", diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json index d69da79c..8e5b8b57 100644 --- a/web/public/static/langs/gl.json +++ b/web/public/static/langs/gl.json @@ -398,7 +398,7 @@ "prefs_notifications_web_push_enabled": "Activadas para {{server}}", "prefs_notifications_web_push_disabled": "Desactivadas", "prefs_appearance_theme_title": "Decorado", - "prefs_appearance_theme_system": "Sistema (por defecto)", + "prefs_system_default": "Sistema por defecto", "prefs_appearance_theme_dark": "Modo escuro", "prefs_appearance_theme_light": "Modo claro", "error_boundary_button_reload_ntfy": "Recargar ntfy", diff --git a/web/public/static/langs/hu.json b/web/public/static/langs/hu.json index 8e17b691..a8a72cf6 100644 --- a/web/public/static/langs/hu.json +++ b/web/public/static/langs/hu.json @@ -371,7 +371,7 @@ "prefs_users_description_no_sync": "A felhasználónevek és jelszavak nem kerülnek szinkronizálásra a fiókjával.", "prefs_users_table_cannot_delete_or_edit": "A bejelentkezett felhasználót nem lehet törölni vagy szerkeszteni", "prefs_appearance_theme_title": "Téma", - "prefs_appearance_theme_system": "Rendszer (alapértelmezett)", + "prefs_system_default": "Rendszer alapértelmezett", "prefs_appearance_theme_dark": "Sötét mód", "prefs_appearance_theme_light": "Világos mód", "prefs_reservations_title": "Fenntartott témák", diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index 736270e9..c86537ac 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -399,7 +399,7 @@ "prefs_notifications_web_push_enabled": "Diaktifkan untuk {{server}}", "prefs_notifications_web_push_disabled": "Dinonaktifkan", "prefs_appearance_theme_dark": "Mode gelap", - "prefs_appearance_theme_system": "Sistem (bawaan)", + "prefs_system_default": "Sistem bawaan", "prefs_appearance_theme_light": "Mode terang", "web_push_subscription_expiring_title": "Notifikasi akan dijeda", "web_push_subscription_expiring_body": "Buka ntfy untuk terus menerima notifikasi", diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index cf2ad718..0bd24be3 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -390,7 +390,7 @@ "prefs_notifications_web_push_disabled": "Disabilitato", "prefs_users_table_cannot_delete_or_edit": "Impossibile eliminare o modificare l'utente registrato", "prefs_appearance_theme_title": "Tema", - "prefs_appearance_theme_system": "Sistema (predefinito)", + "prefs_system_default": "Sistema predefinito", "prefs_appearance_theme_dark": "Modalità scura", "prefs_appearance_theme_light": "Modalità chiara", "prefs_reservations_table_topic_header": "Argomento", diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index 1f672b2c..7eddfc52 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -400,7 +400,7 @@ "prefs_notifications_web_push_enabled": "{{server}}で有効", "prefs_notifications_web_push_disabled": "無効", "prefs_appearance_theme_title": "テーマ", - "prefs_appearance_theme_system": "システム (既定)", + "prefs_system_default": "システム 既定", "prefs_appearance_theme_dark": "ダークモード", "web_push_unknown_notification_title": "不明な通知を受信しました", "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります", diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json index 2bcf6391..e901b678 100644 --- a/web/public/static/langs/nb_NO.json +++ b/web/public/static/langs/nb_NO.json @@ -266,7 +266,7 @@ "account_upgrade_dialog_button_cancel_subscription": "Avslutt abonnement", "account_tokens_description": "Bruk tilgangstokener når du publiserer og abonnerer via ntfy-APIet, slik at du ikke trenger å sende innloggingsinformasjon for kontoen din. Se dokumentasjonen for å lære mer.", "account_tokens_table_current_session": "Nåværende nettleserøkt", - "prefs_appearance_theme_system": "System (standard)", + "prefs_system_default": "System standard", "prefs_notifications_web_push_disabled_description": "Varslinger mottas når web-appen kjører (via WebSocket)", "prefs_appearance_theme_title": "Tema", "prefs_appearance_theme_dark": "Mørk modus", diff --git a/web/public/static/langs/nl.json b/web/public/static/langs/nl.json index 66997fba..ae920efa 100644 --- a/web/public/static/langs/nl.json +++ b/web/public/static/langs/nl.json @@ -397,7 +397,7 @@ "prefs_notifications_web_push_disabled": "Uitgezet", "prefs_notifications_web_push_enabled_description": "Notificaties worden ontvangen, ook als de webapplicatie niet geopend is (via Web Push)", "prefs_appearance_theme_title": "Thema", - "prefs_appearance_theme_system": "Systeem (standaard)", + "prefs_system_default": "Systeem standaard", "prefs_appearance_theme_dark": "Donkere modus", "prefs_appearance_theme_light": "Lichte modus", "error_boundary_button_reload_ntfy": "Herlaad ntfy", diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json index 3f90cac4..c83c45fc 100644 --- a/web/public/static/langs/pl.json +++ b/web/public/static/langs/pl.json @@ -377,7 +377,7 @@ "prefs_notifications_web_push_disabled_description": "Powiadomienia są dostarczane kiedy aplikacja jest aktywna (poprzez WebSocket)", "prefs_notifications_web_push_enabled": "Włączone dla {{server}}", "prefs_notifications_web_push_disabled": "Wyłączone", - "prefs_appearance_theme_system": "Systemowy (domyślny)", + "prefs_system_default": "Systemowy domyślny", "prefs_appearance_theme_dark": "Tryb ciemny", "prefs_appearance_theme_light": "Tryb jasny", "prefs_reservations_edit_button": "Modyfikuj ustawienia dostępu dla tematu", diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index e7ec50aa..cc6cc639 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -370,7 +370,7 @@ "prefs_notifications_web_push_disabled": "Desativado", "prefs_users_table_cannot_delete_or_edit": "Não é possível apagar ou editar o utilizador conectado", "prefs_appearance_theme_title": "Tema", - "prefs_appearance_theme_system": "Sistema (padrão)", + "prefs_system_default": "Sistema padrão", "prefs_appearance_theme_dark": "Modo escuro", "prefs_appearance_theme_light": "Modo claro", "prefs_reservations_title": "Tópicos reservados", diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index e5498948..e84e4633 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -384,7 +384,7 @@ "prefs_notifications_web_push_disabled": "Desativado", "prefs_appearance_theme_title": "Tema", "prefs_users_table_cannot_delete_or_edit": "Não é possível apagar ou editar o usuário conectado", - "prefs_appearance_theme_system": "Sistema (padrão)", + "prefs_system_default": "Sistema padrão", "prefs_appearance_theme_dark": "Modo escuro", "prefs_appearance_theme_light": "Modo claro", "prefs_reservations_title": "Tópicos reservados", diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index 0283c759..637b9886 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -341,7 +341,7 @@ "prefs_appearance_title": "Aspect", "prefs_appearance_language_title": "Limbă", "prefs_appearance_theme_title": "Temă", - "prefs_appearance_theme_system": "Sistem (implicit)", + "prefs_system_default": "Sistem implicit", "prefs_appearance_theme_dark": "Mod întunecat", "prefs_appearance_theme_light": "Mod luminos", "prefs_reservations_title": "Subiecte rezervate", diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json index 43bb432e..b520df96 100644 --- a/web/public/static/langs/ru.json +++ b/web/public/static/langs/ru.json @@ -401,7 +401,7 @@ "notifications_actions_failed_notification": "Неудачное действие", "publish_dialog_checkbox_markdown": "Форматировать как Markdown", "subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто", - "prefs_appearance_theme_system": "Как в системе (по умолчанию)", + "prefs_system_default": "Как в системе по умолчанию", "prefs_appearance_theme_dark": "Тёмная", "prefs_appearance_theme_light": "Светлая", "account_basics_cannot_edit_or_delete_provisioned_user": "Пользователя, созданного автоматически, нельзя изменить или удалить", diff --git a/web/public/static/langs/sk.json b/web/public/static/langs/sk.json index 5e670789..7b26ca81 100644 --- a/web/public/static/langs/sk.json +++ b/web/public/static/langs/sk.json @@ -397,7 +397,7 @@ "prefs_notifications_web_push_enabled": "Povolené pre {{server}}", "prefs_notifications_web_push_disabled": "Zakázané", "prefs_appearance_theme_title": "Téma", - "prefs_appearance_theme_system": "Systémové (predvolené)", + "prefs_system_default": "Systémové predvolené", "prefs_appearance_theme_dark": "Tmavý režim", "prefs_appearance_theme_light": "Svetlý režim", "error_boundary_button_reload_ntfy": "Obnoviť ntfy", diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json index abcf94d6..92c7563e 100644 --- a/web/public/static/langs/sv.json +++ b/web/public/static/langs/sv.json @@ -395,7 +395,7 @@ "prefs_notifications_web_push_enabled": "Aktivera för {{server}}", "prefs_notifications_web_push_disabled": "Avaktivera", "prefs_appearance_theme_title": "Tema", - "prefs_appearance_theme_system": "System (basinställning)", + "prefs_system_default": "System basinställning", "prefs_appearance_theme_dark": "Mörkt läge", "prefs_appearance_theme_light": "Ljust läge", "error_boundary_button_reload_ntfy": "Ladda om ntfy", diff --git a/web/public/static/langs/ta.json b/web/public/static/langs/ta.json index b822107d..f66d7f7d 100644 --- a/web/public/static/langs/ta.json +++ b/web/public/static/langs/ta.json @@ -235,7 +235,7 @@ "prefs_appearance_title": "தோற்றம்", "prefs_appearance_language_title": "மொழி", "prefs_appearance_theme_title": "கருப்பொருள்", - "prefs_appearance_theme_system": "கணினி (இயல்புநிலை)", + "prefs_system_default": "கணினி இயல்புநிலை", "prefs_appearance_theme_dark": "இருண்ட முறை", "prefs_appearance_theme_light": "ஒளி பயன்முறை", "prefs_reservations_title": "ஒதுக்கப்பட்ட தலைப்புகள்", diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json index 864bb7b8..1d523f4b 100644 --- a/web/public/static/langs/tr.json +++ b/web/public/static/langs/tr.json @@ -395,7 +395,7 @@ "prefs_notifications_web_push_enabled": "{{server}} için etkinleştirildi", "prefs_notifications_web_push_disabled": "Devre dışı", "prefs_appearance_theme_title": "Tema", - "prefs_appearance_theme_system": "Sistem (öntanımlı)", + "prefs_system_default": "Sistem öntanımlı", "prefs_appearance_theme_dark": "Koyu mod", "prefs_appearance_theme_light": "Açık mod", "error_boundary_button_reload_ntfy": "ntfy'yi yeniden yükle", diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json index 29b4996f..71e9c6e9 100644 --- a/web/public/static/langs/uk.json +++ b/web/public/static/langs/uk.json @@ -398,7 +398,7 @@ "prefs_notifications_web_push_enabled": "Увімкнено для {{server}}", "prefs_notifications_web_push_disabled": "Вимкнено", "prefs_appearance_theme_title": "Тема оформлення", - "prefs_appearance_theme_system": "Система (за замовчуванням)", + "prefs_system_default": "Система за замовчуванням", "prefs_appearance_theme_light": "Світлий режим", "error_boundary_button_reload_ntfy": "Перезавантажити ntfy", "web_push_subscription_expiring_title": "Сповіщення буде призупинено", diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json index fea7a87e..9f8d8df2 100644 --- a/web/public/static/langs/zh_Hans.json +++ b/web/public/static/langs/zh_Hans.json @@ -152,7 +152,7 @@ "prefs_appearance_title": "外观", "prefs_appearance_language_title": "语言", "prefs_appearance_theme_title": "主題", - "prefs_appearance_theme_system": "系统 (默认)", + "prefs_system_default": "系统 默认", "prefs_appearance_theme_dark": "深色模式", "prefs_appearance_theme_light": "浅色模式", "priority_min": "最低", diff --git a/web/public/static/langs/zh_Hant.json b/web/public/static/langs/zh_Hant.json index 07a92411..e94536df 100644 --- a/web/public/static/langs/zh_Hant.json +++ b/web/public/static/langs/zh_Hant.json @@ -229,7 +229,7 @@ "prefs_appearance_language_title": "語言", "prefs_appearance_theme_dark": "深色模式", "prefs_appearance_theme_light": "淺色模式", - "prefs_appearance_theme_system": "系統 (預設)", + "prefs_system_default": "系統 預設", "prefs_appearance_theme_title": "主題", "prefs_appearance_title": "外觀", "prefs_notifications_delete_after_never_description": "永不自動刪除通知", diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 10facafd..0cc65c2c 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -448,6 +448,12 @@ class AccountApi { if (account.language) { await i18n.changeLanguage(account.language); } + if (account.date_format) { + await prefs.setDateFormat(account.date_format); + } + if (account.time_format) { + await prefs.setTimeFormat(account.time_format); + } if (account.notification) { if (account.notification.sound) { await prefs.setSound(account.notification.sound); diff --git a/web/src/app/AccountApi.test.js b/web/src/app/AccountApi.test.js index d8cdf4c3..9fb16c54 100644 --- a/web/src/app/AccountApi.test.js +++ b/web/src/app/AccountApi.test.js @@ -13,7 +13,7 @@ vi.mock("./Session", () => ({ })); vi.mock("./SubscriptionManager", () => ({ default: { syncFromRemote: vi.fn() } })); vi.mock("./Prefs", () => ({ - default: { setSound: vi.fn(), setDeleteAfter: vi.fn(), setMinPriority: vi.fn() }, + default: { setSound: vi.fn(), setDeleteAfter: vi.fn(), setMinPriority: vi.fn(), setDateFormat: vi.fn(), setTimeFormat: vi.fn() }, THEME: { DARK: "dark", LIGHT: "light", SYSTEM: "system" }, })); vi.mock("i18next", () => ({ default: { changeLanguage: vi.fn() } })); @@ -190,6 +190,8 @@ describe("AccountApi.sync", () => { fetchMock.mockResolvedValue( ok({ language: "de", + date_format: "iso8601", + time_format: "24h", notification: { sound: "ding", delete_after: 3600, min_priority: 3 }, subscriptions: [{ topic: "t" }], reservations: [{ topic: "t" }], @@ -199,6 +201,8 @@ describe("AccountApi.sync", () => { await accountApi.sync(); expect(i18n.changeLanguage).toHaveBeenCalledWith("de"); + expect(prefs.setDateFormat).toHaveBeenCalledWith("iso8601"); + expect(prefs.setTimeFormat).toHaveBeenCalledWith("24h"); expect(prefs.setSound).toHaveBeenCalledWith("ding"); expect(prefs.setDeleteAfter).toHaveBeenCalledWith(3600); expect(prefs.setMinPriority).toHaveBeenCalledWith(3); diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index e77fd3cb..2d2a0808 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -6,16 +6,43 @@ export const THEME = { SYSTEM: "system", }; +// How the date component is displayed. SYSTEM follows the browser's locale (i.e. the OS region/ +// format settings, not the chosen UI translation language); ISO8601 forces YYYY-MM-DD; DMY and MDY +// force the day/month order regardless of locale (e.g. for an English UI that defaults to US order). +export const DATE_FORMAT = { + SYSTEM: "system", + ISO8601: "iso8601", + DMY: "dmy", + DMY_DOT: "dmy_dot", + MDY: "mdy", +}; + +// How the time component (clock) is displayed. Orthogonal to DATE_FORMAT and to the UI language -- +// SYSTEM lets the browser locale decide, H12/H24 force a 12- or 24-hour clock. ISO 8601 dates are +// always 24-hour, so this setting is ignored in that mode. +export const TIME_FORMAT = { + SYSTEM: "system", + H12: "12h", + H24: "24h", +}; + // Default values the getters return when a pref is unset; also used by PrefCache (PrefCache.jsx). export const PREF_DEFAULTS = { sound: "ding", minPriority: 1, deleteAfter: 604800, // one week theme: THEME.SYSTEM, + dateFormat: DATE_FORMAT.SYSTEM, + timeFormat: TIME_FORMAT.SYSTEM, webPushEnabled: false, }; -class Prefs { +// Guards against values the current build doesn't know about (e.g. a pref synced from a newer +// client, or a renamed enum). An unknown value would leave the Settings - {t("prefs_appearance_theme_system")} + + {t("prefs_system_default")} + {prefersDarkMode ? t("prefs_appearance_theme_dark") : t("prefs_appearance_theme_light")} + {t("prefs_appearance_theme_dark")} {t("prefs_appearance_theme_light")} @@ -249,6 +261,84 @@ const Theme = () => { ); }; +// An unambiguous sample (April 26, 2:30 PM, current year) used for each option's example: the day +// (26) is too large to be a month, so the day/month order is unmistakable, and 14:30 distinguishes +// the 12h/24h clock. Using today's date could be ambiguous (e.g. when the day could pass for a month). +const EXAMPLE_TIMESTAMP = Math.round(new Date(new Date().getFullYear(), 3, 26, 14, 30).getTime() / 1000); + +const DateFormat = () => { + const { t } = useTranslation(); + const labelId = "prefDateFormat"; + const { dateFormat } = usePrefCache(); + const handleChange = async (ev) => { + await prefs.setDateFormat(ev.target.value); + await maybeUpdateAccountSettings({ date_format: ev.target.value }); + }; + + return ( + + + + + + ); +}; + +const TimeFormat = () => { + const { t } = useTranslation(); + const labelId = "prefTimeFormat"; + const { timeFormat, dateFormat } = usePrefCache(); + // ISO 8601 dates are always 24-hour, so the clock choice is fixed while it's selected. Disable the + // control and show 24-hour, but leave the stored pref untouched so it returns for other date formats. + const fixedTo24h = dateFormat === DATE_FORMAT.ISO8601; + const handleChange = async (ev) => { + await prefs.setTimeFormat(ev.target.value); + await maybeUpdateAccountSettings({ time_format: ev.target.value }); + }; + + return ( + + + + + + ); +}; + const WebPushEnabled = () => { const { t } = useTranslation(); const labelId = "prefWebPushEnabled"; @@ -518,6 +608,8 @@ const Appearance = () => { + + diff --git a/web/src/components/SubscriptionPopup.jsx b/web/src/components/SubscriptionPopup.jsx index a2bd27cc..4390095f 100644 --- a/web/src/components/SubscriptionPopup.jsx +++ b/web/src/components/SubscriptionPopup.jsx @@ -35,14 +35,16 @@ import accountApi, { Role } from "../app/AccountApi"; import session from "../app/Session"; import routes from "./routes"; import PopupMenu from "./PopupMenu"; -import { formatShortDateTime, shuffle } from "../app/utils"; +import { formatDateTime, shuffle } from "../app/utils"; import api from "../app/Api"; import AccountContext from "./AccountContext"; +import { usePrefCache } from "./PrefCache"; import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; import { UnauthorizedError } from "../app/errors"; export const SubscriptionPopup = (props) => { const { t } = useTranslation(); + const { dateFormat, timeFormat } = usePrefCache(); const { account } = useContext(AccountContext); const navigate = useNavigate(); const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false); @@ -115,15 +117,17 @@ export const SubscriptionPopup = (props) => { ])[0]; const nowSeconds = Math.round(Date.now() / 1000); const message = shuffle([ - `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime( + `Hello friend, this is a test notification from ntfy web. It's ${formatDateTime( nowSeconds, - "en-US" + dateFormat, + timeFormat )} right now. Is that early or late?`, `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, - `Alright then, it's ${formatShortDateTime( + `Alright then, it's ${formatDateTime( nowSeconds, - "en-US" + dateFormat, + timeFormat )} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, diff --git a/web/src/components/UpgradeDialog.jsx b/web/src/components/UpgradeDialog.jsx index 0517b821..319aa578 100644 --- a/web/src/components/UpgradeDialog.jsx +++ b/web/src/components/UpgradeDialog.jsx @@ -28,8 +28,9 @@ import Check from "@mui/icons-material/Check"; import Close from "@mui/icons-material/Close"; import { NavLink } from "react-router-dom"; import { UnauthorizedError } from "../app/errors"; -import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils"; +import { formatBytes, formatNumber, formatPrice, formatDate } from "../app/utils"; import AccountContext from "./AccountContext"; +import { usePrefCache } from "./PrefCache"; import routes from "./routes"; import session from "../app/Session"; import accountApi, { SubscriptionInterval } from "../app/AccountApi"; @@ -63,7 +64,8 @@ const Banner = { const UpgradeDialog = (props) => { const theme = useTheme(); - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); + const { dateFormat } = usePrefCache(); const { account } = useContext(AccountContext); // May be undefined! const [error, setError] = useState(""); const [tiers, setTiers] = useState(null); @@ -234,7 +236,7 @@ const UpgradeDialog = (props) => {