diff --git a/docs/releases.md b/docs/releases.md index 85b33a13..91d32dfd 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -24,8 +24,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bugs:** -* Web app: English language strings fixes ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov)) +* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov)) * Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis)) +* Web app: basic URL validation in user management ([#204](https://github.com/binwiederhier/ntfy/issues/204), thanks to [@cmeis](https://github.com/cmeis)) **Translations (web app):** diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index a2dca266..a27ff867 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -33,7 +33,7 @@ "notifications_none_for_any_title": "You haven't received any notifications.", "notifications_none_for_any_description": "To send notifications to a topic, simply PUT or POST to the topic URL. Here's an example using one of your topics.", "notifications_no_subscriptions_title": "It looks like you don't have any subscriptions yet.", - "notifications_no_subscriptions_description": "Click the \"Add subscription\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", + "notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", "notifications_example": "Example", "notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.", "notifications_loading": "Loading notifications …", @@ -103,8 +103,13 @@ "subscribe_dialog_error_user_anonymous": "anonymous", "prefs_notifications_title": "Notifications", "prefs_notifications_sound_title": "Notification sound", + "prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive", + "prefs_notifications_sound_description_some": "Notifications play the {{sound}} sound when they arrive", "prefs_notifications_sound_no_sound": "No sound", "prefs_notifications_min_priority_title": "Minimum priority", + "prefs_notifications_min_priority_description_any": "Showing all notifications, regardless of priority", + "prefs_notifications_min_priority_description_x_or_higher": "Show notifications if priority is {{number}} ({{name}}) or above", + "prefs_notifications_min_priority_description_max": "Show notifications if priority is 5 (max)", "prefs_notifications_min_priority_any": "Any priority", "prefs_notifications_min_priority_low_and_higher": "Low priority and higher", "prefs_notifications_min_priority_default_and_higher": "Default priority and higher", @@ -116,6 +121,11 @@ "prefs_notifications_delete_after_one_day": "After one day", "prefs_notifications_delete_after_one_week": "After one week", "prefs_notifications_delete_after_one_month": "After one month", + "prefs_notifications_delete_after_never_description": "Notifications are never auto-deleted", + "prefs_notifications_delete_after_three_hours_description": "Notifications are auto-deleted after three hours", + "prefs_notifications_delete_after_one_day_description": "Notifications are auto-deleted after one day", + "prefs_notifications_delete_after_one_week_description": "Notifications are auto-deleted after one week", + "prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month", "prefs_users_title": "Manage users", "prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.", "prefs_users_add_button": "Add user", @@ -131,6 +141,11 @@ "prefs_users_dialog_button_save": "Save", "prefs_appearance_title": "Appearance", "prefs_appearance_language_title": "Language", + "priority_min": "min", + "priority_low": "low", + "priority_default": "default", + "priority_high": "high", + "priority_max": "max", "error_boundary_title": "Oh no, ntfy crashed", "error_boundary_description": "This should obviously not happen. Very sorry about this.<br/>If you have a minute, please <githubLink>report this on GitHub</githubLink>, or let us know via <discordLink>Discord</discordLink> or <matrixLink>Matrix</matrixLink>.", "error_boundary_button_copy_stack_trace": "Copy stack trace", diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 2f04dfd8..adba2e9f 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -24,7 +24,7 @@ export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; export const expandSecureUrl = (url) => `https://${url}`; export const validUrl = (url) => { - return url.match(/^https?:\/\//); + return url.match(/^https?:\/\/.+/); } export const validTopic = (topic) => { @@ -153,17 +153,38 @@ export const openUrl = (url) => { }; export const sounds = { - "beep": beep, - "juntos": juntos, - "pristine": pristine, - "ding": ding, - "dadum": dadum, - "pop": pop, - "pop-swoosh": popSwoosh + "ding": { + file: ding, + label: "Ding" + }, + "juntos": { + file: juntos, + label: "Juntos" + }, + "pristine": { + file: pristine, + label: "Pristine" + }, + "dadum": { + file: dadum, + label: "Dadum" + }, + "pop": { + file: pop, + label: "Pop" + }, + "pop-swoosh": { + file: popSwoosh, + label: "Pop swoosh" + }, + "beep": { + file: beep, + label: "Beep" + } }; -export const playSound = async (sound) => { - const audio = new Audio(sounds[sound]); +export const playSound = async (id) => { + const audio = new Audio(sounds[id].file); return audio.play(); }; diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index eb338803..a412e29e 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -389,7 +389,9 @@ const NoSubscriptions = () => { {t("notifications_no_subscriptions_title")} </Typography> <Paragraph> - {t("notifications_no_subscriptions_description")} + {t("notifications_no_subscriptions_description", { + linktext: t("nav_button_subscribe") + })} </Paragraph> <Paragraph> <ForMoreDetails/> diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index e6f28194..11b688e5 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -32,8 +32,13 @@ import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import userManager from "../app/UserManager"; -import {playSound, shuffle} from "../app/utils"; +import {playSound, shuffle, sounds, validUrl} from "../app/utils"; import {useTranslation} from "react-i18next"; +import priority1 from "../img/priority-1.svg"; +import priority2 from "../img/priority-2.svg"; +import priority3 from "../img/priority-3.svg"; +import priority4 from "../img/priority-4.svg"; +import priority5 from "../img/priority-5.svg"; const Preferences = () => { return ( @@ -51,7 +56,7 @@ const Notifications = () => { const { t } = useTranslation(); return ( <Card sx={{p: 3}}> - <Typography variant="h5"> + <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_notifications_title")} </Typography> <PrefGroup> @@ -72,19 +77,19 @@ const Sound = () => { if (!sound) { return null; // While loading } + let description; + if (sound === "none") { + description = t("prefs_notifications_sound_description_none"); + } else { + description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label }); + } return ( - <Pref title={t("prefs_notifications_sound_title")}> + <Pref title={t("prefs_notifications_sound_title")} description={description}> <div style={{ display: 'flex', width: '100%' }}> <FormControl fullWidth variant="standard" sx={{ margin: 1 }}> <Select value={sound} onChange={handleChange}> <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem> - <MenuItem value={"ding"}>Ding</MenuItem> - <MenuItem value={"juntos"}>Juntos</MenuItem> - <MenuItem value={"pristine"}>Pristine</MenuItem> - <MenuItem value={"dadum"}>Dadum</MenuItem> - <MenuItem value={"pop"}>Pop</MenuItem> - <MenuItem value={"pop-swoosh"}>Pop swoosh</MenuItem> - <MenuItem value={"beep"}>Beep</MenuItem> + {Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)} </Select> </FormControl> <IconButton onClick={() => playSound(sound)} disabled={sound === "none"}> @@ -104,8 +109,26 @@ const MinPriority = () => { if (!minPriority) { return null; // While loading } + const priorities = { + 1: t("priority_min"), + 2: t("priority_low"), + 3: t("priority_default"), + 4: t("priority_high"), + 5: t("priority_max") + } + let description; + if (minPriority === 1) { + description = t("prefs_notifications_min_priority_description_any"); + } else if (minPriority === 5) { + description = t("prefs_notifications_min_priority_description_max"); + } else { + description = t("prefs_notifications_min_priority_description_x_or_higher", { + number: minPriority, + name: priorities[minPriority] + }); + } return ( - <Pref title={t("prefs_notifications_min_priority_title")}> + <Pref title={t("prefs_notifications_min_priority_title")} description={description}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}> <Select value={minPriority} onChange={handleChange}> <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem> @@ -125,11 +148,20 @@ const DeleteAfter = () => { const handleChange = async (ev) => { await prefs.setDeleteAfter(ev.target.value); } - if (!deleteAfter) { + if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0" return null; // While loading } + const description = (() => { + switch (deleteAfter) { + case 0: return t("prefs_notifications_delete_after_never_description"); + case 10800: return t("prefs_notifications_delete_after_three_hours_description"); + case 86400: return t("prefs_notifications_delete_after_one_day_description"); + case 604800: return t("prefs_notifications_delete_after_one_week_description"); + case 2592000: return t("prefs_notifications_delete_after_one_month_description"); + } + })(); return ( - <Pref title={t("prefs_notifications_delete_after_title")}> + <Pref title={t("prefs_notifications_delete_after_title")} description={description}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}> <Select value={deleteAfter} onChange={handleChange}> <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem> @@ -145,10 +177,7 @@ const DeleteAfter = () => { const PrefGroup = (props) => { return ( - <div style={{ - display: 'flex', - flexWrap: 'wrap' - }}> + <div> {props.children} </div> ) @@ -156,26 +185,31 @@ const PrefGroup = (props) => { const Pref = (props) => { return ( - <> + <div style={{ + display: "flex", + flexDirection: "row", + marginTop: "10px", + marginBottom: "20px", + }}> <div style={{ - flex: '1 0 30%', - display: 'inline-flex', + flex: '1 0 40%', + display: 'flex', flexDirection: 'column', - minHeight: '60px', - justifyContent: 'center' + justifyContent: 'center', + paddingRight: '30px' }}> - <b>{props.title}</b> + <div><b>{props.title}</b></div> + {props.description && <div><em>{props.description}</em></div>} </div> <div style={{ - flex: '1 0 calc(70% - 50px)', - display: 'inline-flex', + flex: '1 0 calc(60% - 50px)', + display: 'flex', flexDirection: 'column', - minHeight: '60px', justifyContent: 'center' }}> {props.children} </div> - </> + </div> ); }; @@ -202,8 +236,8 @@ const Users = () => { }; return ( <Card sx={{ padding: 1 }}> - <CardContent> - <Typography variant="h5"> + <CardContent sx={{ paddingBottom: 1 }}> + <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_users_title")} </Typography> <Paragraph> @@ -260,7 +294,7 @@ const UserTable = (props) => { <Table size="small"> <TableHead> <TableRow> - <TableCell>{t("prefs_users_table_user_header")}</TableCell> + <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell> <TableCell>{t("prefs_users_table_base_url_header")}</TableCell> <TableCell/> </TableRow> @@ -271,7 +305,7 @@ const UserTable = (props) => { key={user.baseUrl} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > - <TableCell component="th" scope="row">{user.username}</TableCell> + <TableCell component="th" scope="row" sx={{paddingLeft: 0}}>{user.username}</TableCell> <TableCell>{user.baseUrl}</TableCell> <TableCell align="right"> <IconButton onClick={() => handleEditClick(user)}> @@ -307,8 +341,12 @@ const UserDialog = (props) => { if (editMode) { return username.length > 0 && password.length > 0; } + const baseUrlValid = validUrl(baseUrl); const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl); - return !baseUrlExists && username.length > 0 && password.length > 0; + return baseUrlValid + && !baseUrlExists + && username.length > 0 + && password.length > 0; })(); const handleSubmit = async () => { props.onSubmit({ @@ -373,7 +411,7 @@ const Appearance = () => { const { t } = useTranslation(); return ( <Card sx={{p: 3}}> - <Typography variant="h5"> + <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_appearance_title")} </Typography> <PrefGroup>