Reserved topic stuff

This commit is contained in:
binwiederhier 2023-01-04 20:34:22 -05:00
parent 6c0429351a
commit a91da7cf2c
11 changed files with 240 additions and 133 deletions

View file

@ -108,6 +108,7 @@ type Config struct {
EnableLogin bool
EnableEmailConfirm bool
EnableResetPassword bool
EnableAccountUpgrades bool
Version string // injected by App
}

View file

@ -40,12 +40,12 @@ import (
message cache duration
Keep 10000 messages or keep X days?
Attachment expiration based on plan
reserve topics
purge accounts that were not logged into in X
reset daily limits for users
Account usage not updated "in real time"
max token issue limit
user db startup queries -> foreign keys
UI
- Feature flag for "reserve topic" feature
Sync:
- "mute" setting
- figure out what settings are "web" or "phone"
@ -447,17 +447,20 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
if !s.config.WebRootIsApp {
appRoot = "/app"
}
disallowedTopicsStr := `"` + strings.Join(disallowedTopics, `", "`) + `"`
response := &apiConfigResponse{
BaseURL: "", // Will translate to window.location.origin
AppRoot: appRoot,
EnableLogin: s.config.EnableLogin,
EnableSignup: s.config.EnableSignup,
EnableResetPassword: s.config.EnableResetPassword,
DisallowedTopics: disallowedTopics,
}
b, err := json.Marshal(response)
if err != nil {
return err
}
w.Header().Set("Content-Type", "text/javascript")
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
var config = {
baseUrl: window.location.origin,
appRoot: "%s",
enableLogin: %t,
enableSignup: %t,
enableResetPassword: %t,
disallowedTopics: [%s],
};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
return err
}

View file

@ -280,3 +280,12 @@ type apiAccountAccessRequest struct {
Topic string `json:"topic"`
Everyone string `json:"everyone"`
}
type apiConfigResponse struct {
BaseURL string `json:"base_url"`
AppRoot string `json:"app_root"`
EnableLogin bool `json:"enable_login"`
EnableSignup bool `json:"enable_signup"`
EnableResetPassword bool `json:"enable_reset_password"`
DisallowedTopics []string `json:"disallowed_topics"`
}

View file

@ -83,6 +83,7 @@
"subscription_settings_dialog_title": "Subscription settings",
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
"subscription_settings_dialog_display_name_placeholder": "Display name",
"subscription_settings_dialog_reserve_topic_label": "Reserve topic and configure access",
"subscription_settings_button_cancel": "Cancel",
"subscription_settings_button_save": "Save",
"notifications_loading": "Loading notifications …",
@ -159,6 +160,7 @@
"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_topic_already_reserved": "Topic already reserved",
"subscribe_dialog_error_user_anonymous": "anonymous",
"account_basics_title": "Account",
"account_basics_username_title": "Username",
@ -253,6 +255,7 @@
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
"prefs_reservations_dialog_title_add": "Reserve topic",
"prefs_reservations_dialog_title_edit": "Edit reserved 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_access_label": "Access",
"priority_min": "min",

View file

@ -231,6 +231,8 @@ class AccountApi {
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status === 409) {
throw new TopicReservedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
@ -312,6 +314,13 @@ export class UsernameTakenError extends Error {
}
}
export class TopicReservedError extends Error {
constructor(topic) {
super("Topic already reserved");
this.topic = topic;
}
}
export class AccountCreateLimitReachedError extends Error {
constructor() {
super("Account creation limit reached");

View file

@ -1,2 +1,7 @@
const config = window.config;
if (config.base_url === "") {
config.base_url = window.location.origin;
}
export default config;

View file

@ -177,7 +177,7 @@ const Stats = () => {
<PrefGroup>
<Pref title={t("account_usage_plan_title")}>
<div>
{account?.role === "admin"
{account.role === "admin"
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
: t(`account_usage_plan_code_${planCode}`)}
</div>
@ -187,28 +187,44 @@ const Stats = () => {
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress variant="determinate" value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100} />
<LinearProgress
variant="determinate"
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
color={account?.role !== "admin" && account.stats.topics_remaining === 0 ? 'error' : 'primary'}
/>
</Pref>
<Pref title={t("account_usage_messages_title")}>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
<LinearProgress
variant="determinate"
value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100}
color={account?.role !== "admin" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
/>
</Pref>
<Pref title={t("account_usage_emails_title")}>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
<LinearProgress
variant="determinate"
value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100}
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
/>
</Pref>
<Pref title={t("account_usage_attachment_storage_title")} subtitle={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}>
<Pref title={t("account_usage_attachment_storage_title")} subtitle={account.role !== "admin" ? t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) }) : null}>
<div>
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
<LinearProgress
variant="determinate"
value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
color={account.role !== "admin" && account.stats.attachment_total_size_remaining === 0 ? 'error' : 'primary'}
/>
</Pref>
</PrefGroup>
{account.limits.basis === "ip" &&

View file

@ -1,6 +1,7 @@
import * as React from 'react';
import {useEffect, useState} from 'react';
import {
Alert,
CardActions,
CardContent,
FormControl,
@ -44,6 +45,8 @@ import LockIcon from "@mui/icons-material/Lock";
import {Public, PublicOff} from "@mui/icons-material";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import DialogContentText from "@mui/material/DialogContentText";
import ReserveTopicSelect from "./ReserveTopicSelect";
const Preferences = () => {
return (
@ -482,10 +485,11 @@ const Reservations = () => {
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
if (!session.exists() || !account) {
if (!session.exists() || !account || account.role === "admin") {
return <></>;
}
const reservations = account.reservations || [];
const limitReached = account.role === "user" && account.stats.topics_remaining === 0;
const handleAddClick = () => {
setDialogKey(prev => prev+1);
@ -505,7 +509,7 @@ const Reservations = () => {
} catch (e) {
console.log(`[Preferences] Error topic reservation.`, e);
}
// FIXME handle 401/403
// FIXME handle 401/403/409
};
return (
@ -518,9 +522,15 @@ const Reservations = () => {
{t("prefs_reservations_description")}
</Paragraph>
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
{limitReached &&
<Alert severity="info">
You reached your reserved topics limit.
</Alert>
}
</CardContent>
<CardActions>
<Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
<ReservationsDialog
key={`reservationAddDialog${dialogKey}`}
open={dialogOpen}
@ -559,7 +569,7 @@ const ReservationsTable = (props) => {
} catch (e) {
console.log(`[Preferences] Error topic reservation.`, e);
}
// FIXME handle 401/403
// FIXME handle 401/403/409
};
const handleDeleteClick = async (reservation) => {
@ -670,6 +680,9 @@ const ReservationsDialog = (props) => {
<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"
@ -682,37 +695,11 @@ const ReservationsDialog = (props) => {
fullWidth
variant="standard"
/>}
<FormControl fullWidth variant="standard">
<Select
<ReserveTopicSelect
value={everyone}
onChange={(ev) => setEveryone(ev.target.value)}
aria-label={t("prefs_reservations_dialog_access_label")}
sx={{
marginTop: 1,
"& .MuiSelect-select": {
display: 'flex',
alignItems: 'center'
}
}}
>
<MenuItem value="deny-all">
<ListItemIcon><LockIcon /></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
</MenuItem>
<MenuItem value="read-only">
<ListItemIcon><PublicOff /></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
</MenuItem>
<MenuItem value="write-only">
<ListItemIcon><PublicOff /></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
</MenuItem>
<MenuItem value="read-write">
<ListItemIcon><Public /></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
</MenuItem>
</Select>
</FormControl>
onChange={setEveryone}
sx={{mt: 1}}
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>

View file

@ -0,0 +1,62 @@
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, FormControl, FormControlLabel, Select, 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 MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import LockIcon from "@mui/icons-material/Lock";
import ListItemText from "@mui/material/ListItemText";
import {Public, PublicOff} from "@mui/icons-material";
const ReserveTopicSelect = (props) => {
const { t } = useTranslation();
const sx = props.sx || {};
return (
<FormControl fullWidth variant="standard" sx={sx}>
<Select
value={props.value}
onChange={(ev) => props.onChange(ev.target.value)}
aria-label={t("prefs_reservations_dialog_access_label")}
sx={{
"& .MuiSelect-select": {
display: 'flex',
alignItems: 'center',
paddingTop: "4px",
paddingBottom: "4px",
}
}}
>
<MenuItem value="deny-all">
<ListItemIcon><LockIcon/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
</MenuItem>
<MenuItem value="read-only">
<ListItemIcon><PublicOff/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
</MenuItem>
<MenuItem value="write-only">
<ListItemIcon><PublicOff/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
</MenuItem>
<MenuItem value="read-write">
<ListItemIcon><Public/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
</MenuItem>
</Select>
</FormControl>
);
};
export default ReserveTopicSelect;

View file

@ -6,7 +6,7 @@ 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 {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material";
import theme from "./theme";
import api from "../app/Api";
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
@ -17,14 +17,14 @@ import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import IconButton from "@mui/material/IconButton";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import PublicIcon from '@mui/icons-material/Public';
import LockIcon from '@mui/icons-material/Lock';
import PublicOffIcon from '@mui/icons-material/PublicOff';
import MenuItem from "@mui/material/MenuItem";
import PopupMenu from "./PopupMenu";
import ListItemIcon from "@mui/material/ListItemIcon";
import ReserveTopicSelect from "./ReserveTopicSelect";
const publicBaseUrl = "https://ntfy.sh";
@ -33,6 +33,7 @@ const SubscribeDialog = (props) => {
const [topic, setTopic] = useState("");
const [showLoginPage, setShowLoginPage] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = async () => {
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
@ -44,6 +45,7 @@ const SubscribeDialog = (props) => {
topic: topic
});
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
await accountApi.sync();
} catch (e) {
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
if ((e instanceof UnauthorizedError)) {
@ -54,6 +56,7 @@ const SubscribeDialog = (props) => {
poller.pollInBackground(subscription); // Dangle!
props.onSuccess(subscription);
}
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
{!showLoginPage && <SubscribePage
@ -78,10 +81,11 @@ const SubscribeDialog = (props) => {
const SubscribePage = (props) => {
const { t } = useTranslation();
const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState("");
const [accessAnchorEl, setAccessAnchorEl] = useState(null);
const [access, setAccess] = useState("public");
const [everyone, setEveryone] = useState("deny-all");
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
const topic = props.topic;
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
@ -92,6 +96,8 @@ const SubscribePage = (props) => {
const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
// Check read access to topic
const success = await api.topicAuth(baseUrl, topic, user);
if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
@ -103,6 +109,24 @@ const SubscribePage = (props) => {
return;
}
}
// Reserve topic (if requested)
if (session.exists() && baseUrl === config.baseUrl && reserveTopicVisible) {
console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
try {
await accountApi.upsertAccess(topic, everyone);
// Account sync later after it was added
} catch (e) {
console.log(`[SubscribeDialog] Error reserving topic`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
} else if ((e instanceof TopicReservedError)) {
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
return;
}
}
}
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
props.onSuccess();
};
@ -137,14 +161,7 @@ const SubscribePage = (props) => {
<DialogContentText>
{t("subscribe_dialog_subscribe_description")}
</DialogContentText>
<div style={{display: 'flex'}} role="row">
{session.exists() &&
<IconButton onClick={(ev) => setAccessAnchorEl(ev.currentTarget)} color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
{access === "public" && <PublicIcon/>}
{access === "public-read" && <PublicOffIcon/>}
{access === "private" && <LockIcon/>}
</IconButton>
}
<div style={{display: 'flex', paddingBottom: "8px"}} role="row">
<TextField
autoFocus
margin="dense"
@ -168,19 +185,19 @@ const SubscribePage = (props) => {
open={!!accessAnchorEl}
onClose={() => setAccessAnchorEl(null)}
>
<MenuItem onClick={() => setAccess("private")} selected={access === "private"}>
<MenuItem onClick={() => setEveryone("private")} selected={everyone === "private"}>
<ListItemIcon>
<LockIcon fontSize="small" />
</ListItemIcon>
Only I can publish and subscribe
</MenuItem>
<MenuItem onClick={() => setAccess("public-read")} selected={access === "public-read"}>
<MenuItem onClick={() => setEveryone("public-read")} selected={everyone === "public-read"}>
<ListItemIcon>
<PublicOffIcon fontSize="small" />
</ListItemIcon>
I can publish, everyone can subscribe
</MenuItem>
<MenuItem onClick={() => setAccess("public")} selected={access === "public"}>
<MenuItem onClick={() => setEveryone("public")} selected={everyone === "public"}>
<ListItemIcon>
<PublicIcon fontSize="small" />
</ListItemIcon>
@ -188,8 +205,33 @@ const SubscribePage = (props) => {
</MenuItem>
</PopupMenu>
</div>
{session.exists() && !anotherServerVisible &&
<FormGroup>
<FormControlLabel
variant="standard"
control={
<Checkbox
fullWidth
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}
/>
}
</FormGroup>
}
{!reserveTopicVisible &&
<FormGroup>
<FormControlLabel
sx={{pt: 1}}
control={
<Checkbox
onChange={handleUseAnotherChanged}
@ -198,14 +240,13 @@ const SubscribePage = (props) => {
}}
/>
}
label={t("subscribe_dialog_subscribe_use_another_label")} />
label={t("subscribe_dialog_subscribe_use_another_label")}/>
{anotherServerVisible && <Autocomplete
freeSolo
options={existingBaseUrls}
sx={{ maxWidth: 400 }}
inputValue={props.baseUrl}
onInputChange={updateBaseUrl}
renderInput={ (params) =>
renderInput={(params) =>
<TextField
{...params}
placeholder={config.baseUrl}
@ -214,6 +255,8 @@ const SubscribePage = (props) => {
/>
}
/>}
</FormGroup>
}
</DialogContent>
<DialogFooter status={errorText}>
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>

View file

@ -6,7 +6,7 @@ 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, FormControl, FormControlLabel, Select, useMediaQuery} from "@mui/material";
import {Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
import theme from "./theme";
import subscriptionManager from "../app/SubscriptionManager";
import DialogFooter from "./DialogFooter";
@ -14,11 +14,7 @@ import {useTranslation} from "react-i18next";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import LockIcon from "@mui/icons-material/Lock";
import ListItemText from "@mui/material/ListItemText";
import {Public, PublicOff} from "@mui/icons-material";
import ReserveTopicSelect from "./ReserveTopicSelect";
const SubscriptionSettingsDialog = (props) => {
const { t } = useTranslation();
@ -53,6 +49,8 @@ const SubscriptionSettingsDialog = (props) => {
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
// FIXME handle 409
}
}
props.onClose();
@ -80,7 +78,6 @@ const SubscriptionSettingsDialog = (props) => {
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
}}
/>
<FormControlLabel
fullWidth
variant="standard"
@ -90,45 +87,17 @@ const SubscriptionSettingsDialog = (props) => {
checked={reserveTopicVisible}
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
inputProps={{
"aria-label": t("xxxxxxxxxxxxxxxxxx")
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
}}
/>
}
label={t("Reserve topic and configure custom access:")}
label={t("subscription_settings_dialog_reserve_topic_label")}
/>
{reserveTopicVisible &&
<FormControl variant="standard">
<Select
<ReserveTopicSelect
value={everyone}
onChange={(ev) => setEveryone(ev.target.value)}
aria-label={t("prefs_reservations_dialog_access_label")}
sx={{
"& .MuiSelect-select": {
display: 'flex',
alignItems: 'center',
paddingTop: "4px",
paddingBottom: "4px",
}
}}
>
<MenuItem value="deny-all">
<ListItemIcon><LockIcon/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
</MenuItem>
<MenuItem value="read-only">
<ListItemIcon><PublicOff/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
</MenuItem>
<MenuItem value="write-only">
<ListItemIcon><PublicOff/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
</MenuItem>
<MenuItem value="read-write">
<ListItemIcon><Public/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
</MenuItem>
</Select>
</FormControl>
onChange={setEveryone}
/>
}
</DialogContent>
<DialogFooter>