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