Finish web app translation
This commit is contained in:
		
							parent
							
								
									893701c07b
								
							
						
					
					
						commit
						30726144b8
					
				
					 10 changed files with 272 additions and 132 deletions
				
			
		|  | @ -22,14 +22,16 @@ import api from "../app/Api"; | |||
| import routes from "./routes"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import logo from "../img/ntfy.svg"; | ||||
| import {useTranslation} from "react-i18next"; | ||||
| 
 | ||||
| const ActionBar = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const location = useLocation(); | ||||
|     let title = "ntfy"; | ||||
|     if (props.selected) { | ||||
|         title = topicShortUrl(props.selected.baseUrl, props.selected.topic); | ||||
|     } else if (location.pathname === "/settings") { | ||||
|         title = "Settings"; | ||||
|         title = t("action_bar_settings"); | ||||
|     } | ||||
|     return ( | ||||
|         <AppBar position="fixed" sx={{ | ||||
|  | @ -66,6 +68,7 @@ const ActionBar = (props) => { | |||
| 
 | ||||
| // Originally from https://mui.com/components/menus/#MenuListComposition.js
 | ||||
| const SettingsIcons = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const navigate = useNavigate(); | ||||
|     const [open, setOpen] = useState(false); | ||||
|     const anchorRef = useRef(null); | ||||
|  | @ -189,9 +192,9 @@ const SettingsIcons = (props) => { | |||
|                         <Paper> | ||||
|                             <ClickAwayListener onClickAway={handleClose}> | ||||
|                                 <MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}> | ||||
|                                     <MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem> | ||||
|                                     <MenuItem onClick={handleClearAll}>Clear all notifications</MenuItem> | ||||
|                                     <MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem> | ||||
|                                     <MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem> | ||||
|                                     <MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem> | ||||
|                                     <MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem> | ||||
|                                 </MenuList> | ||||
|                             </ClickAwayListener> | ||||
|                         </Paper> | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ import {expandUrl} from "../app/utils"; | |||
| import ErrorBoundary from "./ErrorBoundary"; | ||||
| import routes from "./routes"; | ||||
| import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; | ||||
| import SendDialog from "./SendDialog"; | ||||
| import PublishDialog from "./PublishDialog"; | ||||
| import Messaging from "./Messaging"; | ||||
| import "./i18n"; // Translations!
 | ||||
| import {Backdrop, CircularProgress} from "@mui/material"; | ||||
|  | @ -91,7 +91,7 @@ const Layout = () => { | |||
|                 mobileDrawerOpen={mobileDrawerOpen} | ||||
|                 onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|                 onNotificationGranted={setNotificationsGranted} | ||||
|                 onPublishMessageClick={() => setSendDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT)} | ||||
|                 onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)} | ||||
|             /> | ||||
|             <Main> | ||||
|                 <Toolbar/> | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import * as React from "react"; | ||||
| import StackTrace from "stacktrace-js"; | ||||
| import {CircularProgress} from "@mui/material"; | ||||
| import {CircularProgress, Link} from "@mui/material"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import {Trans, withTranslation} from "react-i18next"; | ||||
| 
 | ||||
| class ErrorBoundary extends React.Component { | ||||
| class ErrorBoundaryImpl extends React.Component { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|         this.state = { | ||||
|  | @ -45,22 +46,28 @@ class ErrorBoundary extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const { t } = this.props; | ||||
|         if (this.state.error) { | ||||
|             return ( | ||||
|                 <div style={{margin: '20px'}}> | ||||
|                     <h2>Oh no, ntfy crashed 😮</h2> | ||||
|                     <h2>{t("error_boundary_title")} 😮</h2> | ||||
|                     <p> | ||||
|                         This should obviously not happen. Very sorry about this.<br/> | ||||
|                         If you have a minute, please <a href="https://github.com/binwiederhier/ntfy/issues">report this on GitHub</a>, or let us | ||||
|                         know via <a href="https://discord.gg/cT7ECsZj9w">Discord</a> or <a href="https://matrix.to/#/#ntfy:matrix.org">Matrix</a>. | ||||
|                         <Trans | ||||
|                             i18nKey="error_boundary_description" | ||||
|                             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> | ||||
|                         <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> | ||||
|                     <h3>Stack trace</h3> | ||||
|                     <h3>{t("error_boundary_stack_trace")}</h3> | ||||
|                     {this.state.niceStack | ||||
|                         ? <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> | ||||
|                 </div> | ||||
|             ); | ||||
|  | @ -69,4 +76,5 @@ class ErrorBoundary extends React.Component { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t
 | ||||
| export default ErrorBoundary; | ||||
|  |  | |||
|  | @ -1,15 +1,15 @@ | |||
| import * as React from 'react'; | ||||
| import {useState} from 'react'; | ||||
| import Navigation from "./Navigation"; | ||||
| import {topicUrl} from "../app/utils"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import SendIcon from "@mui/icons-material/Send"; | ||||
| import api from "../app/Api"; | ||||
| import SendDialog from "./SendDialog"; | ||||
| import PublishDialog from "./PublishDialog"; | ||||
| import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; | ||||
| import {Portal, Snackbar} from "@mui/material"; | ||||
| import {useTranslation} from "react-i18next"; | ||||
| 
 | ||||
| const Messaging = (props) => { | ||||
|     const [message, setMessage] = useState(""); | ||||
|  | @ -19,10 +19,10 @@ const Messaging = (props) => { | |||
|     const subscription = props.selected; | ||||
| 
 | ||||
|     const handleOpenDialogClick = () => { | ||||
|         props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT); | ||||
|         props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSendDialogClose = () => { | ||||
|     const handleDialogClose = () => { | ||||
|         props.onDialogOpenModeChange(""); | ||||
|         setDialogKey(prev => prev+1); | ||||
|     }; | ||||
|  | @ -35,21 +35,22 @@ const Messaging = (props) => { | |||
|                 onMessageChange={setMessage} | ||||
|                 onOpenDialogClick={handleOpenDialogClick} | ||||
|             />} | ||||
|             <SendDialog | ||||
|                 key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
 | ||||
|             <PublishDialog | ||||
|                 key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
 | ||||
|                 openMode={dialogOpenMode} | ||||
|                 baseUrl={subscription?.baseUrl ?? window.location.origin} | ||||
|                 topic={subscription?.topic ?? ""} | ||||
|                 message={message} | ||||
|                 onClose={handleSendDialogClose} | ||||
|                 onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG)} // Only update if not already open
 | ||||
|                 onResetOpenMode={() => props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT)} | ||||
|                 onClose={handleDialogClose} | ||||
|                 onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open
 | ||||
|                 onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const MessageBar = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const subscription = props.subscription; | ||||
|     const [snackOpen, setSnackOpen] = useState(false); | ||||
|     const handleSendClick = async () => { | ||||
|  | @ -80,7 +81,7 @@ const MessageBar = (props) => { | |||
|             <TextField | ||||
|                 autoFocus | ||||
|                 margin="dense" | ||||
|                 placeholder="Type a message here" | ||||
|                 placeholder={t("message_bar_type_message")} | ||||
|                 type="text" | ||||
|                 fullWidth | ||||
|                 variant="standard" | ||||
|  | @ -101,7 +102,7 @@ const MessageBar = (props) => { | |||
|                     open={snackOpen} | ||||
|                     autoHideDuration={3000} | ||||
|                     onClose={() => setSnackOpen(false)} | ||||
|                     message="Error publishing message" | ||||
|                     message={t("message_bar_error_publishing")} | ||||
|                 /> | ||||
|             </Portal> | ||||
|         </Paper> | ||||
|  |  | |||
|  | @ -48,10 +48,11 @@ const Preferences = () => { | |||
| }; | ||||
| 
 | ||||
| const Notifications = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     return ( | ||||
|         <Card sx={{p: 3}}> | ||||
|             <Typography variant="h5"> | ||||
|                 Notifications | ||||
|                 {t("prefs_notifications_title")} | ||||
|             </Typography> | ||||
|             <PrefGroup> | ||||
|                 <Sound/> | ||||
|  | @ -63,6 +64,7 @@ const Notifications = () => { | |||
| }; | ||||
| 
 | ||||
| const Sound = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const sound = useLiveQuery(async () => prefs.sound()); | ||||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setSound(ev.target.value); | ||||
|  | @ -71,11 +73,11 @@ const Sound = () => { | |||
|         return null; // While loading
 | ||||
|     } | ||||
|     return ( | ||||
|         <Pref title="Notification sound"> | ||||
|         <Pref title={t("prefs_notifications_sound_title")}> | ||||
|             <div style={{ display: 'flex', width: '100%' }}> | ||||
|                 <FormControl fullWidth variant="standard" sx={{ margin: 1 }}> | ||||
|                     <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={"juntos"}>Juntos</MenuItem> | ||||
|                         <MenuItem value={"pristine"}>Pristine</MenuItem> | ||||
|  | @ -94,6 +96,7 @@ const Sound = () => { | |||
| }; | ||||
| 
 | ||||
| const MinPriority = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const minPriority = useLiveQuery(async () => prefs.minPriority()); | ||||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setMinPriority(ev.target.value); | ||||
|  | @ -102,14 +105,14 @@ const MinPriority = () => { | |||
|         return null; // While loading
 | ||||
|     } | ||||
|     return ( | ||||
|         <Pref title="Minimum priority"> | ||||
|         <Pref title={t("prefs_notifications_min_priority_title")}> | ||||
|             <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|                 <Select value={minPriority} onChange={handleChange}> | ||||
|                     <MenuItem value={1}>Any priority</MenuItem> | ||||
|                     <MenuItem value={2}>Low priority and higher</MenuItem> | ||||
|                     <MenuItem value={3}>Default priority and higher</MenuItem> | ||||
|                     <MenuItem value={4}>High priority and higher</MenuItem> | ||||
|                     <MenuItem value={5}>Only max priority</MenuItem> | ||||
|                     <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem> | ||||
|                     <MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem> | ||||
|                     <MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem> | ||||
|                     <MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem> | ||||
|                     <MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem> | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|         </Pref> | ||||
|  | @ -117,6 +120,7 @@ const MinPriority = () => { | |||
| }; | ||||
| 
 | ||||
| const DeleteAfter = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); | ||||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setDeleteAfter(ev.target.value); | ||||
|  | @ -125,14 +129,14 @@ const DeleteAfter = () => { | |||
|         return null; // While loading
 | ||||
|     } | ||||
|     return ( | ||||
|         <Pref title="Delete notifications"> | ||||
|         <Pref title={t("prefs_notifications_delete_after_title")}> | ||||
|             <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|                 <Select value={deleteAfter} onChange={handleChange}> | ||||
|                     <MenuItem value={0}>Never</MenuItem> | ||||
|                     <MenuItem value={10800}>After three hours</MenuItem> | ||||
|                     <MenuItem value={86400}>After one day</MenuItem> | ||||
|                     <MenuItem value={604800}>After one week</MenuItem> | ||||
|                     <MenuItem value={2592000}>After one month</MenuItem> | ||||
|                     <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem> | ||||
|                     <MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem> | ||||
|                     <MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem> | ||||
|                     <MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem> | ||||
|                     <MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem> | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|         </Pref> | ||||
|  | @ -176,6 +180,7 @@ const Pref = (props) => { | |||
| }; | ||||
| 
 | ||||
| const Users = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
|     const [dialogOpen, setDialogOpen] = useState(false); | ||||
|     const users = useLiveQuery(() => userManager.all()); | ||||
|  | @ -199,16 +204,15 @@ const Users = () => { | |||
|         <Card sx={{ padding: 1 }}> | ||||
|             <CardContent> | ||||
|                 <Typography variant="h5"> | ||||
|                     Manage users | ||||
|                     {t("prefs_users_title")} | ||||
|                 </Typography> | ||||
|                 <Paragraph> | ||||
|                     Add/remove users for your protected topics here. Please note that username and password are | ||||
|                     stored in the browser's local storage. | ||||
|                     {t("prefs_users_description")} | ||||
|                 </Paragraph> | ||||
|                 {users?.length > 0 && <UserTable users={users}/>} | ||||
|             </CardContent> | ||||
|             <CardActions> | ||||
|                 <Button onClick={handleAddClick}>Add user</Button> | ||||
|                 <Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button> | ||||
|                 <UserDialog | ||||
|                     key={`userAddDialog${dialogKey}`} | ||||
|                     open={dialogOpen} | ||||
|  | @ -223,6 +227,7 @@ const Users = () => { | |||
| }; | ||||
| 
 | ||||
| const UserTable = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
|     const [dialogOpen, setDialogOpen] = useState(false); | ||||
|     const [dialogUser, setDialogUser] = useState(null); | ||||
|  | @ -255,8 +260,8 @@ const UserTable = (props) => { | |||
|         <Table size="small"> | ||||
|             <TableHead> | ||||
|                 <TableRow> | ||||
|                     <TableCell>User</TableCell> | ||||
|                     <TableCell>Service URL</TableCell> | ||||
|                     <TableCell>{t("prefs_users_table_user_header")}</TableCell> | ||||
|                     <TableCell>{t("prefs_users_table_base_url_header")}</TableCell> | ||||
|                     <TableCell/> | ||||
|                 </TableRow> | ||||
|             </TableHead> | ||||
|  | @ -292,6 +297,7 @@ const UserTable = (props) => { | |||
| }; | ||||
| 
 | ||||
| const UserDialog = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [baseUrl, setBaseUrl] = useState(""); | ||||
|     const [username, setUsername] = useState(""); | ||||
|     const [password, setPassword] = useState(""); | ||||
|  | @ -320,13 +326,13 @@ const UserDialog = (props) => { | |||
|     }, [editMode, props.user]); | ||||
|     return ( | ||||
|         <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> | ||||
|                 {!editMode && <TextField | ||||
|                     autoFocus | ||||
|                     margin="dense" | ||||
|                     id="baseUrl" | ||||
|                     label="Service URL, e.g. https://ntfy.sh" | ||||
|                     label={t("prefs_users_dialog_base_url_label")} | ||||
|                     value={baseUrl} | ||||
|                     onChange={ev => setBaseUrl(ev.target.value)} | ||||
|                     type="url" | ||||
|  | @ -337,7 +343,7 @@ const UserDialog = (props) => { | |||
|                     autoFocus={editMode} | ||||
|                     margin="dense" | ||||
|                     id="username" | ||||
|                     label="Username, e.g. phil" | ||||
|                     label={t("prefs_users_dialog_username_label")} | ||||
|                     value={username} | ||||
|                     onChange={ev => setUsername(ev.target.value)} | ||||
|                     type="text" | ||||
|  | @ -347,7 +353,7 @@ const UserDialog = (props) => { | |||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     id="password" | ||||
|                     label="Password" | ||||
|                     label={t("prefs_users_dialog_password_label")} | ||||
|                     type="password" | ||||
|                     value={password} | ||||
|                     onChange={ev => setPassword(ev.target.value)} | ||||
|  | @ -356,18 +362,19 @@ const UserDialog = (props) => { | |||
|                 /> | ||||
|             </DialogContent> | ||||
|             <DialogActions> | ||||
|                 <Button onClick={props.onCancel}>Cancel</Button> | ||||
|                 <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? "Save" : "Add"}</Button> | ||||
|                 <Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button> | ||||
|                 <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? t("prefs_users_dialog_button_save") : t("prefs_users_dialog_button_add")}</Button> | ||||
|             </DialogActions> | ||||
|         </Dialog> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Appearance = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     return ( | ||||
|         <Card sx={{p: 3}}> | ||||
|             <Typography variant="h5"> | ||||
|                 Appearance | ||||
|                 {t("prefs_appearance_title")} | ||||
|             </Typography> | ||||
|             <PrefGroup> | ||||
|                 <Language/> | ||||
|  | @ -379,7 +386,7 @@ const Appearance = () => { | |||
| const Language = () => { | ||||
|     const { t, i18n } = useTranslation(); | ||||
|     return ( | ||||
|         <Pref title="Language"> | ||||
|         <Pref title={t("prefs_appearance_language_title")}> | ||||
|             <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|                 <Select value={i18n.language} onChange={(ev) => i18n.changeLanguage(ev.target.value)}> | ||||
|                     <MenuItem value="en">English</MenuItem> | ||||
|  |  | |||
|  | @ -25,8 +25,10 @@ import DialogFooter from "./DialogFooter"; | |||
| import api from "../app/Api"; | ||||
| import userManager from "../app/UserManager"; | ||||
| import EmojiPicker from "./EmojiPicker"; | ||||
| import {Trans, useTranslation} from "react-i18next"; | ||||
| 
 | ||||
| const SendDialog = (props) => { | ||||
| const PublishDialog = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [baseUrl, setBaseUrl] = useState(""); | ||||
|     const [topic, setTopic] = useState(""); | ||||
|     const [message, setMessage] = useState(""); | ||||
|  | @ -123,10 +125,13 @@ const SendDialog = (props) => { | |||
|             const headers = maybeWithBasicAuth({}, user); | ||||
|             const progressFn = (ev) => { | ||||
|                 if (ev.loaded > 0 && ev.total > 0) { | ||||
|                     const percent = Math.round(ev.loaded * 100.0 / ev.total); | ||||
|                     setStatus(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`); | ||||
|                     setStatus(t("publish_dialog_progress_uploading_detail", { | ||||
|                         loaded: formatBytes(ev.loaded), | ||||
|                         total: formatBytes(ev.total), | ||||
|                         percent: Math.round(ev.loaded * 100.0 / ev.total) | ||||
|                     })); | ||||
|                 } else { | ||||
|                     setStatus(`Uploading ...`); | ||||
|                     setStatus(t("publish_dialog_progress_uploading")); | ||||
|                 } | ||||
|             }; | ||||
|             const request = api.publishXHR(url, body, headers, progressFn); | ||||
|  | @ -135,7 +140,7 @@ const SendDialog = (props) => { | |||
|             if (!publishAnother) { | ||||
|                 props.onClose(); | ||||
|             } else { | ||||
|                 setStatus("Message published"); | ||||
|                 setStatus(t("publish_dialog_message_published")); | ||||
|                 setActiveRequest(null); | ||||
|             } | ||||
|         } catch (e) { | ||||
|  | @ -152,11 +157,14 @@ const SendDialog = (props) => { | |||
|             const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; | ||||
|             const quotaReached = remainingBytes > 0 && file.size > remainingBytes; | ||||
|             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) { | ||||
|                 return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit`); | ||||
|                 return setAttachFileError(t("publish_dialog_attachment_limits_file_reached", { fileSizeLimit: formatBytes(fileSizeLimit) })); | ||||
|             } else if (quotaReached) { | ||||
|                 return setAttachFileError(`exceeds quota, ${formatBytes(remainingBytes)} remaining`); | ||||
|                 return setAttachFileError(t("publish_dialog_attachment_limits_quota_reached", { remainingBytes: formatBytes(remainingBytes) })); | ||||
|             } | ||||
|             setAttachFileError(""); | ||||
|         } catch (e) { | ||||
|  | @ -188,7 +196,7 @@ const SendDialog = (props) => { | |||
| 
 | ||||
|     const handleAttachFileDragLeave = () => { | ||||
|         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
 | ||||
|         } | ||||
|     }; | ||||
|  | @ -205,6 +213,14 @@ const SendDialog = (props) => { | |||
|         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 ( | ||||
|         <> | ||||
|             {dropZone && <DropArea | ||||
|  | @ -212,7 +228,7 @@ const SendDialog = (props) => { | |||
|                 onDragLeave={handleAttachFileDragLeave}/> | ||||
|             } | ||||
|             <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> | ||||
|                     {dropZone && <DropBox/>} | ||||
|                     {showTopicUrl && | ||||
|  | @ -223,8 +239,8 @@ const SendDialog = (props) => { | |||
|                         }}> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Server URL" | ||||
|                                 placeholder="Server URL, e.g. https://example.com" | ||||
|                                 label={t("publish_dialog_base_url_label")} | ||||
|                                 placeholder={t("publish_dialog_base_url_placeholder")} | ||||
|                                 value={baseUrl} | ||||
|                                 onChange={ev => setBaseUrl(ev.target.value)} | ||||
|                                 disabled={disabled} | ||||
|  | @ -234,8 +250,8 @@ const SendDialog = (props) => { | |||
|                             /> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Topic" | ||||
|                                 placeholder="Topic name, e.g. phil_alerts" | ||||
|                                 label={t("publish_dialog_topic_label")} | ||||
|                                 placeholder={t("publish_dialog_topic_placeholder")} | ||||
|                                 value={topic} | ||||
|                                 onChange={ev => setTopic(ev.target.value)} | ||||
|                                 disabled={disabled} | ||||
|  | @ -248,19 +264,19 @@ const SendDialog = (props) => { | |||
|                     } | ||||
|                     <TextField | ||||
|                         margin="dense" | ||||
|                         label="Title" | ||||
|                         label={t("publish_dialog_title_label")} | ||||
|                         placeholder={t("publish_dialog_title_placeholder")} | ||||
|                         value={title} | ||||
|                         onChange={ev => setTitle(ev.target.value)} | ||||
|                         disabled={disabled} | ||||
|                         type="text" | ||||
|                         fullWidth | ||||
|                         variant="standard" | ||||
|                         placeholder="Notification title, e.g. Disk space alert" | ||||
|                     /> | ||||
|                     <TextField | ||||
|                         margin="dense" | ||||
|                         label="Message" | ||||
|                         placeholder="Type a message here" | ||||
|                         label={t("publish_dialog_message_label")} | ||||
|                         placeholder={t("publish_dialog_message_placeholder")} | ||||
|                         value={message} | ||||
|                         onChange={ev => setMessage(ev.target.value)} | ||||
|                         disabled={disabled} | ||||
|  | @ -282,8 +298,8 @@ const SendDialog = (props) => { | |||
|                         </DialogIconButton> | ||||
|                         <TextField | ||||
|                             margin="dense" | ||||
|                             label="Tags" | ||||
|                             placeholder="Comma-separated list of tags, e.g. warning, srv1-backup" | ||||
|                             label={t("publish_dialog_tags_label")} | ||||
|                             placeholder={t("publish_dialog_tags_placeholder")} | ||||
|                             value={tags} | ||||
|                             onChange={ev => setTags(ev.target.value)} | ||||
|                             disabled={disabled} | ||||
|  | @ -298,7 +314,7 @@ const SendDialog = (props) => { | |||
|                         > | ||||
|                             <InputLabel/> | ||||
|                             <Select | ||||
|                                 label="Priority" | ||||
|                                 label={t("publish_dialog_priority_label")} | ||||
|                                 margin="dense" | ||||
|                                 value={priority} | ||||
|                                 onChange={(ev) => setPriority(ev.target.value)} | ||||
|  | @ -322,8 +338,8 @@ const SendDialog = (props) => { | |||
|                         }}> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Click URL" | ||||
|                                 placeholder="URL that is opened when notification is clicked" | ||||
|                                 label={t("publish_dialog_click_label")} | ||||
|                                 placeholder={t("publish_dialog_click_placeholder")} | ||||
|                                 value={clickUrl} | ||||
|                                 onChange={ev => setClickUrl(ev.target.value)} | ||||
|                                 disabled={disabled} | ||||
|  | @ -340,8 +356,8 @@ const SendDialog = (props) => { | |||
|                         }}> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Email" | ||||
|                                 placeholder="Address to forward the message to, e.g. phil@example.com" | ||||
|                                 label={t("publish_dialog_email_label")} | ||||
|                                 placeholder={t("publish_dialog_email_placeholder")} | ||||
|                                 value={email} | ||||
|                                 onChange={ev => setEmail(ev.target.value)} | ||||
|                                 disabled={disabled} | ||||
|  | @ -360,8 +376,8 @@ const SendDialog = (props) => { | |||
|                         }}> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Attachment URL" | ||||
|                                 placeholder="Attach file by URL, e.g. https://f-droid.org/F-Droid.apk" | ||||
|                                 label={t("publish_dialog_attach_label")} | ||||
|                                 placeholder={t("publish_dialog_attach_placeholder")} | ||||
|                                 value={attachUrl} | ||||
|                                 onChange={ev => { | ||||
|                                     const url = ev.target.value; | ||||
|  | @ -385,8 +401,8 @@ const SendDialog = (props) => { | |||
|                             /> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Filename" | ||||
|                                 placeholder="Attachment filename" | ||||
|                                 label={t("publish_dialog_filename_label")} | ||||
|                                 placeholder={t("publish_dialog_filename_placeholder")} | ||||
|                                 value={filename} | ||||
|                                 onChange={ev => { | ||||
|                                     setFilename(ev.target.value); | ||||
|  | @ -424,8 +440,8 @@ const SendDialog = (props) => { | |||
|                         }}> | ||||
|                             <TextField | ||||
|                                 margin="dense" | ||||
|                                 label="Delay" | ||||
|                                 placeholder="Delay delivery, e.g. 1649029748, 30m, or tomorrow, 9am" | ||||
|                                 label={t("publish_dialog_delay_label")} | ||||
|                                 placeholder={t("publish_dialog_delay_placeholder")} | ||||
|                                 value={delay} | ||||
|                                 onChange={ev => setDelay(ev.target.value)} | ||||
|                                 disabled={disabled} | ||||
|  | @ -436,33 +452,37 @@ const SendDialog = (props) => { | |||
|                         </ClosableRow> | ||||
|                     } | ||||
|                     <Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}> | ||||
|                         Other features: | ||||
|                         {t("publish_dialog_other_features")} | ||||
|                     </Typography> | ||||
|                     <div> | ||||
|                         {!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" 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}}/>} | ||||
|                         {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" 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}}/>} | ||||
|                         {!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} | ||||
|                         {!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(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={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(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={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} 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={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} | ||||
|                     </div> | ||||
|                     <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}> | ||||
|                         For examples and a detailed description of all send features, please | ||||
|                         refer to the <Link href="/docs" target="_blank">documentation</Link>. | ||||
|                         <Trans | ||||
|                             i18nKey="publish_dialog_details_examples_description" | ||||
|                             components={{ | ||||
|                                 docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener"/> | ||||
|                             }} | ||||
|                         /> | ||||
|                     </Typography> | ||||
|                 </DialogContent> | ||||
|                 <DialogFooter status={status}> | ||||
|                     {activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>} | ||||
|                     {activeRequest && <Button onClick={() => activeRequest.abort()}>{t("publish_dialog_button_cancel_sending")}</Button>} | ||||
|                     {!activeRequest && | ||||
|                         <> | ||||
|                             <FormControlLabel | ||||
|                                 label="Publish another" | ||||
|                                 label={t("publish_dialog_checkbox_publish_another")} | ||||
|                                 sx={{marginRight: 2}} | ||||
|                                 control={ | ||||
|                                     <Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} /> | ||||
|                                 } /> | ||||
|                             <Button onClick={props.onClose}>Cancel</Button> | ||||
|                             <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button> | ||||
|                             <Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button> | ||||
|                             <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button> | ||||
|                         </> | ||||
|                     } | ||||
|                 </DialogFooter> | ||||
|  | @ -506,11 +526,12 @@ const DialogIconButton = (props) => { | |||
| }; | ||||
| 
 | ||||
| const AttachmentBox = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const file = props.file; | ||||
|     return ( | ||||
|         <> | ||||
|             <Typography variant="body1" sx={{marginTop: 2}}> | ||||
|                 Attached file: | ||||
|                 {t("publish_dialog_attached_file_title")} | ||||
|             </Typography> | ||||
|             <Box sx={{ | ||||
|                 display: 'flex', | ||||
|  | @ -523,6 +544,7 @@ const AttachmentBox = (props) => { | |||
|                     <ExpandingTextField | ||||
|                         minWidth={140} | ||||
|                         variant="body2" | ||||
|                         placeholder={t("publish_dialog_attached_file_filename_placeholder")} | ||||
|                         value={props.filename} | ||||
|                         onChange={(ev) => props.onChangeFilename(ev.target.value)} | ||||
|                         disabled={props.disabled} | ||||
|  | @ -568,7 +590,7 @@ const ExpandingTextField = (props) => { | |||
|             </Typography> | ||||
|             <TextField | ||||
|                 margin="dense" | ||||
|                 placeholder="Attachment filename" | ||||
|                 placeholder={props.placeholder} | ||||
|                 value={props.value} | ||||
|                 onChange={props.onChange} | ||||
|                 type="text" | ||||
|  | @ -610,6 +632,7 @@ const DropArea = (props) => { | |||
| }; | ||||
| 
 | ||||
| const DropBox = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     return ( | ||||
|         <Box sx={{ | ||||
|             position: 'absolute', | ||||
|  | @ -635,21 +658,13 @@ const DropBox = () => { | |||
|                     alignItems: "center", | ||||
|                 }} | ||||
|             > | ||||
|                 <Typography variant="h5">Drop file here</Typography> | ||||
|                 <Typography variant="h5">{t("publish_dialog_drop_file_here")}</Typography> | ||||
|             </Box> | ||||
|         </Box> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const priorities = { | ||||
|     1: { label: "Min. priority", file: priority1 }, | ||||
|     2: { label: "Low priority", file: priority2 }, | ||||
|     3: { label: "Default priority", file: priority3 }, | ||||
|     4: { label: "High priority", file: priority4 }, | ||||
|     5: { label: "Max. priority", file: priority5 } | ||||
| }; | ||||
| PublishDialog.OPEN_MODE_DEFAULT = "default"; | ||||
| PublishDialog.OPEN_MODE_DRAG = "drag"; | ||||
| 
 | ||||
| SendDialog.OPEN_MODE_DEFAULT = "default"; | ||||
| SendDialog.OPEN_MODE_DRAG = "drag"; | ||||
| 
 | ||||
| export default SendDialog; | ||||
| export default PublishDialog; | ||||
|  | @ -14,6 +14,7 @@ import userManager from "../app/UserManager"; | |||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import poller from "../app/Poller"; | ||||
| import DialogFooter from "./DialogFooter"; | ||||
| import {useTranslation} from "react-i18next"; | ||||
| 
 | ||||
| const publicBaseUrl = "https://ntfy.sh"; | ||||
| 
 | ||||
|  | @ -51,6 +52,7 @@ const SubscribeDialog = (props) => { | |||
| }; | ||||
| 
 | ||||
| const SubscribePage = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [anotherServerVisible, setAnotherServerVisible] = useState(false); | ||||
|     const [errorText, setErrorText] = useState(""); | ||||
|     const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin; | ||||
|  | @ -60,12 +62,12 @@ const SubscribePage = (props) => { | |||
|         .filter(s => s !== window.location.origin); | ||||
|     const handleSubscribe = async () => { | ||||
|         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); | ||||
|         if (!success) { | ||||
|             console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||
|             if (user) { | ||||
|                 setErrorText(`User ${username} not authorized`); | ||||
|                 setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username })); | ||||
|                 return; | ||||
|             } else { | ||||
|                 props.onNeedsLogin(); | ||||
|  | @ -90,17 +92,16 @@ const SubscribePage = (props) => { | |||
|     })(); | ||||
|     return ( | ||||
|         <> | ||||
|             <DialogTitle>Subscribe to topic</DialogTitle> | ||||
|             <DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle> | ||||
|             <DialogContent> | ||||
|                 <DialogContentText> | ||||
|                     Topics may not be password-protected, so choose a name that's not easy to guess. | ||||
|                     Once subscribed, you can PUT/POST notifications. | ||||
|                     {t("subscribe_dialog_subscribe_description")} | ||||
|                 </DialogContentText> | ||||
|                 <TextField | ||||
|                     autoFocus | ||||
|                     margin="dense" | ||||
|                     id="topic" | ||||
|                     placeholder="Topic name, e.g. phil_alerts" | ||||
|                     placeholder={t("subscribe_dialog_subscribe_topic_placeholder")} | ||||
|                     inputProps={{ maxLength: 64 }} | ||||
|                     value={props.topic} | ||||
|                     onChange={ev => props.setTopic(ev.target.value)} | ||||
|  | @ -111,7 +112,7 @@ const SubscribePage = (props) => { | |||
|                 <FormControlLabel | ||||
|                     sx={{pt: 1}} | ||||
|                     control={<Checkbox onChange={handleUseAnotherChanged}/>} | ||||
|                     label="Use another server" /> | ||||
|                     label={t("subscribe_dialog_subscribe_use_another_label")} /> | ||||
|                 {anotherServerVisible && <Autocomplete | ||||
|                     freeSolo | ||||
|                     options={existingBaseUrls} | ||||
|  | @ -124,14 +125,15 @@ const SubscribePage = (props) => { | |||
|                 />} | ||||
|             </DialogContent> | ||||
|             <DialogFooter status={errorText}> | ||||
|                 <Button onClick={props.onCancel}>Cancel</Button> | ||||
|                 <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>Subscribe</Button> | ||||
|                 <Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button> | ||||
|                 <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>{t("subscribe_dialog_subscribe_button_subscribe")}</Button> | ||||
|             </DialogFooter> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const LoginPage = (props) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [username, setUsername] = useState(""); | ||||
|     const [password, setPassword] = useState(""); | ||||
|     const [errorText, setErrorText] = useState(""); | ||||
|  | @ -142,7 +144,7 @@ const LoginPage = (props) => { | |||
|         const success = await api.auth(baseUrl, topic, user); | ||||
|         if (!success) { | ||||
|             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; | ||||
|         } | ||||
|         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); | ||||
|  | @ -151,17 +153,16 @@ const LoginPage = (props) => { | |||
|     }; | ||||
|     return ( | ||||
|         <> | ||||
|             <DialogTitle>Login required</DialogTitle> | ||||
|             <DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle> | ||||
|             <DialogContent> | ||||
|                 <DialogContentText> | ||||
|                     This topic is password-protected. Please enter username and | ||||
|                     password to subscribe. | ||||
|                     {t("subscribe_dialog_login_description")} | ||||
|                 </DialogContentText> | ||||
|                 <TextField | ||||
|                     autoFocus | ||||
|                     margin="dense" | ||||
|                     id="username" | ||||
|                     label="Username, e.g. phil" | ||||
|                     label={t("subscribe_dialog_login_username_label")} | ||||
|                     value={username} | ||||
|                     onChange={ev => setUsername(ev.target.value)} | ||||
|                     type="text" | ||||
|  | @ -171,7 +172,7 @@ const LoginPage = (props) => { | |||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     id="password" | ||||
|                     label="Password" | ||||
|                     label={t("subscribe_dialog_login_password_label")} | ||||
|                     type="password" | ||||
|                     value={password} | ||||
|                     onChange={ev => setPassword(ev.target.value)} | ||||
|  | @ -180,8 +181,8 @@ const LoginPage = (props) => { | |||
|                 /> | ||||
|             </DialogContent> | ||||
|             <DialogFooter status={errorText}> | ||||
|                 <Button onClick={props.onBack}>Back</Button> | ||||
|                 <Button onClick={handleLogin}>Login</Button> | ||||
|                 <Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button> | ||||
|                 <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button> | ||||
|             </DialogFooter> | ||||
|         </> | ||||
|     ); | ||||
|  |  | |||
|  | @ -13,4 +13,5 @@ const routes = { | |||
|         return `/${subscription.topic}`; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default routes; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue