Additional descriptions for settings (#203), URL validation (#204)

This commit is contained in:
Philipp Heckel 2022-04-10 15:13:12 -04:00
parent 9c3f5929c7
commit 136883fd94
5 changed files with 123 additions and 46 deletions

View file

@ -24,8 +24,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Bugs:** **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: 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):** **Translations (web app):**

View file

@ -33,7 +33,7 @@
"notifications_none_for_any_title": "You haven't received any notifications.", "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_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_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_example": "Example",
"notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.", "notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.",
"notifications_loading": "Loading notifications …", "notifications_loading": "Loading notifications …",
@ -103,8 +103,13 @@
"subscribe_dialog_error_user_anonymous": "anonymous", "subscribe_dialog_error_user_anonymous": "anonymous",
"prefs_notifications_title": "Notifications", "prefs_notifications_title": "Notifications",
"prefs_notifications_sound_title": "Notification sound", "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_sound_no_sound": "No sound",
"prefs_notifications_min_priority_title": "Minimum priority", "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_any": "Any priority",
"prefs_notifications_min_priority_low_and_higher": "Low priority and higher", "prefs_notifications_min_priority_low_and_higher": "Low priority and higher",
"prefs_notifications_min_priority_default_and_higher": "Default 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_day": "After one day",
"prefs_notifications_delete_after_one_week": "After one week", "prefs_notifications_delete_after_one_week": "After one week",
"prefs_notifications_delete_after_one_month": "After one month", "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_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_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", "prefs_users_add_button": "Add user",
@ -131,6 +141,11 @@
"prefs_users_dialog_button_save": "Save", "prefs_users_dialog_button_save": "Save",
"prefs_appearance_title": "Appearance", "prefs_appearance_title": "Appearance",
"prefs_appearance_language_title": "Language", "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_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_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", "error_boundary_button_copy_stack_trace": "Copy stack trace",

View file

@ -24,7 +24,7 @@ export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
export const expandSecureUrl = (url) => `https://${url}`; export const expandSecureUrl = (url) => `https://${url}`;
export const validUrl = (url) => { export const validUrl = (url) => {
return url.match(/^https?:\/\//); return url.match(/^https?:\/\/.+/);
} }
export const validTopic = (topic) => { export const validTopic = (topic) => {
@ -153,17 +153,38 @@ export const openUrl = (url) => {
}; };
export const sounds = { export const sounds = {
"beep": beep, "ding": {
"juntos": juntos, file: ding,
"pristine": pristine, label: "Ding"
"ding": ding, },
"dadum": dadum, "juntos": {
"pop": pop, file: juntos,
"pop-swoosh": popSwoosh 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) => { export const playSound = async (id) => {
const audio = new Audio(sounds[sound]); const audio = new Audio(sounds[id].file);
return audio.play(); return audio.play();
}; };

View file

@ -389,7 +389,9 @@ const NoSubscriptions = () => {
{t("notifications_no_subscriptions_title")} {t("notifications_no_subscriptions_title")}
</Typography> </Typography>
<Paragraph> <Paragraph>
{t("notifications_no_subscriptions_description")} {t("notifications_no_subscriptions_description", {
linktext: t("nav_button_subscribe")
})}
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
<ForMoreDetails/> <ForMoreDetails/>

View file

@ -32,8 +32,13 @@ import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import userManager from "../app/UserManager"; 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 {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 = () => { const Preferences = () => {
return ( return (
@ -51,7 +56,7 @@ const Notifications = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Card sx={{p: 3}}> <Card sx={{p: 3}}>
<Typography variant="h5"> <Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_notifications_title")} {t("prefs_notifications_title")}
</Typography> </Typography>
<PrefGroup> <PrefGroup>
@ -72,19 +77,19 @@ const Sound = () => {
if (!sound) { if (!sound) {
return null; // While loading 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 ( return (
<Pref title={t("prefs_notifications_sound_title")}> <Pref title={t("prefs_notifications_sound_title")} description={description}>
<div style={{ display: 'flex', width: '100%' }}> <div style={{ display: 'flex', width: '100%' }}>
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}> <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
<Select value={sound} onChange={handleChange}> <Select value={sound} onChange={handleChange}>
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem> <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
<MenuItem value={"ding"}>Ding</MenuItem> {Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</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>
</Select> </Select>
</FormControl> </FormControl>
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"}> <IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
@ -104,8 +109,26 @@ const MinPriority = () => {
if (!minPriority) { if (!minPriority) {
return null; // While loading 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 ( 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 }}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={minPriority} onChange={handleChange}> <Select value={minPriority} onChange={handleChange}>
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem> <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
@ -125,11 +148,20 @@ const DeleteAfter = () => {
const handleChange = async (ev) => { const handleChange = async (ev) => {
await prefs.setDeleteAfter(ev.target.value); await prefs.setDeleteAfter(ev.target.value);
} }
if (!deleteAfter) { if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
return null; // While loading 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 ( 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 }}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={deleteAfter} onChange={handleChange}> <Select value={deleteAfter} onChange={handleChange}>
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem> <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
@ -145,10 +177,7 @@ const DeleteAfter = () => {
const PrefGroup = (props) => { const PrefGroup = (props) => {
return ( return (
<div style={{ <div>
display: 'flex',
flexWrap: 'wrap'
}}>
{props.children} {props.children}
</div> </div>
) )
@ -156,26 +185,31 @@ const PrefGroup = (props) => {
const Pref = (props) => { const Pref = (props) => {
return ( return (
<>
<div style={{ <div style={{
flex: '1 0 30%', display: "flex",
display: 'inline-flex', flexDirection: "row",
flexDirection: 'column', marginTop: "10px",
minHeight: '60px', marginBottom: "20px",
justifyContent: 'center'
}}> }}>
<b>{props.title}</b> <div style={{
flex: '1 0 40%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
paddingRight: '30px'
}}>
<div><b>{props.title}</b></div>
{props.description && <div><em>{props.description}</em></div>}
</div> </div>
<div style={{ <div style={{
flex: '1 0 calc(70% - 50px)', flex: '1 0 calc(60% - 50px)',
display: 'inline-flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: '60px',
justifyContent: 'center' justifyContent: 'center'
}}> }}>
{props.children} {props.children}
</div> </div>
</> </div>
); );
}; };
@ -202,8 +236,8 @@ const Users = () => {
}; };
return ( return (
<Card sx={{ padding: 1 }}> <Card sx={{ padding: 1 }}>
<CardContent> <CardContent sx={{ paddingBottom: 1 }}>
<Typography variant="h5"> <Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_users_title")} {t("prefs_users_title")}
</Typography> </Typography>
<Paragraph> <Paragraph>
@ -260,7 +294,7 @@ const UserTable = (props) => {
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <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>{t("prefs_users_table_base_url_header")}</TableCell>
<TableCell/> <TableCell/>
</TableRow> </TableRow>
@ -271,7 +305,7 @@ const UserTable = (props) => {
key={user.baseUrl} key={user.baseUrl}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} 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>{user.baseUrl}</TableCell>
<TableCell align="right"> <TableCell align="right">
<IconButton onClick={() => handleEditClick(user)}> <IconButton onClick={() => handleEditClick(user)}>
@ -307,8 +341,12 @@ const UserDialog = (props) => {
if (editMode) { if (editMode) {
return username.length > 0 && password.length > 0; return username.length > 0 && password.length > 0;
} }
const baseUrlValid = validUrl(baseUrl);
const baseUrlExists = props.users?.map(user => user.baseUrl).includes(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 () => { const handleSubmit = async () => {
props.onSubmit({ props.onSubmit({
@ -373,7 +411,7 @@ const Appearance = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Card sx={{p: 3}}> <Card sx={{p: 3}}>
<Typography variant="h5"> <Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_appearance_title")} {t("prefs_appearance_title")}
</Typography> </Typography>
<PrefGroup> <PrefGroup>