Reserve dialogs
This commit is contained in:
parent
259293f9b3
commit
07cdf2bc7a
13 changed files with 587 additions and 397 deletions
|
@ -447,6 +447,12 @@ func (s *Server) handleAccountReservationDelete(w http.ResponseWriter, r *http.R
|
||||||
if err := s.userManager.RemoveReservations(u.Name, topic); err != nil {
|
if err := s.userManager.RemoveReservations(u.Name, topic); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
deleteMessages := readBoolParam(r, false, "X-Delete-Messages", "Delete-Messages")
|
||||||
|
if deleteMessages {
|
||||||
|
if err := s.messageCache.ExpireMessages(topic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.writeJSON(w, newSuccessResponse())
|
return s.writeJSON(w, newSuccessResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
// During web development, you may change values here for rapid testing.
|
// During web development, you may change values here for rapid testing.
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
base_url: "https://127-0-0-1.my.local-ip.co", // window.location.origin FIXME update before merging
|
base_url: "https://127.0.0.1", // window.location.origin FIXME update before merging
|
||||||
app_root: "/app",
|
app_root: "/app",
|
||||||
enable_login: true,
|
enable_login: true,
|
||||||
enable_signup: true,
|
enable_signup: true,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"common_cancel": "Cancel",
|
"common_cancel": "Cancel",
|
||||||
|
"common_save": "Save",
|
||||||
"signup_title": "Create a ntfy account",
|
"signup_title": "Create a ntfy account",
|
||||||
"signup_form_username": "Username",
|
"signup_form_username": "Username",
|
||||||
"signup_form_password": "Password",
|
"signup_form_password": "Password",
|
||||||
|
@ -18,7 +19,10 @@
|
||||||
"action_bar_logo_alt": "ntfy logo",
|
"action_bar_logo_alt": "ntfy logo",
|
||||||
"action_bar_settings": "Settings",
|
"action_bar_settings": "Settings",
|
||||||
"action_bar_account": "Account",
|
"action_bar_account": "Account",
|
||||||
"action_bar_subscription_settings": "Subscription settings",
|
"action_bar_change_display_name": "Change display name",
|
||||||
|
"action_bar_reservation_add": "Reserve topic",
|
||||||
|
"action_bar_reservation_edit": "Change reservation",
|
||||||
|
"action_bar_reservation_delete": "Remove reservation",
|
||||||
"action_bar_send_test_notification": "Send test notification",
|
"action_bar_send_test_notification": "Send test notification",
|
||||||
"action_bar_clear_notifications": "Clear all notifications",
|
"action_bar_clear_notifications": "Clear all notifications",
|
||||||
"action_bar_unsubscribe": "Unsubscribe",
|
"action_bar_unsubscribe": "Unsubscribe",
|
||||||
|
@ -82,12 +86,10 @@
|
||||||
"notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.",
|
"notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.",
|
||||||
"notifications_example": "Example",
|
"notifications_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>.",
|
||||||
"subscription_settings_dialog_title": "Subscription settings",
|
"display_name_dialog_title": "Change display name",
|
||||||
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
|
"display_name_dialog_description": "Set an alternative name for a topic that is displayed in the subscription list. This helps identify topics with complicated names more easily.",
|
||||||
"subscription_settings_dialog_display_name_placeholder": "Display name",
|
"display_name_dialog_placeholder": "Display name",
|
||||||
"subscription_settings_dialog_reserve_topic_label": "Reserve topic and configure access",
|
"reserve_dialog_checkbox_label": "Reserve topic and configure access",
|
||||||
"subscription_settings_button_cancel": "Cancel",
|
|
||||||
"subscription_settings_button_save": "Save",
|
|
||||||
"notifications_loading": "Loading notifications …",
|
"notifications_loading": "Loading notifications …",
|
||||||
"publish_dialog_title_topic": "Publish to {{topic}}",
|
"publish_dialog_title_topic": "Publish to {{topic}}",
|
||||||
"publish_dialog_title_no_topic": "Publish notification",
|
"publish_dialog_title_no_topic": "Publish notification",
|
||||||
|
@ -309,11 +311,19 @@
|
||||||
"prefs_reservations_table_everyone_read_only": "I can publish and subscribe, everyone can subscribe",
|
"prefs_reservations_table_everyone_read_only": "I can publish and subscribe, everyone can subscribe",
|
||||||
"prefs_reservations_table_everyone_write_only": "I can publish and subscribe, everyone can publish",
|
"prefs_reservations_table_everyone_write_only": "I can publish and subscribe, everyone can publish",
|
||||||
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
||||||
|
"prefs_reservations_table_not_subscribed": "Not subscribed",
|
||||||
"prefs_reservations_dialog_title_add": "Reserve topic",
|
"prefs_reservations_dialog_title_add": "Reserve topic",
|
||||||
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
||||||
|
"prefs_reservations_dialog_title_delete": "Delete topic reservation",
|
||||||
"prefs_reservations_dialog_description": "Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
"prefs_reservations_dialog_description": "Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
||||||
"prefs_reservations_dialog_topic_label": "Topic",
|
"prefs_reservations_dialog_topic_label": "Topic",
|
||||||
"prefs_reservations_dialog_access_label": "Access",
|
"prefs_reservations_dialog_access_label": "Access",
|
||||||
|
"reservation_delete_dialog_description": "Removing a reservation gives up ownership over the topic, and allows others to reserve it. You can keep, or delete existing messages and attachments.",
|
||||||
|
"reservation_delete_dialog_action_keep_title": "Keep cached messages and attachments",
|
||||||
|
"reservation_delete_dialog_action_keep_description": "Messages and attachments that are cached on the server will become publicly visible for people with knowledge of the topic name.",
|
||||||
|
"reservation_delete_dialog_action_delete_title": "Delete cached messages and attachments",
|
||||||
|
"reservation_delete_dialog_action_delete_description": "Cached messages and attachments will be permanently deleted. This action cannot be undone.",
|
||||||
|
"reservation_delete_dialog_submit_button": "Delete reservation",
|
||||||
"priority_min": "min",
|
"priority_min": "min",
|
||||||
"priority_low": "low",
|
"priority_low": "low",
|
||||||
"priority_default": "default",
|
"priority_default": "default",
|
||||||
|
|
|
@ -308,12 +308,15 @@ class AccountApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteReservation(topic) {
|
async deleteReservation(topic, deleteMessages) {
|
||||||
const url = accountReservationSingleUrl(config.base_url, topic);
|
const url = accountReservationSingleUrl(config.base_url, topic);
|
||||||
console.log(`[AccountApi] Removing topic reservation ${url}`);
|
console.log(`[AccountApi] Removing topic reservation ${url}`);
|
||||||
|
const headers = {
|
||||||
|
"X-Delete-Messages": deleteMessages ? "true" : "false"
|
||||||
|
}
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: withBearerAuth({}, session.token())
|
headers: withBearerAuth(headers, session.token())
|
||||||
});
|
});
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
|
|
|
@ -7,28 +7,26 @@ import Typography from "@mui/material/Typography";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import {formatShortDateTime, shuffle, topicDisplayName} from "../app/utils";
|
import {topicDisplayName} from "../app/utils";
|
||||||
import db from "../app/db";
|
import db from "../app/db";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
||||||
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";
|
import {useTranslation} from "react-i18next";
|
||||||
import {Portal, Snackbar} from "@mui/material";
|
|
||||||
import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog";
|
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import {Logout, Person, Settings} from "@mui/icons-material";
|
import {Logout, Person, Settings} from "@mui/icons-material";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
import accountApi from "../app/AccountApi";
|
||||||
import PopupMenu from "./PopupMenu";
|
import PopupMenu from "./PopupMenu";
|
||||||
|
import SubscriptionPopup from "./SubscriptionPopup";
|
||||||
|
|
||||||
const ActionBar = (props) => {
|
const ActionBar = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -86,133 +84,28 @@ const ActionBar = (props) => {
|
||||||
|
|
||||||
const SettingsIcons = (props) => {
|
const SettingsIcons = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [snackOpen, setSnackOpen] = useState(false);
|
|
||||||
const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false);
|
|
||||||
const subscription = props.subscription;
|
const subscription = props.subscription;
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const handleToggleOpen = (event) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleMute = async () => {
|
const handleToggleMute = async () => {
|
||||||
const mutedUntil = (subscription.mutedUntil) ? 0 : 1; // Make this a timestamp in the future
|
const mutedUntil = (subscription.mutedUntil) ? 0 : 1; // Make this a timestamp in the future
|
||||||
await subscriptionManager.setMutedUntil(subscription.id, mutedUntil);
|
await subscriptionManager.setMutedUntil(subscription.id, mutedUntil);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAll = async (event) => {
|
|
||||||
console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
|
|
||||||
await subscriptionManager.deleteNotifications(props.subscription.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnsubscribe = async (event) => {
|
|
||||||
console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
|
|
||||||
await subscriptionManager.remove(props.subscription.id);
|
|
||||||
if (session.exists() && props.subscription.remoteId) {
|
|
||||||
try {
|
|
||||||
await accountApi.deleteSubscription(props.subscription.remoteId);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[ActionBar] Error unsubscribing`, e);
|
|
||||||
if ((e instanceof UnauthorizedError)) {
|
|
||||||
session.resetAndRedirect(routes.login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newSelected = await subscriptionManager.first(); // May be undefined
|
|
||||||
if (newSelected) {
|
|
||||||
navigate(routes.forSubscription(newSelected));
|
|
||||||
} else {
|
|
||||||
navigate(routes.app);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubscriptionSettings = async () => {
|
|
||||||
setSubscriptionSettingsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSendTestMessage = async () => {
|
|
||||||
const baseUrl = props.subscription.baseUrl;
|
|
||||||
const topic = props.subscription.topic;
|
|
||||||
const tags = shuffle([
|
|
||||||
"grinning", "octopus", "upside_down_face", "palm_tree", "maple_leaf", "apple", "skull", "warning", "jack_o_lantern",
|
|
||||||
"de-server-1", "backups", "cron-script", "script-error", "phils-automation", "mouse", "go-rocks", "hi-ben"])
|
|
||||||
.slice(0, Math.round(Math.random() * 4));
|
|
||||||
const priority = shuffle([1, 2, 3, 4, 5])[0];
|
|
||||||
const title = shuffle([
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"", // Higher chance of no title
|
|
||||||
"Oh my, another test message?",
|
|
||||||
"Titles are optional, did you know that?",
|
|
||||||
"ntfy is open source, and will always be free. Cool, right?",
|
|
||||||
"I don't really like apples",
|
|
||||||
"My favorite TV show is The Wire. You should watch it!",
|
|
||||||
"You can attach files and URLs to messages too",
|
|
||||||
"You can delay messages up to 3 days"
|
|
||||||
])[0];
|
|
||||||
const nowSeconds = Math.round(Date.now()/1000);
|
|
||||||
const message = shuffle([
|
|
||||||
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`,
|
|
||||||
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
|
|
||||||
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
|
|
||||||
`Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
|
|
||||||
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
|
|
||||||
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
|
|
||||||
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`
|
|
||||||
])[0];
|
|
||||||
try {
|
|
||||||
await api.publish(baseUrl, topic, message, {
|
|
||||||
title: title,
|
|
||||||
priority: priority,
|
|
||||||
tags: tags
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[ActionBar] Error publishing message`, e);
|
|
||||||
setSnackOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} aria-label={t("action_bar_toggle_mute")}>
|
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} aria-label={t("action_bar_toggle_mute")}>
|
||||||
{subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
|
{subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
|
<IconButton color="inherit" size="large" edge="end" onClick={(ev) => setAnchorEl(ev.currentTarget)} aria-label={t("action_bar_toggle_action_menu")}>
|
||||||
<MoreVertIcon/>
|
<MoreVertIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<PopupMenu
|
<SubscriptionPopup
|
||||||
horizontal="right"
|
subscription={subscription}
|
||||||
anchorEl={anchorEl}
|
anchor={anchorEl}
|
||||||
open={open}
|
placement="right"
|
||||||
onClose={handleClose}
|
onClose={() => setAnchorEl(null)}
|
||||||
>
|
/>
|
||||||
<MenuItem onClick={handleSubscriptionSettings}>{t("action_bar_subscription_settings")}</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>
|
|
||||||
</PopupMenu>
|
|
||||||
<Portal>
|
|
||||||
<Snackbar
|
|
||||||
open={snackOpen}
|
|
||||||
autoHideDuration={3000}
|
|
||||||
onClose={() => setSnackOpen(false)}
|
|
||||||
message={t("message_bar_error_publishing")}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
<Portal>
|
|
||||||
<SubscriptionSettingsDialog
|
|
||||||
key={`subscriptionSettingsDialog${subscription.id}`}
|
|
||||||
open={subscriptionSettingsOpen}
|
|
||||||
subscription={subscription}
|
|
||||||
onClose={() => setSubscriptionSettingsOpen(false)}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,7 @@ import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import SubscribeDialog from "./SubscribeDialog";
|
import SubscribeDialog from "./SubscribeDialog";
|
||||||
import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Tooltip} from "@mui/material";
|
import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Menu, Portal, Tooltip} from "@mui/material";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {openUrl, topicDisplayName, topicUrl} from "../app/utils";
|
import {openUrl, topicDisplayName, topicUrl} from "../app/utils";
|
||||||
|
@ -21,7 +21,16 @@ import routes from "./routes";
|
||||||
import {ConnectionState} from "../app/Connection";
|
import {ConnectionState} from "../app/Connection";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import {ChatBubble, Lock, NotificationsOffOutlined, Public, PublicOff, Send} from "@mui/icons-material";
|
import {
|
||||||
|
ChatBubble,
|
||||||
|
Lock, Logout,
|
||||||
|
MoreHoriz, MoreVert,
|
||||||
|
NotificationsOffOutlined,
|
||||||
|
Public,
|
||||||
|
PublicOff,
|
||||||
|
Send,
|
||||||
|
Settings
|
||||||
|
} from "@mui/icons-material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import notifier from "../app/Notifier";
|
import notifier from "../app/Notifier";
|
||||||
import config from "../app/config";
|
import config from "../app/config";
|
||||||
|
@ -33,6 +42,10 @@ import CelebrationIcon from '@mui/icons-material/Celebration';
|
||||||
import UpgradeDialog from "./UpgradeDialog";
|
import UpgradeDialog from "./UpgradeDialog";
|
||||||
import {AccountContext} from "./App";
|
import {AccountContext} from "./App";
|
||||||
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
|
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import PopupMenu from "./PopupMenu";
|
||||||
|
import SubscriptionPopup from "./SubscriptionPopup";
|
||||||
|
|
||||||
const navWidth = 280;
|
const navWidth = 280;
|
||||||
|
|
||||||
|
@ -245,19 +258,23 @@ const SubscriptionList = (props) => {
|
||||||
const SubscriptionItem = (props) => {
|
const SubscriptionItem = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [menuAnchorEl, setMenuAnchorEl] = useState(null);
|
||||||
|
|
||||||
const subscription = props.subscription;
|
const subscription = props.subscription;
|
||||||
const iconBadge = (subscription.new <= 99) ? subscription.new : "99+";
|
const iconBadge = (subscription.new <= 99) ? subscription.new : "99+";
|
||||||
const icon = (subscription.state === ConnectionState.Connecting)
|
|
||||||
? <CircularProgress size="24px"/>
|
|
||||||
: <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
|
|
||||||
const displayName = topicDisplayName(subscription);
|
const displayName = topicDisplayName(subscription);
|
||||||
const ariaLabel = (subscription.state === ConnectionState.Connecting)
|
const ariaLabel = (subscription.state === ConnectionState.Connecting)
|
||||||
? `${displayName} (${t("nav_button_connecting")})`
|
? `${displayName} (${t("nav_button_connecting")})`
|
||||||
: displayName;
|
: displayName;
|
||||||
|
const icon = (subscription.state === ConnectionState.Connecting)
|
||||||
|
? <CircularProgress size="24px"/>
|
||||||
|
: <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
navigate(routes.forSubscription(subscription));
|
navigate(routes.forSubscription(subscription));
|
||||||
await subscriptionManager.markNotificationsRead(subscription.id);
|
await subscriptionManager.markNotificationsRead(subscription.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite">
|
<ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite">
|
||||||
<ListItemIcon>{icon}</ListItemIcon>
|
<ListItemIcon>{icon}</ListItemIcon>
|
||||||
|
@ -283,6 +300,18 @@ const SubscriptionItem = (props) => {
|
||||||
<Tooltip title={t("nav_button_muted")}><NotificationsOffOutlined /></Tooltip>
|
<Tooltip title={t("nav_button_muted")}><NotificationsOffOutlined /></Tooltip>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
}
|
}
|
||||||
|
<ListItemIcon edge="end" sx={{minWidth: "26px"}}>
|
||||||
|
<IconButton size="small" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => setMenuAnchorEl(e.currentTarget)}>
|
||||||
|
<MoreVert fontSize="small"/>
|
||||||
|
</IconButton>
|
||||||
|
<Portal>
|
||||||
|
<SubscriptionPopup
|
||||||
|
subscription={subscription}
|
||||||
|
anchor={menuAnchorEl}
|
||||||
|
onClose={() => setMenuAnchorEl(null)}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
</ListItemIcon>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Menu} from "@mui/material";
|
import {Fade, Menu} from "@mui/material";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const PopupMenu = (props) => {
|
const PopupMenu = (props) => {
|
||||||
|
@ -10,6 +10,7 @@ const PopupMenu = (props) => {
|
||||||
open={props.open}
|
open={props.open}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
|
TransitionComponent={Fade}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
sx: {
|
sx: {
|
||||||
|
|
|
@ -35,18 +35,17 @@ import DialogTitle from "@mui/material/DialogTitle";
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
import userManager from "../app/UserManager";
|
import userManager from "../app/UserManager";
|
||||||
import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
|
import {playSound, shuffle, sounds, validUrl} from "../app/utils";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import accountApi, {Permission, Role, UnauthorizedError} from "../app/AccountApi";
|
import accountApi, {Permission, Role, UnauthorizedError} from "../app/AccountApi";
|
||||||
import {Pref, PrefGroup} from "./Pref";
|
import {Pref, PrefGroup} from "./Pref";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import {Info} from "@mui/icons-material";
|
||||||
import {Info, Public, PublicOff} from "@mui/icons-material";
|
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
|
||||||
import ReserveTopicSelect from "./ReserveTopicSelect";
|
|
||||||
import {AccountContext} from "./App";
|
import {AccountContext} from "./App";
|
||||||
import {useOutletContext} from "react-router-dom";
|
import {useOutletContext} from "react-router-dom";
|
||||||
|
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
|
||||||
|
import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs";
|
||||||
|
|
||||||
const Preferences = () => {
|
const Preferences = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -496,22 +495,6 @@ const Reservations = () => {
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogCancel = () => {
|
|
||||||
setDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDialogSubmit = async (reservation) => {
|
|
||||||
setDialogOpen(false);
|
|
||||||
try {
|
|
||||||
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
|
|
||||||
await accountApi.sync();
|
|
||||||
console.debug(`[Preferences] Added topic reservation`, reservation);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[Preferences] Error topic reservation.`, e);
|
|
||||||
}
|
|
||||||
// FIXME handle 401/403/409
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
|
<Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
|
||||||
<CardContent sx={{ paddingBottom: 1 }}>
|
<CardContent sx={{ paddingBottom: 1 }}>
|
||||||
|
@ -526,14 +509,11 @@ const Reservations = () => {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
|
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
|
||||||
|
<ReserveAddDialog
|
||||||
<ReservationsDialog
|
|
||||||
key={`reservationAddDialog${dialogKey}`}
|
key={`reservationAddDialog${dialogKey}`}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
reservation={null}
|
|
||||||
reservations={reservations}
|
reservations={reservations}
|
||||||
onCancel={handleDialogCancel}
|
onClose={() => setDialogOpen(false)}
|
||||||
onSubmit={handleDialogSubmit}
|
|
||||||
/>
|
/>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -543,8 +523,9 @@ const Reservations = () => {
|
||||||
const ReservationsTable = (props) => {
|
const ReservationsTable = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dialogKey, setDialogKey] = useState(0);
|
const [dialogKey, setDialogKey] = useState(0);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const [dialogReservation, setDialogReservation] = useState(null);
|
const [dialogReservation, setDialogReservation] = useState(null);
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const { subscriptions } = useOutletContext();
|
const { subscriptions } = useOutletContext();
|
||||||
const localSubscriptions = (subscriptions?.length > 0)
|
const localSubscriptions = (subscriptions?.length > 0)
|
||||||
? Object.assign(...subscriptions.filter(s => s.baseUrl === config.base_url).map(s => ({[s.topic]: s})))
|
? Object.assign(...subscriptions.filter(s => s.baseUrl === config.base_url).map(s => ({[s.topic]: s})))
|
||||||
|
@ -553,34 +534,13 @@ const ReservationsTable = (props) => {
|
||||||
const handleEditClick = (reservation) => {
|
const handleEditClick = (reservation) => {
|
||||||
setDialogKey(prev => prev+1);
|
setDialogKey(prev => prev+1);
|
||||||
setDialogReservation(reservation);
|
setDialogReservation(reservation);
|
||||||
setDialogOpen(true);
|
setEditDialogOpen(true);
|
||||||
};
|
|
||||||
|
|
||||||
const handleDialogCancel = () => {
|
|
||||||
setDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDialogSubmit = async (reservation) => {
|
|
||||||
setDialogOpen(false);
|
|
||||||
try {
|
|
||||||
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
|
|
||||||
await accountApi.sync();
|
|
||||||
console.debug(`[Preferences] Added topic reservation`, reservation);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[Preferences] Error topic reservation.`, e);
|
|
||||||
}
|
|
||||||
// FIXME handle 401/403/409
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = async (reservation) => {
|
const handleDeleteClick = async (reservation) => {
|
||||||
try {
|
setDialogKey(prev => prev+1);
|
||||||
await accountApi.deleteReservation(reservation.topic);
|
setDialogReservation(reservation);
|
||||||
await accountApi.sync();
|
setDeleteDialogOpen(true);
|
||||||
console.debug(`[Preferences] Deleted topic reservation`, reservation);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[Preferences] Error topic reservation.`, e);
|
|
||||||
}
|
|
||||||
// FIXME handle 401/403
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -604,32 +564,32 @@ const ReservationsTable = (props) => {
|
||||||
<TableCell aria-label={t("prefs_reservations_table_access_header")}>
|
<TableCell aria-label={t("prefs_reservations_table_access_header")}>
|
||||||
{reservation.everyone === Permission.READ_WRITE &&
|
{reservation.everyone === Permission.READ_WRITE &&
|
||||||
<>
|
<>
|
||||||
<Public fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
|
<PermissionReadWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
|
||||||
{t("prefs_reservations_table_everyone_read_write")}
|
{t("prefs_reservations_table_everyone_read_write")}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{reservation.everyone === Permission.READ_ONLY &&
|
{reservation.everyone === Permission.READ_ONLY &&
|
||||||
<>
|
<>
|
||||||
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
|
<PermissionRead size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
|
||||||
{t("prefs_reservations_table_everyone_read_only")}
|
{t("prefs_reservations_table_everyone_read_only")}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{reservation.everyone === Permission.WRITE_ONLY &&
|
{reservation.everyone === Permission.WRITE_ONLY &&
|
||||||
<>
|
<>
|
||||||
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
|
<PermissionWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
|
||||||
{t("prefs_reservations_table_everyone_write_only")}
|
{t("prefs_reservations_table_everyone_write_only")}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{reservation.everyone === Permission.DENY_ALL &&
|
{reservation.everyone === Permission.DENY_ALL &&
|
||||||
<>
|
<>
|
||||||
<LockIcon fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
|
<PermissionDenyAll size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
|
||||||
{t("prefs_reservations_table_everyone_deny_all")}
|
{t("prefs_reservations_table_everyone_deny_all")}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
|
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
|
||||||
{!localSubscriptions[reservation.topic] &&
|
{!localSubscriptions[reservation.topic] &&
|
||||||
<Chip icon={<Info/>} label="Not subscribed" color="primary" variant="outlined"/>
|
<Chip icon={<Info/>} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/>
|
||||||
}
|
}
|
||||||
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
|
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
|
||||||
<EditIcon/>
|
<EditIcon/>
|
||||||
|
@ -641,79 +601,23 @@ const ReservationsTable = (props) => {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<ReservationsDialog
|
<ReserveEditDialog
|
||||||
key={`reservationEditDialog${dialogKey}`}
|
key={`reservationEditDialog${dialogKey}`}
|
||||||
open={dialogOpen}
|
open={editDialogOpen}
|
||||||
reservation={dialogReservation}
|
reservation={dialogReservation}
|
||||||
reservations={props.reservations}
|
reservations={props.reservations}
|
||||||
onCancel={handleDialogCancel}
|
onClose={() => setEditDialogOpen(false)}
|
||||||
onSubmit={handleDialogSubmit}
|
/>
|
||||||
|
<ReserveDeleteDialog
|
||||||
|
key={`reservationDeleteDialog${dialogKey}`}
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
topic={dialogReservation?.topic}
|
||||||
|
onClose={() => setDeleteDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReservationsDialog = (props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [topic, setTopic] = useState("");
|
|
||||||
const [everyone, setEveryone] = useState("deny-all");
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
const editMode = props.reservation !== null;
|
|
||||||
const addButtonEnabled = (() => {
|
|
||||||
if (editMode) {
|
|
||||||
return true;
|
|
||||||
} else if (!validTopic(topic)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return props.reservations
|
|
||||||
.filter(r => r.topic === topic)
|
|
||||||
.length === 0;
|
|
||||||
})();
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
props.onSubmit({
|
|
||||||
topic: (editMode) ? props.reservation.topic : topic,
|
|
||||||
everyone: everyone
|
|
||||||
})
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (editMode) {
|
|
||||||
setTopic(props.reservation.topic);
|
|
||||||
setEveryone(props.reservation.everyone);
|
|
||||||
}
|
|
||||||
}, [editMode, props.reservation]);
|
|
||||||
return (
|
|
||||||
<Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
|
||||||
<DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
{t("prefs_reservations_dialog_description")}
|
|
||||||
</DialogContentText>
|
|
||||||
{!editMode && <TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
id="topic"
|
|
||||||
label={t("prefs_reservations_dialog_topic_label")}
|
|
||||||
aria-label={t("prefs_reservations_dialog_topic_label")}
|
|
||||||
value={topic}
|
|
||||||
onChange={ev => setTopic(ev.target.value)}
|
|
||||||
type="url"
|
|
||||||
fullWidth
|
|
||||||
variant="standard"
|
|
||||||
/>}
|
|
||||||
<ReserveTopicSelect
|
|
||||||
value={everyone}
|
|
||||||
onChange={setEveryone}
|
|
||||||
sx={{mt: 1}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<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 maybeUpdateAccountSettings = async (payload) => {
|
const maybeUpdateAccountSettings = async (payload) => {
|
||||||
if (!session.exists()) {
|
if (!session.exists()) {
|
||||||
return;
|
return;
|
||||||
|
|
206
web/src/components/ReserveDialogs.js
Normal file
206
web/src/components/ReserveDialogs.js
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useContext, useEffect, useState} from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Autocomplete,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
useMediaQuery
|
||||||
|
} from "@mui/material";
|
||||||
|
import theme from "./theme";
|
||||||
|
import api from "../app/Api";
|
||||||
|
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
||||||
|
import userManager from "../app/UserManager";
|
||||||
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
|
import poller from "../app/Poller";
|
||||||
|
import DialogFooter from "./DialogFooter";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import session from "../app/Session";
|
||||||
|
import routes from "./routes";
|
||||||
|
import accountApi, {Permission, Role, TopicReservedError, UnauthorizedError} from "../app/AccountApi";
|
||||||
|
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||||
|
import {AccountContext} from "./App";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import {Check, DeleteForever} from "@mui/icons-material";
|
||||||
|
|
||||||
|
export const ReserveAddDialog = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [topic, setTopic] = useState(props.topic || "");
|
||||||
|
const [everyone, setEveryone] = useState(Permission.DENY_ALL);
|
||||||
|
const [errorText, setErrorText] = useState("");
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const allowTopicEdit = !props.topic;
|
||||||
|
const alreadyReserved = props.reservations.filter(r => r.topic === topic).length > 0;
|
||||||
|
const submitButtonEnabled = validTopic(topic) && !alreadyReserved;
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await accountApi.upsertReservation(topic, everyone);
|
||||||
|
console.debug(`[ReserveAddDialog] Added reservation for topic ${t}: ${everyone}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ReserveAddDialog] Error adding topic reservation.`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
} else if ((e instanceof TopicReservedError)) {
|
||||||
|
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
// FIXME handle 401/403/409
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
|
<DialogTitle>{t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t("prefs_reservations_dialog_description")}
|
||||||
|
</DialogContentText>
|
||||||
|
{allowTopicEdit && <TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="topic"
|
||||||
|
label={t("prefs_reservations_dialog_topic_label")}
|
||||||
|
aria-label={t("prefs_reservations_dialog_topic_label")}
|
||||||
|
value={topic}
|
||||||
|
onChange={ev => setTopic(ev.target.value)}
|
||||||
|
type="url"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
/>}
|
||||||
|
<ReserveTopicSelect
|
||||||
|
value={everyone}
|
||||||
|
onChange={setEveryone}
|
||||||
|
sx={{mt: 1}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter status={errorText}>
|
||||||
|
<Button onClick={props.onClose}>{t("prefs_users_dialog_button_cancel")}</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!submitButtonEnabled}>{t("prefs_users_dialog_button_add")}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReserveEditDialog = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL);
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await accountApi.upsertReservation(props.reservation.topic, everyone);
|
||||||
|
console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ReserveEditDialog] Error updating topic reservation.`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
// FIXME handle 401/403/409
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
|
<DialogTitle>{t("prefs_reservations_dialog_title_edit")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t("prefs_reservations_dialog_description")}
|
||||||
|
</DialogContentText>
|
||||||
|
<ReserveTopicSelect
|
||||||
|
value={everyone}
|
||||||
|
onChange={setEveryone}
|
||||||
|
sx={{mt: 1}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||||
|
<Button onClick={handleSubmit}>{t("common_save")}</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReserveDeleteDialog = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [deleteMessages, setDeleteMessages] = useState(false);
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await accountApi.deleteReservation(props.topic, deleteMessages);
|
||||||
|
console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${t}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
// FIXME handle 401/403/409
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
|
<DialogTitle>{t("prefs_reservations_dialog_title_delete")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t("reservation_delete_dialog_description")}
|
||||||
|
</DialogContentText>
|
||||||
|
<FormControl fullWidth variant="standard">
|
||||||
|
<Select
|
||||||
|
value={deleteMessages}
|
||||||
|
onChange={(ev) => setDeleteMessages(ev.target.value)}
|
||||||
|
sx={{
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: "4px",
|
||||||
|
paddingBottom: "4px",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>
|
||||||
|
<ListItemIcon><Check/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("reservation_delete_dialog_action_keep_title")}/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={true}>
|
||||||
|
<ListItemIcon><DeleteForever/></ListItemIcon>
|
||||||
|
<ListItemText primary={t("reservation_delete_dialog_action_delete_title")}/>
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{!deleteMessages &&
|
||||||
|
<Alert severity="info" sx={{ mt: 1 }}>
|
||||||
|
{t("reservation_delete_dialog_action_keep_description")}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
{deleteMessages &&
|
||||||
|
<Alert severity="warning" sx={{ mt: 1 }}>
|
||||||
|
{t("reservation_delete_dialog_action_delete_description")}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||||
|
<Button onClick={handleSubmit} color="error">{t("reservation_delete_dialog_submit_button")}</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -3,43 +3,44 @@ import {Lock, Public} from "@mui/icons-material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
export const PermissionReadWrite = React.forwardRef((props, ref) => {
|
export const PermissionReadWrite = React.forwardRef((props, ref) => {
|
||||||
const size = props.size ?? "medium";
|
return <PermissionInternal icon={Public} ref={ref} {...props}/>;
|
||||||
return <Public fontSize={size} ref={ref} {...props}/>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PermissionDenyAll = React.forwardRef((props, ref) => {
|
export const PermissionDenyAll = React.forwardRef((props, ref) => {
|
||||||
const size = props.size ?? "medium";
|
return <PermissionInternal icon={Lock} ref={ref} {...props}/>;
|
||||||
return <Lock fontSize={size} ref={ref} {...props}/>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PermissionRead = React.forwardRef((props, ref) => {
|
export const PermissionRead = React.forwardRef((props, ref) => {
|
||||||
return <PermissionReadOrWrite text="R" ref={ref} {...props}/>;
|
return <PermissionInternal icon={Public} text="R" ref={ref} {...props}/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PermissionWrite = React.forwardRef((props, ref) => {
|
export const PermissionWrite = React.forwardRef((props, ref) => {
|
||||||
return <PermissionReadOrWrite text="W" ref={ref} {...props}/>;
|
return <PermissionInternal icon={Public} text="W" ref={ref} {...props}/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const PermissionReadOrWrite = React.forwardRef((props, ref) => {
|
const PermissionInternal = React.forwardRef((props, ref) => {
|
||||||
const size = props.size ?? "medium";
|
const size = props.size ?? "medium";
|
||||||
|
const Icon = props.icon;
|
||||||
return (
|
return (
|
||||||
<div ref={ref} {...props} style={{position: "relative", display: "inline-flex", verticalAlign: "middle", height: "24px"}}>
|
<Box ref={ref} {...props} style={{ position: "relative", display: "inline-flex", verticalAlign: "middle", height: "24px" }}>
|
||||||
<Public fontSize={size}/>
|
<Icon fontSize={size} sx={{ color: "gray" }}/>
|
||||||
<Box
|
{props.text &&
|
||||||
sx={{
|
<Box
|
||||||
position: "absolute",
|
sx={{
|
||||||
right: "-6px",
|
position: "absolute",
|
||||||
bottom: "5px",
|
right: "-6px",
|
||||||
fontSize: 10,
|
bottom: "5px",
|
||||||
fontWeight: 600,
|
fontSize: 10,
|
||||||
color: "gray",
|
fontWeight: 600,
|
||||||
width: "8px",
|
color: "gray",
|
||||||
height: "8px",
|
width: "8px",
|
||||||
marginTop: "3px"
|
height: "8px",
|
||||||
}}
|
marginTop: "3px"
|
||||||
>
|
}}
|
||||||
{props.text}
|
>
|
||||||
</Box>
|
{props.text}
|
||||||
</div>
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -188,11 +188,11 @@ const SubscribePage = (props) => {
|
||||||
checked={reserveTopicVisible}
|
checked={reserveTopicVisible}
|
||||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
"aria-label": t("reserve_dialog_checkbox_label")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
label={t("reserve_dialog_checkbox_label")}
|
||||||
/>
|
/>
|
||||||
{reserveTopicVisible &&
|
{reserveTopicVisible &&
|
||||||
<ReserveTopicSelect
|
<ReserveTopicSelect
|
||||||
|
|
252
web/src/components/SubscriptionPopup.js
Normal file
252
web/src/components/SubscriptionPopup.js
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useContext, useState} from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import {InputAdornment, Portal, Snackbar, useMediaQuery} from "@mui/material";
|
||||||
|
import theme from "./theme";
|
||||||
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
|
import DialogFooter from "./DialogFooter";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import accountApi, {Permission, UnauthorizedError} from "../app/AccountApi";
|
||||||
|
import session from "../app/Session";
|
||||||
|
import routes from "./routes";
|
||||||
|
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import PopupMenu from "./PopupMenu";
|
||||||
|
import {formatShortDateTime, shuffle} from "../app/utils";
|
||||||
|
import api from "../app/Api";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import {Clear} from "@mui/icons-material";
|
||||||
|
import {AccountContext} from "./App";
|
||||||
|
import {ReserveEditDialog, ReserveAddDialog, ReserveDeleteDialog} from "./ReserveDialogs";
|
||||||
|
|
||||||
|
const SubscriptionPopup = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { account } = useContext(AccountContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false);
|
||||||
|
const [reserveAddDialogOpen, setReserveAddDialogOpen] = useState(false);
|
||||||
|
const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false);
|
||||||
|
const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false);
|
||||||
|
const [showPublishError, setShowPublishError] = useState(false);
|
||||||
|
const subscription = props.subscription;
|
||||||
|
const placement = props.placement ?? "left";
|
||||||
|
const reservations = account?.reservations || [];
|
||||||
|
|
||||||
|
const showReservationAdd = !subscription?.reservation && account?.stats.reservations_remaining > 0;
|
||||||
|
const showReservationAddDisabled = !subscription?.reservation && account?.stats.reservations_remaining === 0;
|
||||||
|
const showReservationEdit = !!subscription?.reservation;
|
||||||
|
const showReservationDelete = !!subscription?.reservation;
|
||||||
|
|
||||||
|
const handleChangeDisplayName = async () => {
|
||||||
|
setDisplayNameDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReserveAdd = async () => {
|
||||||
|
setReserveAddDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReserveEdit = async () => {
|
||||||
|
setReserveEditDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReserveDelete = async () => {
|
||||||
|
setReserveDeleteDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendTestMessage = async () => {
|
||||||
|
const baseUrl = props.subscription.baseUrl;
|
||||||
|
const topic = props.subscription.topic;
|
||||||
|
const tags = shuffle([
|
||||||
|
"grinning", "octopus", "upside_down_face", "palm_tree", "maple_leaf", "apple", "skull", "warning", "jack_o_lantern",
|
||||||
|
"de-server-1", "backups", "cron-script", "script-error", "phils-automation", "mouse", "go-rocks", "hi-ben"])
|
||||||
|
.slice(0, Math.round(Math.random() * 4));
|
||||||
|
const priority = shuffle([1, 2, 3, 4, 5])[0];
|
||||||
|
const title = shuffle([
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"", // Higher chance of no title
|
||||||
|
"Oh my, another test message?",
|
||||||
|
"Titles are optional, did you know that?",
|
||||||
|
"ntfy is open source, and will always be free. Cool, right?",
|
||||||
|
"I don't really like apples",
|
||||||
|
"My favorite TV show is The Wire. You should watch it!",
|
||||||
|
"You can attach files and URLs to messages too",
|
||||||
|
"You can delay messages up to 3 days"
|
||||||
|
])[0];
|
||||||
|
const nowSeconds = Math.round(Date.now()/1000);
|
||||||
|
const message = shuffle([
|
||||||
|
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`,
|
||||||
|
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
|
||||||
|
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
|
||||||
|
`Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
|
||||||
|
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
|
||||||
|
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
|
||||||
|
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`
|
||||||
|
])[0];
|
||||||
|
try {
|
||||||
|
await api.publish(baseUrl, topic, message, {
|
||||||
|
title: title,
|
||||||
|
priority: priority,
|
||||||
|
tags: tags
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ActionBar] Error publishing message`, e);
|
||||||
|
setShowPublishError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearAll = async () => {
|
||||||
|
console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
|
||||||
|
await subscriptionManager.deleteNotifications(props.subscription.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnsubscribe = async (event) => {
|
||||||
|
console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
|
||||||
|
await subscriptionManager.remove(props.subscription.id);
|
||||||
|
if (session.exists() && props.subscription.remoteId) {
|
||||||
|
try {
|
||||||
|
await accountApi.deleteSubscription(props.subscription.remoteId);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ActionBar] Error unsubscribing`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newSelected = await subscriptionManager.first(); // May be undefined
|
||||||
|
if (newSelected && !newSelected.internal) {
|
||||||
|
navigate(routes.forSubscription(newSelected));
|
||||||
|
} else {
|
||||||
|
navigate(routes.app);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PopupMenu
|
||||||
|
horizontal={placement}
|
||||||
|
anchorEl={props.anchor}
|
||||||
|
open={!!props.anchor}
|
||||||
|
onClose={props.onClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleChangeDisplayName}>{t("action_bar_change_display_name")}</MenuItem>
|
||||||
|
{showReservationAdd && <MenuItem onClick={handleReserveAdd}>{t("action_bar_reservation_add")}</MenuItem>}
|
||||||
|
{showReservationAddDisabled && <MenuItem disabled={true}>{t("action_bar_reservation_add")}</MenuItem>}
|
||||||
|
{showReservationEdit && <MenuItem onClick={handleReserveEdit}>{t("action_bar_reservation_edit")}</MenuItem>}
|
||||||
|
{showReservationDelete && <MenuItem onClick={handleReserveDelete}>{t("action_bar_reservation_delete")}</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>
|
||||||
|
</PopupMenu>
|
||||||
|
<Portal>
|
||||||
|
<Snackbar
|
||||||
|
open={showPublishError}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={() => setShowPublishError(false)}
|
||||||
|
message={t("message_bar_error_publishing")}
|
||||||
|
/>
|
||||||
|
<DisplayNameDialog
|
||||||
|
open={displayNameDialogOpen}
|
||||||
|
subscription={subscription}
|
||||||
|
onClose={() => setDisplayNameDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
{showReservationAdd &&
|
||||||
|
<ReserveAddDialog
|
||||||
|
open={reserveAddDialogOpen}
|
||||||
|
topic={subscription.topic}
|
||||||
|
reservations={reservations}
|
||||||
|
onClose={() => setReserveAddDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{showReservationEdit &&
|
||||||
|
<ReserveEditDialog
|
||||||
|
open={reserveEditDialogOpen}
|
||||||
|
reservation={subscription.reservation}
|
||||||
|
reservations={props.reservations}
|
||||||
|
onClose={() => setReserveEditDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{showReservationDelete &&
|
||||||
|
<ReserveDeleteDialog
|
||||||
|
open={reserveDeleteDialogOpen}
|
||||||
|
topic={subscription.topic}
|
||||||
|
onClose={() => setReserveDeleteDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Portal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DisplayNameDialog = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const subscription = props.subscription;
|
||||||
|
const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// Apply locally
|
||||||
|
await subscriptionManager.setDisplayName(subscription.id, displayName);
|
||||||
|
|
||||||
|
// Apply remotely
|
||||||
|
if (session.exists() && subscription.remoteId) {
|
||||||
|
try {
|
||||||
|
console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`);
|
||||||
|
await accountApi.updateSubscription(subscription.remoteId, { display_name: displayName });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e);
|
||||||
|
if ((e instanceof UnauthorizedError)) {
|
||||||
|
session.resetAndRedirect(routes.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME handle 409
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||||
|
<DialogTitle>{t("display_name_dialog_title")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t("display_name_dialog_description")}
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
placeholder={t("display_name_dialog_placeholder")}
|
||||||
|
value={displayName}
|
||||||
|
onChange={ev => setDisplayName(ev.target.value)}
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: 64,
|
||||||
|
"aria-label": t("display_name_dialog_placeholder")
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={() => setDisplayName("")} edge="end">
|
||||||
|
<Clear/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
|
||||||
|
<Button onClick={handleSave}>{t("common_save")}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubscriptionPopup;
|
|
@ -1,115 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import {useState} from 'react';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Dialog from '@mui/material/Dialog';
|
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
|
||||||
import {Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
|
||||||
import theme from "./theme";
|
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
|
||||||
import DialogFooter from "./DialogFooter";
|
|
||||||
import {useTranslation} from "react-i18next";
|
|
||||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
|
||||||
import session from "../app/Session";
|
|
||||||
import routes from "./routes";
|
|
||||||
import ReserveTopicSelect from "./ReserveTopicSelect";
|
|
||||||
|
|
||||||
const SubscriptionSettingsDialog = (props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const subscription = props.subscription;
|
|
||||||
const [reserveTopicVisible, setReserveTopicVisible] = useState(!!subscription.reservation);
|
|
||||||
const [everyone, setEveryone] = useState(subscription.reservation?.everyone || "deny-all");
|
|
||||||
const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
// Apply locally
|
|
||||||
await subscriptionManager.setDisplayName(subscription.id, displayName);
|
|
||||||
|
|
||||||
// Apply remotely
|
|
||||||
if (session.exists() && subscription.remoteId) {
|
|
||||||
try {
|
|
||||||
// Display name
|
|
||||||
console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`);
|
|
||||||
await accountApi.updateSubscription(subscription.remoteId, { display_name: displayName });
|
|
||||||
|
|
||||||
// Reservation
|
|
||||||
if (reserveTopicVisible) {
|
|
||||||
await accountApi.upsertReservation(subscription.topic, everyone);
|
|
||||||
} else if (!reserveTopicVisible && subscription.reservation) { // Was removed
|
|
||||||
await accountApi.deleteReservation(subscription.topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync account
|
|
||||||
await accountApi.sync();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e);
|
|
||||||
if ((e instanceof UnauthorizedError)) {
|
|
||||||
session.resetAndRedirect(routes.login);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME handle 409
|
|
||||||
}
|
|
||||||
}
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
|
|
||||||
<DialogTitle>{t("subscription_settings_dialog_title")}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
{t("subscription_settings_dialog_description")}
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
id="topic"
|
|
||||||
placeholder={t("subscription_settings_dialog_display_name_placeholder")}
|
|
||||||
value={displayName}
|
|
||||||
onChange={ev => setDisplayName(ev.target.value)}
|
|
||||||
type="text"
|
|
||||||
fullWidth
|
|
||||||
variant="standard"
|
|
||||||
inputProps={{
|
|
||||||
maxLength: 64,
|
|
||||||
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{config.enable_reservations && session.exists() &&
|
|
||||||
<>
|
|
||||||
<FormControlLabel
|
|
||||||
fullWidth
|
|
||||||
variant="standard"
|
|
||||||
sx={{pt: 1}}
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={reserveTopicVisible}
|
|
||||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
|
||||||
inputProps={{
|
|
||||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
|
||||||
/>
|
|
||||||
{reserveTopicVisible &&
|
|
||||||
<ReserveTopicSelect
|
|
||||||
value={everyone}
|
|
||||||
onChange={setEveryone}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={props.onClose}>{t("subscription_settings_button_cancel")}</Button>
|
|
||||||
<Button onClick={handleSave}>{t("subscription_settings_button_save")}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubscriptionSettingsDialog;
|
|
Loading…
Reference in a new issue