Finish web app translation

This commit is contained in:
Philipp Heckel 2022-04-08 10:44:35 -04:00
parent 893701c07b
commit 30726144b8
10 changed files with 272 additions and 132 deletions

View file

@ -1,4 +1,10 @@
{ {
"action_bar_settings": "Settings",
"action_bar_send_test_notification": "Send test notification",
"action_bar_clear_notifications": "Clear all notifications",
"action_bar_unsubscribe": "Unsubscribe",
"message_bar_type_message": "Type a message here",
"message_bar_error_publishing": "Error publishing message",
"nav_topics_title": "Subscribed topics", "nav_topics_title": "Subscribed topics",
"nav_button_all_notifications": "All notifications", "nav_button_all_notifications": "All notifications",
"nav_button_settings": "Settings", "nav_button_settings": "Settings",
@ -31,5 +37,103 @@
"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 ...",
"emoji_picker_search_placeholder": "Search emoji" "publish_dialog_title_topic": "Publish to {{topic}}",
"publish_dialog_title_no_topic": "Publish message",
"publish_dialog_progress_uploading": "Uploading ...",
"publish_dialog_progress_uploading_detail": "Uploading {{loaded}}/{{total}} ({{percent}}%) ...",
"publish_dialog_message_published": "Message published",
"publish_dialog_attachment_limits_file_and_quota_reached": "exceeds {{fileSizeLimit}} file limit and quota, {{remainingBytes}} remaining",
"publish_dialog_attachment_limits_file_reached": "exceeds {{fileSizeLimit}} file limit",
"publish_dialog_attachment_limits_quota_reached": "exceeds quota,{{remainingBytes}} remaining",
"publish_dialog_priority_min": "Min. priority",
"publish_dialog_priority_low": "Low priority",
"publish_dialog_priority_default": "Default priority",
"publish_dialog_priority_high": "High priority",
"publish_dialog_priority_max": "Max. priority",
"publish_dialog_base_url_label": "Server URL",
"publish_dialog_base_url_placeholder": "Server URL, e.g. https://example.com",
"publish_dialog_topic_label": "Topic name",
"publish_dialog_topic_placeholder": "Topic name, e.g. phil_alerts",
"publish_dialog_title_label": "Title",
"publish_dialog_title_placeholder": "Notification title, e.g. Disk space alert",
"publish_dialog_message_label": "Message",
"publish_dialog_message_placeholder": "Type a message here",
"publish_dialog_tags_label": "Tags",
"publish_dialog_tags_placeholder": "Comma-separated list of tags, e.g. warning, srv1-backup",
"publish_dialog_priority_label": "Priority",
"publish_dialog_click_label": "Click URL",
"publish_dialog_click_placeholder": "URL that is opened when notification is clicked",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Address to forward the message to, e.g. phil@example.com",
"publish_dialog_attach_label": "Attachment URL",
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_label": "Filename",
"publish_dialog_filename_placeholder": "Attachment filename",
"publish_dialog_delay_label": "Delay",
"publish_dialog_delay_placeholder": "Delay delivery, e.g. 1649029748, 30m, or tomorrow, 9am",
"publish_dialog_other_features": "Other features:",
"publish_dialog_chip_click_label": "Click URL",
"publish_dialog_chip_email_label": "Forward to email",
"publish_dialog_chip_attach_url_label": "Attach file by URL",
"publish_dialog_chip_attach_file_label": "Attach local file",
"publish_dialog_chip_delay_label": "Delay delivery",
"publish_dialog_chip_topic_label": "Change topic",
"publish_dialog_details_examples_description": "For examples and a detailed description of all send features, please refer to the <docsLink>documentation</docsLink>.",
"publish_dialog_button_cancel_sending": "Cancel sending",
"publish_dialog_button_cancel": "Cancel",
"publish_dialog_button_send": "Send",
"publish_dialog_checkbox_publish_another": "Publish another",
"publish_dialog_attached_file_title": "Attached file:",
"publish_dialog_attached_file_filename_placeholder": "Attachment filename",
"publish_dialog_drop_file_here": "Drop file here",
"emoji_picker_search_placeholder": "Search emoji",
"subscribe_dialog_subscribe_title": "Subscribe to topic",
"subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.",
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Use another server",
"subscribe_dialog_subscribe_button_cancel": "Cancel",
"subscribe_dialog_subscribe_button_subscribe": "Subscribe",
"subscribe_dialog_login_title": "Login required",
"subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.",
"subscribe_dialog_login_username_label": "Username, e.g. phil",
"subscribe_dialog_login_password_label": "Password",
"subscribe_dialog_login_button_back": "Back",
"subscribe_dialog_login_button_login": "Login",
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
"subscribe_dialog_error_user_anonymous": "anonymous",
"prefs_notifications_title": "Notifications",
"prefs_notifications_sound_title": "Notification sound",
"prefs_notifications_sound_no_sound": "No sound",
"prefs_notifications_min_priority_title": "Minimum priority",
"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",
"prefs_notifications_min_priority_high_and_higher": "High priority and higher",
"prefs_notifications_min_priority_max_only": "Only max priority",
"prefs_notifications_delete_after_title": "Delete notifications",
"prefs_notifications_delete_after_never": "Never",
"prefs_notifications_delete_after_three_hours": "After three hours",
"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_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",
"prefs_users_table_user_header": "User",
"prefs_users_table_base_url_header": "Service URL",
"prefs_users_dialog_title_add": "Add user",
"prefs_users_dialog_title_edit": "Edit user",
"prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh",
"prefs_users_dialog_username_label": "Username, e.g. phil",
"prefs_users_dialog_password_label": "Password",
"prefs_users_dialog_button_cancel": "Cancel",
"prefs_users_dialog_button_add": "Add",
"prefs_users_dialog_button_save": "Save",
"prefs_appearance_title": "Appearance",
"prefs_appearance_language_title": "Language",
"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",
"error_boundary_stack_trace": "Stack trace",
"error_boundary_gathering_info": "Gather more info ..."
} }

View file

@ -71,7 +71,7 @@ class Connection {
this.onStateChanged(this.subscriptionId, ConnectionState.Connecting); this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
} }
}; };
this.ws.onerror = (event) => { this.ws.onerrgoogle.ccor = (event) => {
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event); console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event);
}; };
} }

View file

@ -22,14 +22,16 @@ import api from "../app/Api";
import routes from "./routes"; import routes from "./routes";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import logo from "../img/ntfy.svg"; import logo from "../img/ntfy.svg";
import {useTranslation} from "react-i18next";
const ActionBar = (props) => { const ActionBar = (props) => {
const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
let title = "ntfy"; let title = "ntfy";
if (props.selected) { if (props.selected) {
title = topicShortUrl(props.selected.baseUrl, props.selected.topic); title = topicShortUrl(props.selected.baseUrl, props.selected.topic);
} else if (location.pathname === "/settings") { } else if (location.pathname === "/settings") {
title = "Settings"; title = t("action_bar_settings");
} }
return ( return (
<AppBar position="fixed" sx={{ <AppBar position="fixed" sx={{
@ -66,6 +68,7 @@ const ActionBar = (props) => {
// Originally from https://mui.com/components/menus/#MenuListComposition.js // Originally from https://mui.com/components/menus/#MenuListComposition.js
const SettingsIcons = (props) => { const SettingsIcons = (props) => {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const anchorRef = useRef(null); const anchorRef = useRef(null);
@ -189,9 +192,9 @@ const SettingsIcons = (props) => {
<Paper> <Paper>
<ClickAwayListener onClickAway={handleClose}> <ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}> <MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}>
<MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem> <MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem>
<MenuItem onClick={handleClearAll}>Clear all notifications</MenuItem> <MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem>
<MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem> <MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem>
</MenuList> </MenuList>
</ClickAwayListener> </ClickAwayListener>
</Paper> </Paper>

View file

@ -19,7 +19,7 @@ import {expandUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks";
import SendDialog from "./SendDialog"; import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging"; import Messaging from "./Messaging";
import "./i18n"; // Translations! import "./i18n"; // Translations!
import {Backdrop, CircularProgress} from "@mui/material"; import {Backdrop, CircularProgress} from "@mui/material";
@ -91,7 +91,7 @@ const Layout = () => {
mobileDrawerOpen={mobileDrawerOpen} mobileDrawerOpen={mobileDrawerOpen}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
onNotificationGranted={setNotificationsGranted} onNotificationGranted={setNotificationsGranted}
onPublishMessageClick={() => setSendDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT)} onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
/> />
<Main> <Main>
<Toolbar/> <Toolbar/>

View file

@ -1,9 +1,10 @@
import * as React from "react"; import * as React from "react";
import StackTrace from "stacktrace-js"; import StackTrace from "stacktrace-js";
import {CircularProgress} from "@mui/material"; import {CircularProgress, Link} from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import {Trans, withTranslation} from "react-i18next";
class ErrorBoundary extends React.Component { class ErrorBoundaryImpl extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -45,22 +46,28 @@ class ErrorBoundary extends React.Component {
} }
render() { render() {
const { t } = this.props;
if (this.state.error) { if (this.state.error) {
return ( return (
<div style={{margin: '20px'}}> <div style={{margin: '20px'}}>
<h2>Oh no, ntfy crashed 😮</h2> <h2>{t("error_boundary_title")} 😮</h2>
<p> <p>
This should obviously not happen. Very sorry about this.<br/> <Trans
If you have a minute, please <a href="https://github.com/binwiederhier/ntfy/issues">report this on GitHub</a>, or let us i18nKey="error_boundary_description"
know via <a href="https://discord.gg/cT7ECsZj9w">Discord</a> or <a href="https://matrix.to/#/#ntfy:matrix.org">Matrix</a>. components={{
githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues"/>,
discordLink: <Link href="https://discord.gg/cT7ECsZj9w"/>,
matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org"/>
}}
/>
</p> </p>
<p> <p>
<Button variant="outlined" onClick={() => this.copyStack()}>Copy stack trace</Button> <Button variant="outlined" onClick={() => this.copyStack()}>{t("error_boundary_button_copy_stack_trace")}</Button>
</p> </p>
<h3>Stack trace</h3> <h3>{t("error_boundary_stack_trace")}</h3>
{this.state.niceStack {this.state.niceStack
? <pre>{this.state.niceStack}</pre> ? <pre>{this.state.niceStack}</pre>
: <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> Gather more info ...</>} : <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> {t("error_boundary_gathering_info")}</>}
<pre>{this.state.originalStack}</pre> <pre>{this.state.originalStack}</pre>
</div> </div>
); );
@ -69,4 +76,5 @@ class ErrorBoundary extends React.Component {
} }
} }
const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t
export default ErrorBoundary; export default ErrorBoundary;

View file

@ -1,15 +1,15 @@
import * as React from 'react'; import * as React from 'react';
import {useState} from 'react'; import {useState} from 'react';
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import {topicUrl} from "../app/utils";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import SendIcon from "@mui/icons-material/Send"; import SendIcon from "@mui/icons-material/Send";
import api from "../app/Api"; import api from "../app/Api";
import SendDialog from "./SendDialog"; import PublishDialog from "./PublishDialog";
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import {Portal, Snackbar} from "@mui/material"; import {Portal, Snackbar} from "@mui/material";
import {useTranslation} from "react-i18next";
const Messaging = (props) => { const Messaging = (props) => {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
@ -19,10 +19,10 @@ const Messaging = (props) => {
const subscription = props.selected; const subscription = props.selected;
const handleOpenDialogClick = () => { const handleOpenDialogClick = () => {
props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT); props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT);
}; };
const handleSendDialogClose = () => { const handleDialogClose = () => {
props.onDialogOpenModeChange(""); props.onDialogOpenModeChange("");
setDialogKey(prev => prev+1); setDialogKey(prev => prev+1);
}; };
@ -35,21 +35,22 @@ const Messaging = (props) => {
onMessageChange={setMessage} onMessageChange={setMessage}
onOpenDialogClick={handleOpenDialogClick} onOpenDialogClick={handleOpenDialogClick}
/>} />}
<SendDialog <PublishDialog
key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
openMode={dialogOpenMode} openMode={dialogOpenMode}
baseUrl={subscription?.baseUrl ?? window.location.origin} baseUrl={subscription?.baseUrl ?? window.location.origin}
topic={subscription?.topic ?? ""} topic={subscription?.topic ?? ""}
message={message} message={message}
onClose={handleSendDialogClose} onClose={handleDialogClose}
onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG)} // Only update if not already open onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open
onResetOpenMode={() => props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT)} onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)}
/> />
</> </>
); );
} }
const MessageBar = (props) => { const MessageBar = (props) => {
const { t } = useTranslation();
const subscription = props.subscription; const subscription = props.subscription;
const [snackOpen, setSnackOpen] = useState(false); const [snackOpen, setSnackOpen] = useState(false);
const handleSendClick = async () => { const handleSendClick = async () => {
@ -80,7 +81,7 @@ const MessageBar = (props) => {
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
placeholder="Type a message here" placeholder={t("message_bar_type_message")}
type="text" type="text"
fullWidth fullWidth
variant="standard" variant="standard"
@ -101,7 +102,7 @@ const MessageBar = (props) => {
open={snackOpen} open={snackOpen}
autoHideDuration={3000} autoHideDuration={3000}
onClose={() => setSnackOpen(false)} onClose={() => setSnackOpen(false)}
message="Error publishing message" message={t("message_bar_error_publishing")}
/> />
</Portal> </Portal>
</Paper> </Paper>

View file

@ -48,10 +48,11 @@ const Preferences = () => {
}; };
const Notifications = () => { const Notifications = () => {
const { t } = useTranslation();
return ( return (
<Card sx={{p: 3}}> <Card sx={{p: 3}}>
<Typography variant="h5"> <Typography variant="h5">
Notifications {t("prefs_notifications_title")}
</Typography> </Typography>
<PrefGroup> <PrefGroup>
<Sound/> <Sound/>
@ -63,6 +64,7 @@ const Notifications = () => {
}; };
const Sound = () => { const Sound = () => {
const { t } = useTranslation();
const sound = useLiveQuery(async () => prefs.sound()); const sound = useLiveQuery(async () => prefs.sound());
const handleChange = async (ev) => { const handleChange = async (ev) => {
await prefs.setSound(ev.target.value); await prefs.setSound(ev.target.value);
@ -71,11 +73,11 @@ const Sound = () => {
return null; // While loading return null; // While loading
} }
return ( return (
<Pref title="Notification sound"> <Pref title={t("prefs_notifications_sound_title")}>
<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"}>No sound</MenuItem> <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
<MenuItem value={"ding"}>Ding</MenuItem> <MenuItem value={"ding"}>Ding</MenuItem>
<MenuItem value={"juntos"}>Juntos</MenuItem> <MenuItem value={"juntos"}>Juntos</MenuItem>
<MenuItem value={"pristine"}>Pristine</MenuItem> <MenuItem value={"pristine"}>Pristine</MenuItem>
@ -94,6 +96,7 @@ const Sound = () => {
}; };
const MinPriority = () => { const MinPriority = () => {
const { t } = useTranslation();
const minPriority = useLiveQuery(async () => prefs.minPriority()); const minPriority = useLiveQuery(async () => prefs.minPriority());
const handleChange = async (ev) => { const handleChange = async (ev) => {
await prefs.setMinPriority(ev.target.value); await prefs.setMinPriority(ev.target.value);
@ -102,14 +105,14 @@ const MinPriority = () => {
return null; // While loading return null; // While loading
} }
return ( return (
<Pref title="Minimum priority"> <Pref title={t("prefs_notifications_min_priority_title")}>
<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}>Any priority</MenuItem> <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
<MenuItem value={2}>Low priority and higher</MenuItem> <MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
<MenuItem value={3}>Default priority and higher</MenuItem> <MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
<MenuItem value={4}>High priority and higher</MenuItem> <MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem>
<MenuItem value={5}>Only max priority</MenuItem> <MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Pref> </Pref>
@ -117,6 +120,7 @@ const MinPriority = () => {
}; };
const DeleteAfter = () => { const DeleteAfter = () => {
const { t } = useTranslation();
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
const handleChange = async (ev) => { const handleChange = async (ev) => {
await prefs.setDeleteAfter(ev.target.value); await prefs.setDeleteAfter(ev.target.value);
@ -125,14 +129,14 @@ const DeleteAfter = () => {
return null; // While loading return null; // While loading
} }
return ( return (
<Pref title="Delete notifications"> <Pref title={t("prefs_notifications_delete_after_title")}>
<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}>Never</MenuItem> <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
<MenuItem value={10800}>After three hours</MenuItem> <MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
<MenuItem value={86400}>After one day</MenuItem> <MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
<MenuItem value={604800}>After one week</MenuItem> <MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem>
<MenuItem value={2592000}>After one month</MenuItem> <MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Pref> </Pref>
@ -176,6 +180,7 @@ const Pref = (props) => {
}; };
const Users = () => { const Users = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
@ -199,16 +204,15 @@ const Users = () => {
<Card sx={{ padding: 1 }}> <Card sx={{ padding: 1 }}>
<CardContent> <CardContent>
<Typography variant="h5"> <Typography variant="h5">
Manage users {t("prefs_users_title")}
</Typography> </Typography>
<Paragraph> <Paragraph>
Add/remove users for your protected topics here. Please note that username and password are {t("prefs_users_description")}
stored in the browser's local storage.
</Paragraph> </Paragraph>
{users?.length > 0 && <UserTable users={users}/>} {users?.length > 0 && <UserTable users={users}/>}
</CardContent> </CardContent>
<CardActions> <CardActions>
<Button onClick={handleAddClick}>Add user</Button> <Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
<UserDialog <UserDialog
key={`userAddDialog${dialogKey}`} key={`userAddDialog${dialogKey}`}
open={dialogOpen} open={dialogOpen}
@ -223,6 +227,7 @@ const Users = () => {
}; };
const UserTable = (props) => { const UserTable = (props) => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [dialogUser, setDialogUser] = useState(null); const [dialogUser, setDialogUser] = useState(null);
@ -255,8 +260,8 @@ const UserTable = (props) => {
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>User</TableCell> <TableCell>{t("prefs_users_table_user_header")}</TableCell>
<TableCell>Service URL</TableCell> <TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
<TableCell/> <TableCell/>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -292,6 +297,7 @@ const UserTable = (props) => {
}; };
const UserDialog = (props) => { const UserDialog = (props) => {
const { t } = useTranslation();
const [baseUrl, setBaseUrl] = useState(""); const [baseUrl, setBaseUrl] = useState("");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -320,13 +326,13 @@ const UserDialog = (props) => {
}, [editMode, props.user]); }, [editMode, props.user]);
return ( return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{editMode ? "Edit user" : "Add user"}</DialogTitle> <DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle>
<DialogContent> <DialogContent>
{!editMode && <TextField {!editMode && <TextField
autoFocus autoFocus
margin="dense" margin="dense"
id="baseUrl" id="baseUrl"
label="Service URL, e.g. https://ntfy.sh" label={t("prefs_users_dialog_base_url_label")}
value={baseUrl} value={baseUrl}
onChange={ev => setBaseUrl(ev.target.value)} onChange={ev => setBaseUrl(ev.target.value)}
type="url" type="url"
@ -337,7 +343,7 @@ const UserDialog = (props) => {
autoFocus={editMode} autoFocus={editMode}
margin="dense" margin="dense"
id="username" id="username"
label="Username, e.g. phil" label={t("prefs_users_dialog_username_label")}
value={username} value={username}
onChange={ev => setUsername(ev.target.value)} onChange={ev => setUsername(ev.target.value)}
type="text" type="text"
@ -347,7 +353,7 @@ const UserDialog = (props) => {
<TextField <TextField
margin="dense" margin="dense"
id="password" id="password"
label="Password" label={t("prefs_users_dialog_password_label")}
type="password" type="password"
value={password} value={password}
onChange={ev => setPassword(ev.target.value)} onChange={ev => setPassword(ev.target.value)}
@ -356,18 +362,19 @@ const UserDialog = (props) => {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={props.onCancel}>Cancel</Button> <Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? "Save" : "Add"}</Button> <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? t("prefs_users_dialog_button_save") : t("prefs_users_dialog_button_add")}</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}; };
const Appearance = () => { const Appearance = () => {
const { t } = useTranslation();
return ( return (
<Card sx={{p: 3}}> <Card sx={{p: 3}}>
<Typography variant="h5"> <Typography variant="h5">
Appearance {t("prefs_appearance_title")}
</Typography> </Typography>
<PrefGroup> <PrefGroup>
<Language/> <Language/>
@ -379,7 +386,7 @@ const Appearance = () => {
const Language = () => { const Language = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
return ( return (
<Pref title="Language"> <Pref title={t("prefs_appearance_language_title")}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}> <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={i18n.language} onChange={(ev) => i18n.changeLanguage(ev.target.value)}> <Select value={i18n.language} onChange={(ev) => i18n.changeLanguage(ev.target.value)}>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>

View file

@ -25,8 +25,10 @@ import DialogFooter from "./DialogFooter";
import api from "../app/Api"; import api from "../app/Api";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import EmojiPicker from "./EmojiPicker"; import EmojiPicker from "./EmojiPicker";
import {Trans, useTranslation} from "react-i18next";
const SendDialog = (props) => { const PublishDialog = (props) => {
const { t } = useTranslation();
const [baseUrl, setBaseUrl] = useState(""); const [baseUrl, setBaseUrl] = useState("");
const [topic, setTopic] = useState(""); const [topic, setTopic] = useState("");
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
@ -123,10 +125,13 @@ const SendDialog = (props) => {
const headers = maybeWithBasicAuth({}, user); const headers = maybeWithBasicAuth({}, user);
const progressFn = (ev) => { const progressFn = (ev) => {
if (ev.loaded > 0 && ev.total > 0) { if (ev.loaded > 0 && ev.total > 0) {
const percent = Math.round(ev.loaded * 100.0 / ev.total); setStatus(t("publish_dialog_progress_uploading_detail", {
setStatus(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`); loaded: formatBytes(ev.loaded),
total: formatBytes(ev.total),
percent: Math.round(ev.loaded * 100.0 / ev.total)
}));
} else { } else {
setStatus(`Uploading ...`); setStatus(t("publish_dialog_progress_uploading"));
} }
}; };
const request = api.publishXHR(url, body, headers, progressFn); const request = api.publishXHR(url, body, headers, progressFn);
@ -135,7 +140,7 @@ const SendDialog = (props) => {
if (!publishAnother) { if (!publishAnother) {
props.onClose(); props.onClose();
} else { } else {
setStatus("Message published"); setStatus(t("publish_dialog_message_published"));
setActiveRequest(null); setActiveRequest(null);
} }
} catch (e) { } catch (e) {
@ -152,11 +157,14 @@ const SendDialog = (props) => {
const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit;
const quotaReached = remainingBytes > 0 && file.size > remainingBytes; const quotaReached = remainingBytes > 0 && file.size > remainingBytes;
if (fileSizeLimitReached && quotaReached) { if (fileSizeLimitReached && quotaReached) {
return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit and quota, ${formatBytes(remainingBytes)} remaining`); return setAttachFileError(t("publish_dialog_attachment_limits_file_and_quota_reached", {
fileSizeLimit: formatBytes(fileSizeLimit),
remainingBytes: formatBytes(remainingBytes)
}));
} else if (fileSizeLimitReached) { } else if (fileSizeLimitReached) {
return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit`); return setAttachFileError(t("publish_dialog_attachment_limits_file_reached", { fileSizeLimit: formatBytes(fileSizeLimit) }));
} else if (quotaReached) { } else if (quotaReached) {
return setAttachFileError(`exceeds quota, ${formatBytes(remainingBytes)} remaining`); return setAttachFileError(t("publish_dialog_attachment_limits_quota_reached", { remainingBytes: formatBytes(remainingBytes) }));
} }
setAttachFileError(""); setAttachFileError("");
} catch (e) { } catch (e) {
@ -188,7 +196,7 @@ const SendDialog = (props) => {
const handleAttachFileDragLeave = () => { const handleAttachFileDragLeave = () => {
setDropZone(false); setDropZone(false);
if (props.openMode === SendDialog.OPEN_MODE_DRAG) { if (props.openMode === PublishDialog.OPEN_MODE_DRAG) {
props.onClose(); // Only close dialog if it was not open before dragging file in props.onClose(); // Only close dialog if it was not open before dragging file in
} }
}; };
@ -205,6 +213,14 @@ const SendDialog = (props) => {
setEmojiPickerAnchorEl(null); setEmojiPickerAnchorEl(null);
}; };
const priorities = {
1: { label: t("publish_dialog_priority_min"), file: priority1 },
2: { label: t("publish_dialog_priority_low"), file: priority2 },
3: { label: t("publish_dialog_priority_default"), file: priority3 },
4: { label: t("publish_dialog_priority_high"), file: priority4 },
5: { label: t("publish_dialog_priority_max"), file: priority5 }
};
return ( return (
<> <>
{dropZone && <DropArea {dropZone && <DropArea
@ -212,7 +228,7 @@ const SendDialog = (props) => {
onDragLeave={handleAttachFileDragLeave}/> onDragLeave={handleAttachFileDragLeave}/>
} }
<Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}> <Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{(baseUrl && topic) ? `Publish to ${topicShortUrl(baseUrl, topic)}` : "Publish message"}</DialogTitle> <DialogTitle>{(baseUrl && topic) ? t("publish_dialog_title_topic", { topic: topicShortUrl(baseUrl, topic) }) : t("publish_dialog_title_no_topic")}</DialogTitle>
<DialogContent> <DialogContent>
{dropZone && <DropBox/>} {dropZone && <DropBox/>}
{showTopicUrl && {showTopicUrl &&
@ -223,8 +239,8 @@ const SendDialog = (props) => {
}}> }}>
<TextField <TextField
margin="dense" margin="dense"
label="Server URL" label={t("publish_dialog_base_url_label")}
placeholder="Server URL, e.g. https://example.com" placeholder={t("publish_dialog_base_url_placeholder")}
value={baseUrl} value={baseUrl}
onChange={ev => setBaseUrl(ev.target.value)} onChange={ev => setBaseUrl(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -234,8 +250,8 @@ const SendDialog = (props) => {
/> />
<TextField <TextField
margin="dense" margin="dense"
label="Topic" label={t("publish_dialog_topic_label")}
placeholder="Topic name, e.g. phil_alerts" placeholder={t("publish_dialog_topic_placeholder")}
value={topic} value={topic}
onChange={ev => setTopic(ev.target.value)} onChange={ev => setTopic(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -248,19 +264,19 @@ const SendDialog = (props) => {
} }
<TextField <TextField
margin="dense" margin="dense"
label="Title" label={t("publish_dialog_title_label")}
placeholder={t("publish_dialog_title_placeholder")}
value={title} value={title}
onChange={ev => setTitle(ev.target.value)} onChange={ev => setTitle(ev.target.value)}
disabled={disabled} disabled={disabled}
type="text" type="text"
fullWidth fullWidth
variant="standard" variant="standard"
placeholder="Notification title, e.g. Disk space alert"
/> />
<TextField <TextField
margin="dense" margin="dense"
label="Message" label={t("publish_dialog_message_label")}
placeholder="Type a message here" placeholder={t("publish_dialog_message_placeholder")}
value={message} value={message}
onChange={ev => setMessage(ev.target.value)} onChange={ev => setMessage(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -282,8 +298,8 @@ const SendDialog = (props) => {
</DialogIconButton> </DialogIconButton>
<TextField <TextField
margin="dense" margin="dense"
label="Tags" label={t("publish_dialog_tags_label")}
placeholder="Comma-separated list of tags, e.g. warning, srv1-backup" placeholder={t("publish_dialog_tags_placeholder")}
value={tags} value={tags}
onChange={ev => setTags(ev.target.value)} onChange={ev => setTags(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -298,7 +314,7 @@ const SendDialog = (props) => {
> >
<InputLabel/> <InputLabel/>
<Select <Select
label="Priority" label={t("publish_dialog_priority_label")}
margin="dense" margin="dense"
value={priority} value={priority}
onChange={(ev) => setPriority(ev.target.value)} onChange={(ev) => setPriority(ev.target.value)}
@ -322,8 +338,8 @@ const SendDialog = (props) => {
}}> }}>
<TextField <TextField
margin="dense" margin="dense"
label="Click URL" label={t("publish_dialog_click_label")}
placeholder="URL that is opened when notification is clicked" placeholder={t("publish_dialog_click_placeholder")}
value={clickUrl} value={clickUrl}
onChange={ev => setClickUrl(ev.target.value)} onChange={ev => setClickUrl(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -340,8 +356,8 @@ const SendDialog = (props) => {
}}> }}>
<TextField <TextField
margin="dense" margin="dense"
label="Email" label={t("publish_dialog_email_label")}
placeholder="Address to forward the message to, e.g. phil@example.com" placeholder={t("publish_dialog_email_placeholder")}
value={email} value={email}
onChange={ev => setEmail(ev.target.value)} onChange={ev => setEmail(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -360,8 +376,8 @@ const SendDialog = (props) => {
}}> }}>
<TextField <TextField
margin="dense" margin="dense"
label="Attachment URL" label={t("publish_dialog_attach_label")}
placeholder="Attach file by URL, e.g. https://f-droid.org/F-Droid.apk" placeholder={t("publish_dialog_attach_placeholder")}
value={attachUrl} value={attachUrl}
onChange={ev => { onChange={ev => {
const url = ev.target.value; const url = ev.target.value;
@ -385,8 +401,8 @@ const SendDialog = (props) => {
/> />
<TextField <TextField
margin="dense" margin="dense"
label="Filename" label={t("publish_dialog_filename_label")}
placeholder="Attachment filename" placeholder={t("publish_dialog_filename_placeholder")}
value={filename} value={filename}
onChange={ev => { onChange={ev => {
setFilename(ev.target.value); setFilename(ev.target.value);
@ -424,8 +440,8 @@ const SendDialog = (props) => {
}}> }}>
<TextField <TextField
margin="dense" margin="dense"
label="Delay" label={t("publish_dialog_delay_label")}
placeholder="Delay delivery, e.g. 1649029748, 30m, or tomorrow, 9am" placeholder={t("publish_dialog_delay_placeholder")}
value={delay} value={delay}
onChange={ev => setDelay(ev.target.value)} onChange={ev => setDelay(ev.target.value)}
disabled={disabled} disabled={disabled}
@ -436,33 +452,37 @@ const SendDialog = (props) => {
</ClosableRow> </ClosableRow>
} }
<Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}> <Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}>
Other features: {t("publish_dialog_other_features")}
</Typography> </Typography>
<div> <div>
{!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} {!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showEmail && <Chip clickable disabled={disabled} label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>} {!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>} {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} {!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} {!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
</div> </div>
<Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}> <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
For examples and a detailed description of all send features, please <Trans
refer to the <Link href="/docs" target="_blank">documentation</Link>. i18nKey="publish_dialog_details_examples_description"
components={{
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener"/>
}}
/>
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogFooter status={status}> <DialogFooter status={status}>
{activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>} {activeRequest && <Button onClick={() => activeRequest.abort()}>{t("publish_dialog_button_cancel_sending")}</Button>}
{!activeRequest && {!activeRequest &&
<> <>
<FormControlLabel <FormControlLabel
label="Publish another" label={t("publish_dialog_checkbox_publish_another")}
sx={{marginRight: 2}} sx={{marginRight: 2}}
control={ control={
<Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} /> <Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
} /> } />
<Button onClick={props.onClose}>Cancel</Button> <Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button> <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button>
</> </>
} }
</DialogFooter> </DialogFooter>
@ -506,11 +526,12 @@ const DialogIconButton = (props) => {
}; };
const AttachmentBox = (props) => { const AttachmentBox = (props) => {
const { t } = useTranslation();
const file = props.file; const file = props.file;
return ( return (
<> <>
<Typography variant="body1" sx={{marginTop: 2}}> <Typography variant="body1" sx={{marginTop: 2}}>
Attached file: {t("publish_dialog_attached_file_title")}
</Typography> </Typography>
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
@ -523,6 +544,7 @@ const AttachmentBox = (props) => {
<ExpandingTextField <ExpandingTextField
minWidth={140} minWidth={140}
variant="body2" variant="body2"
placeholder={t("publish_dialog_attached_file_filename_placeholder")}
value={props.filename} value={props.filename}
onChange={(ev) => props.onChangeFilename(ev.target.value)} onChange={(ev) => props.onChangeFilename(ev.target.value)}
disabled={props.disabled} disabled={props.disabled}
@ -568,7 +590,7 @@ const ExpandingTextField = (props) => {
</Typography> </Typography>
<TextField <TextField
margin="dense" margin="dense"
placeholder="Attachment filename" placeholder={props.placeholder}
value={props.value} value={props.value}
onChange={props.onChange} onChange={props.onChange}
type="text" type="text"
@ -610,6 +632,7 @@ const DropArea = (props) => {
}; };
const DropBox = () => { const DropBox = () => {
const { t } = useTranslation();
return ( return (
<Box sx={{ <Box sx={{
position: 'absolute', position: 'absolute',
@ -635,21 +658,13 @@ const DropBox = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Typography variant="h5">Drop file here</Typography> <Typography variant="h5">{t("publish_dialog_drop_file_here")}</Typography>
</Box> </Box>
</Box> </Box>
); );
} }
const priorities = { PublishDialog.OPEN_MODE_DEFAULT = "default";
1: { label: "Min. priority", file: priority1 }, PublishDialog.OPEN_MODE_DRAG = "drag";
2: { label: "Low priority", file: priority2 },
3: { label: "Default priority", file: priority3 },
4: { label: "High priority", file: priority4 },
5: { label: "Max. priority", file: priority5 }
};
SendDialog.OPEN_MODE_DEFAULT = "default"; export default PublishDialog;
SendDialog.OPEN_MODE_DRAG = "drag";
export default SendDialog;

View file

@ -14,6 +14,7 @@ import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller"; import poller from "../app/Poller";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
const publicBaseUrl = "https://ntfy.sh"; const publicBaseUrl = "https://ntfy.sh";
@ -51,6 +52,7 @@ const SubscribeDialog = (props) => {
}; };
const SubscribePage = (props) => { const SubscribePage = (props) => {
const { t } = useTranslation();
const [anotherServerVisible, setAnotherServerVisible] = useState(false); const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin; const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
@ -60,12 +62,12 @@ const SubscribePage = (props) => {
.filter(s => s !== window.location.origin); .filter(s => s !== window.location.origin);
const handleSubscribe = async () => { const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined const user = await userManager.get(baseUrl); // May be undefined
const username = (user) ? user.username : "anonymous"; const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
const success = await api.auth(baseUrl, topic, user); const success = await api.auth(baseUrl, topic, user);
if (!success) { if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
if (user) { if (user) {
setErrorText(`User ${username} not authorized`); setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return; return;
} else { } else {
props.onNeedsLogin(); props.onNeedsLogin();
@ -90,17 +92,16 @@ const SubscribePage = (props) => {
})(); })();
return ( return (
<> <>
<DialogTitle>Subscribe to topic</DialogTitle> <DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
Topics may not be password-protected, so choose a name that's not easy to guess. {t("subscribe_dialog_subscribe_description")}
Once subscribed, you can PUT/POST notifications.
</DialogContentText> </DialogContentText>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
id="topic" id="topic"
placeholder="Topic name, e.g. phil_alerts" placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
inputProps={{ maxLength: 64 }} inputProps={{ maxLength: 64 }}
value={props.topic} value={props.topic}
onChange={ev => props.setTopic(ev.target.value)} onChange={ev => props.setTopic(ev.target.value)}
@ -111,7 +112,7 @@ const SubscribePage = (props) => {
<FormControlLabel <FormControlLabel
sx={{pt: 1}} sx={{pt: 1}}
control={<Checkbox onChange={handleUseAnotherChanged}/>} control={<Checkbox onChange={handleUseAnotherChanged}/>}
label="Use another server" /> label={t("subscribe_dialog_subscribe_use_another_label")} />
{anotherServerVisible && <Autocomplete {anotherServerVisible && <Autocomplete
freeSolo freeSolo
options={existingBaseUrls} options={existingBaseUrls}
@ -124,14 +125,15 @@ const SubscribePage = (props) => {
/>} />}
</DialogContent> </DialogContent>
<DialogFooter status={errorText}> <DialogFooter status={errorText}>
<Button onClick={props.onCancel}>Cancel</Button> <Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
<Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>Subscribe</Button> <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>{t("subscribe_dialog_subscribe_button_subscribe")}</Button>
</DialogFooter> </DialogFooter>
</> </>
); );
}; };
const LoginPage = (props) => { const LoginPage = (props) => {
const { t } = useTranslation();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -142,7 +144,7 @@ const LoginPage = (props) => {
const success = await api.auth(baseUrl, topic, user); const success = await api.auth(baseUrl, topic, user);
if (!success) { if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
setErrorText(`User ${username} not authorized`); setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return; return;
} }
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
@ -151,17 +153,16 @@ const LoginPage = (props) => {
}; };
return ( return (
<> <>
<DialogTitle>Login required</DialogTitle> <DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
This topic is password-protected. Please enter username and {t("subscribe_dialog_login_description")}
password to subscribe.
</DialogContentText> </DialogContentText>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
id="username" id="username"
label="Username, e.g. phil" label={t("subscribe_dialog_login_username_label")}
value={username} value={username}
onChange={ev => setUsername(ev.target.value)} onChange={ev => setUsername(ev.target.value)}
type="text" type="text"
@ -171,7 +172,7 @@ const LoginPage = (props) => {
<TextField <TextField
margin="dense" margin="dense"
id="password" id="password"
label="Password" label={t("subscribe_dialog_login_password_label")}
type="password" type="password"
value={password} value={password}
onChange={ev => setPassword(ev.target.value)} onChange={ev => setPassword(ev.target.value)}
@ -180,8 +181,8 @@ const LoginPage = (props) => {
/> />
</DialogContent> </DialogContent>
<DialogFooter status={errorText}> <DialogFooter status={errorText}>
<Button onClick={props.onBack}>Back</Button> <Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
<Button onClick={handleLogin}>Login</Button> <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
</DialogFooter> </DialogFooter>
</> </>
); );

View file

@ -13,4 +13,5 @@ const routes = {
return `/${subscription.topic}`; return `/${subscription.topic}`;
} }
}; };
export default routes; export default routes;