Finish web app translation
This commit is contained in:
parent
893701c07b
commit
30726144b8
10 changed files with 272 additions and 132 deletions
|
@ -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_button_all_notifications": "All notifications",
|
||||
"nav_button_settings": "Settings",
|
||||
|
@ -31,5 +37,103 @@
|
|||
"notifications_example": "Example",
|
||||
"notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.",
|
||||
"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 ..."
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ class Connection {
|
|||
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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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…
Reference in a new issue