parent
9c3f5929c7
commit
136883fd94
|
@ -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):**
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue