ntfy/web/src/components/Preferences.js

735 lines
30 KiB
JavaScript
Raw Normal View History

2022-03-01 10:56:38 +13:00
import * as React from 'react';
2023-01-10 14:37:13 +13:00
import {useContext, useEffect, useState} from 'react';
import {
2023-01-05 14:34:22 +13:00
Alert,
CardActions,
CardContent, Chip,
FormControl,
Select,
2023-01-03 16:21:11 +13:00
Stack,
Table,
TableBody,
TableCell,
TableHead,
2023-01-02 15:56:24 +13:00
TableRow,
Tooltip,
useMediaQuery
} from "@mui/material";
2022-03-01 10:56:38 +13:00
import Typography from "@mui/material/Typography";
2022-03-03 10:16:30 +13:00
import prefs from "../app/Prefs";
import {Paragraph} from "./styles";
import EditIcon from '@mui/icons-material/Edit';
import CloseIcon from "@mui/icons-material/Close";
import WarningIcon from '@mui/icons-material/Warning';
import IconButton from "@mui/material/IconButton";
2022-03-06 18:02:27 +13:00
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import Container from "@mui/material/Container";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Card from "@mui/material/Card";
import Button from "@mui/material/Button";
import {useLiveQuery} from "dexie-react-hooks";
import theme from "./theme";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import userManager from "../app/UserManager";
2023-01-03 15:52:20 +13:00
import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
2022-04-08 12:31:24 +12:00
import {useTranslation} from "react-i18next";
2022-12-08 15:26:18 +13:00
import session from "../app/Session";
import routes from "./routes";
2022-12-26 05:59:44 +13:00
import accountApi, {UnauthorizedError} from "../app/AccountApi";
2022-12-30 02:20:53 +13:00
import {Pref, PrefGroup} from "./Pref";
2023-01-02 15:56:24 +13:00
import LockIcon from "@mui/icons-material/Lock";
import {Check, Info, Public, PublicOff} from "@mui/icons-material";
2023-01-05 14:34:22 +13:00
import DialogContentText from "@mui/material/DialogContentText";
import ReserveTopicSelect from "./ReserveTopicSelect";
2023-01-10 14:37:13 +13:00
import {AccountContext} from "./App";
import {useOutletContext} from "react-router-dom";
import subscriptionManager from "../app/SubscriptionManager";
2022-03-01 10:56:38 +13:00
2022-03-05 10:10:04 +13:00
const Preferences = () => {
return (
2022-03-04 08:51:56 +13:00
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
<Stack spacing={3}>
<Notifications/>
2023-01-03 14:08:37 +13:00
<Reservations/>
2022-04-10 02:54:09 +12:00
<Users/>
2023-01-02 15:56:24 +13:00
<Appearance/>
</Stack>
</Container>
);
};
2022-03-02 16:01:51 +13:00
const Notifications = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
return (
2022-05-03 11:30:29 +12:00
<Card sx={{p: 3}} aria-label={t("prefs_notifications_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}>
2022-04-09 02:44:35 +12:00
{t("prefs_notifications_title")}
</Typography>
<PrefGroup>
2022-03-06 18:02:27 +13:00
<Sound/>
<MinPriority/>
<DeleteAfter/>
</PrefGroup>
</Card>
);
};
2022-03-06 18:02:27 +13:00
const Sound = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
2022-05-03 11:30:29 +12:00
const labelId = "prefSound";
2022-03-06 18:02:27 +13:00
const sound = useLiveQuery(async () => prefs.sound());
const handleChange = async (ev) => {
await prefs.setSound(ev.target.value);
await maybeUpdateAccountSettings({
notification: {
sound: ev.target.value
}
});
2022-03-06 18:02:27 +13:00
}
if (!sound) {
return null; // While loading
}
let description;
if (sound === "none") {
description = t("prefs_notifications_sound_description_none");
} else {
description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
}
2022-03-06 18:02:27 +13:00
return (
2022-05-03 11:30:29 +12:00
<Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}>
2022-03-06 18:02:27 +13:00
<div style={{ display: 'flex', width: '100%' }}>
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
2022-05-03 11:30:29 +12:00
<Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
2022-04-09 02:44:35 +12:00
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
{Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
2022-03-06 18:02:27 +13:00
</Select>
</FormControl>
2022-05-03 11:30:29 +12:00
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}>
2022-03-06 18:02:27 +13:00
<PlayArrowIcon />
</IconButton>
</div>
</Pref>
)
};
const MinPriority = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
2022-05-03 11:30:29 +12:00
const labelId = "prefMinPriority";
2022-03-06 18:02:27 +13:00
const minPriority = useLiveQuery(async () => prefs.minPriority());
2022-03-02 16:01:51 +13:00
const handleChange = async (ev) => {
2022-03-03 10:16:30 +13:00
await prefs.setMinPriority(ev.target.value);
await maybeUpdateAccountSettings({
notification: {
min_priority: ev.target.value
}
});
2022-03-02 16:01:51 +13:00
}
if (!minPriority) {
return null; // While loading
}
const priorities = {
1: t("priority_min"),
2: t("priority_low"),
3: t("priority_default"),
4: t("priority_high"),
5: t("priority_max")
}
let description;
if (minPriority === 1) {
description = t("prefs_notifications_min_priority_description_any");
} else if (minPriority === 5) {
description = t("prefs_notifications_min_priority_description_max");
} else {
description = t("prefs_notifications_min_priority_description_x_or_higher", {
number: minPriority,
name: priorities[minPriority]
});
}
return (
2022-05-03 11:30:29 +12:00
<Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
2022-05-03 11:30:29 +12:00
<Select value={minPriority} onChange={handleChange} aria-labelledby={labelId}>
2022-04-09 02:44:35 +12:00
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
<MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
<MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
<MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem>
<MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem>
</Select>
</FormControl>
</Pref>
)
};
const DeleteAfter = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
2022-05-03 11:30:29 +12:00
const labelId = "prefDeleteAfter";
2022-03-03 10:16:30 +13:00
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
2022-03-02 16:01:51 +13:00
const handleChange = async (ev) => {
2022-03-03 10:16:30 +13:00
await prefs.setDeleteAfter(ev.target.value);
await maybeUpdateAccountSettings({
notification: {
delete_after: ev.target.value
}
});
2022-03-02 16:01:51 +13:00
}
if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
2022-03-02 16:01:51 +13:00
return null; // While loading
}
const description = (() => {
switch (deleteAfter) {
case 0: return t("prefs_notifications_delete_after_never_description");
case 10800: return t("prefs_notifications_delete_after_three_hours_description");
case 86400: return t("prefs_notifications_delete_after_one_day_description");
case 604800: return t("prefs_notifications_delete_after_one_week_description");
case 2592000: return t("prefs_notifications_delete_after_one_month_description");
}
})();
return (
2022-05-03 11:30:29 +12:00
<Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
2022-05-03 11:30:29 +12:00
<Select value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}>
2022-04-09 02:44:35 +12:00
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
<MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
<MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
<MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem>
<MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem>
</Select>
</FormControl>
</Pref>
)
};
2022-03-02 16:01:51 +13:00
const Users = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const users = useLiveQuery(() => userManager.all());
const handleAddClick = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
const handleDialogSubmit = async (user) => {
setDialogOpen(false);
try {
await userManager.save(user);
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
} catch (e) {
console.log(`[Preferences] Error adding user.`, e);
}
};
return (
2022-05-03 11:30:29 +12:00
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
<CardContent sx={{ paddingBottom: 1 }}>
<Typography variant="h5" sx={{marginBottom: 2}}>
2022-04-09 02:44:35 +12:00
{t("prefs_users_title")}
</Typography>
<Paragraph>
2022-04-09 02:44:35 +12:00
{t("prefs_users_description")}
{session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>}
</Paragraph>
{users?.length > 0 && <UserTable users={users}/>}
</CardContent>
<CardActions>
2022-04-09 02:44:35 +12:00
<Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
<UserDialog
key={`userAddDialog${dialogKey}`}
open={dialogOpen}
user={null}
users={users}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
/>
</CardActions>
</Card>
2022-03-01 10:56:38 +13:00
);
};
const UserTable = (props) => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogUser, setDialogUser] = useState(null);
2022-12-31 08:20:48 +13:00
const handleEditClick = (user) => {
setDialogKey(prev => prev+1);
setDialogUser(user);
setDialogOpen(true);
};
2022-12-31 08:20:48 +13:00
const handleDialogCancel = () => {
setDialogOpen(false);
};
2022-12-31 08:20:48 +13:00
const handleDialogSubmit = async (user) => {
setDialogOpen(false);
try {
await userManager.save(user);
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
} catch (e) {
console.log(`[Preferences] Error updating user.`, e);
}
};
2022-12-31 08:20:48 +13:00
const handleDeleteClick = async (user) => {
try {
await userManager.delete(user.baseUrl);
console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
} catch (e) {
console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
}
};
2022-12-31 08:20:48 +13:00
return (
<Table size="small" aria-label={t("prefs_users_table")}>
<TableHead>
<TableRow>
<TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
<TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
<TableCell/>
</TableRow>
</TableHead>
<TableBody>
{props.users?.map(user => (
<TableRow
key={user.baseUrl}
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" sx={{paddingLeft: 0}}
aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
<TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
<TableCell align="right">
2023-01-05 16:47:12 +13:00
{(!session.exists() || user.baseUrl !== config.base_url) &&
<>
2022-12-31 08:20:48 +13:00
<IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
<EditIcon/>
</IconButton>
2022-12-31 08:20:48 +13:00
<IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
<CloseIcon/>
</IconButton>
</>
}
2023-01-05 16:47:12 +13:00
{session.exists() && user.baseUrl === config.base_url &&
2022-12-31 08:20:48 +13:00
<Tooltip title={t("prefs_users_table_cannot_delete_or_edit")}>
<span>
<IconButton disabled><EditIcon/></IconButton>
<IconButton disabled><CloseIcon/></IconButton>
</span>
</Tooltip>
}
</TableCell>
</TableRow>
))}
</TableBody>
<UserDialog
key={`userEditDialog${dialogKey}`}
open={dialogOpen}
user={dialogUser}
users={props.users}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
/>
</Table>
);
};
const UserDialog = (props) => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
const [baseUrl, setBaseUrl] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const editMode = props.user !== null;
const addButtonEnabled = (() => {
if (editMode) {
return username.length > 0 && password.length > 0;
}
const baseUrlValid = validUrl(baseUrl);
const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl);
return baseUrlValid
&& !baseUrlExists
&& username.length > 0
&& password.length > 0;
})();
const handleSubmit = async () => {
props.onSubmit({
baseUrl: baseUrl,
username: username,
password: password
})
};
useEffect(() => {
if (editMode) {
setBaseUrl(props.user.baseUrl);
setUsername(props.user.username);
setPassword(props.user.password);
}
}, [editMode, props.user]);
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
2022-04-09 02:44:35 +12:00
<DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle>
<DialogContent>
{!editMode && <TextField
autoFocus
margin="dense"
id="baseUrl"
2022-04-09 02:44:35 +12:00
label={t("prefs_users_dialog_base_url_label")}
2022-05-03 11:30:29 +12:00
aria-label={t("prefs_users_dialog_base_url_label")}
value={baseUrl}
onChange={ev => setBaseUrl(ev.target.value)}
type="url"
fullWidth
variant="standard"
/>}
<TextField
autoFocus={editMode}
margin="dense"
id="username"
2022-04-09 02:44:35 +12:00
label={t("prefs_users_dialog_username_label")}
2022-05-03 11:30:29 +12:00
aria-label={t("prefs_users_dialog_username_label")}
value={username}
onChange={ev => setUsername(ev.target.value)}
type="text"
fullWidth
variant="standard"
/>
<TextField
margin="dense"
id="password"
2022-04-09 02:44:35 +12:00
label={t("prefs_users_dialog_password_label")}
2022-05-03 11:30:29 +12:00
aria-label={t("prefs_users_dialog_password_label")}
type="password"
value={password}
onChange={ev => setPassword(ev.target.value)}
fullWidth
variant="standard"
/>
</DialogContent>
<DialogActions>
2022-04-09 02:44:35 +12:00
<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>
);
};
2022-04-08 12:31:24 +12:00
const Appearance = () => {
2022-04-09 02:44:35 +12:00
const { t } = useTranslation();
2022-04-08 12:31:24 +12:00
return (
2022-05-03 11:30:29 +12:00
<Card sx={{p: 3}} aria-label={t("prefs_appearance_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}>
2022-04-09 02:44:35 +12:00
{t("prefs_appearance_title")}
2022-04-08 12:31:24 +12:00
</Typography>
<PrefGroup>
<Language/>
</PrefGroup>
</Card>
);
};
const Language = () => {
const { t, i18n } = useTranslation();
2022-05-03 11:30:29 +12:00
const labelId = "prefLanguage";
2022-09-28 00:44:00 +13:00
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
2022-04-10 02:54:09 +12:00
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
2022-04-30 12:12:12 +12:00
const lang = i18n.language ?? "en";
2022-04-10 02:54:09 +12:00
2022-12-08 14:44:20 +13:00
const handleChange = async (ev) => {
await i18n.changeLanguage(ev.target.value);
await maybeUpdateAccountSettings({
language: ev.target.value
});
2022-12-08 14:44:20 +13:00
};
2022-04-10 02:54:09 +12:00
// Remember: Flags are not languages. Don't put flags next to the language in the list.
// Languages names from: https://www.omniglot.com/language/names.htm
2022-04-12 12:18:18 +12:00
// Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
2022-04-10 07:12:03 +12:00
2022-04-08 12:31:24 +12:00
return (
2022-05-03 11:30:29 +12:00
<Pref labelId={labelId} title={title}>
2022-04-08 12:31:24 +12:00
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
2022-12-08 14:44:20 +13:00
<Select value={lang} onChange={handleChange} aria-labelledby={labelId}>
2022-04-08 12:31:24 +12:00
<MenuItem value="en">English</MenuItem>
2022-05-27 08:38:09 +12:00
<MenuItem value="id">Bahasa Indonesia</MenuItem>
<MenuItem value="bg">Български</MenuItem>
2022-04-30 12:12:12 +12:00
<MenuItem value="cs">Čeština</MenuItem>
2022-06-01 16:03:56 +12:00
<MenuItem value="zh_Hans">中文</MenuItem>
2022-04-10 02:54:09 +12:00
<MenuItem value="de">Deutsch</MenuItem>
2022-05-01 12:16:17 +12:00
<MenuItem value="es">Español</MenuItem>
<MenuItem value="fr">Français</MenuItem>
2022-05-27 08:38:09 +12:00
<MenuItem value="it">Italiano</MenuItem>
2022-05-08 11:26:17 +12:00
<MenuItem value="hu">Magyar</MenuItem>
2022-10-02 07:54:16 +13:00
<MenuItem value="ko">한국어</MenuItem>
2022-04-10 02:54:09 +12:00
<MenuItem value="ja">日本語</MenuItem>
2022-06-03 06:45:36 +12:00
<MenuItem value="nl">Nederlands</MenuItem>
2022-04-12 12:18:18 +12:00
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
2022-09-24 04:55:40 +12:00
<MenuItem value="uk">Українська</MenuItem>
2022-05-14 06:46:30 +12:00
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
2022-09-28 00:44:00 +13:00
<MenuItem value="pl">Polski</MenuItem>
2022-04-20 11:31:50 +12:00
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem>
2022-04-08 12:31:24 +12:00
</Select>
</FormControl>
</Pref>
)
};
2023-01-03 14:08:37 +13:00
const Reservations = () => {
2023-01-02 15:56:24 +13:00
const { t } = useTranslation();
2023-01-10 14:37:13 +13:00
const { account } = useContext(AccountContext);
2023-01-02 15:56:24 +13:00
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
2023-01-10 14:37:13 +13:00
if (!config.enable_reservations || !session.exists() || !account || account.role === "admin") {
2023-01-03 16:21:11 +13:00
return <></>;
}
const reservations = account.reservations || [];
const limitReached = account.role === "user" && account.stats.reservations_remaining === 0;
2023-01-03 16:21:11 +13:00
2023-01-02 15:56:24 +13:00
const handleAddClick = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
2023-01-03 15:52:20 +13:00
const handleDialogSubmit = async (reservation) => {
2023-01-02 15:56:24 +13:00
setDialogOpen(false);
try {
2023-01-15 00:43:44 +13:00
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
2023-01-03 16:21:11 +13:00
await accountApi.sync();
2023-01-03 15:52:20 +13:00
console.debug(`[Preferences] Added topic reservation`, reservation);
2023-01-02 15:56:24 +13:00
} catch (e) {
2023-01-03 15:52:20 +13:00
console.log(`[Preferences] Error topic reservation.`, e);
2023-01-02 15:56:24 +13:00
}
2023-01-05 14:34:22 +13:00
// FIXME handle 401/403/409
2023-01-02 15:56:24 +13:00
};
return (
2023-01-03 14:08:37 +13:00
<Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
2023-01-02 15:56:24 +13:00
<CardContent sx={{ paddingBottom: 1 }}>
<Typography variant="h5" sx={{marginBottom: 2}}>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_title")}
2023-01-02 15:56:24 +13:00
</Typography>
<Paragraph>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_description")}
2023-01-02 15:56:24 +13:00
</Paragraph>
2023-01-03 16:21:11 +13:00
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
2023-01-10 14:37:13 +13:00
{limitReached && <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>}
2023-01-02 15:56:24 +13:00
</CardContent>
<CardActions>
2023-01-05 14:34:22 +13:00
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
2023-01-03 14:08:37 +13:00
<ReservationsDialog
2023-01-03 15:52:20 +13:00
key={`reservationAddDialog${dialogKey}`}
2023-01-03 04:46:37 +13:00
open={dialogOpen}
2023-01-03 15:52:20 +13:00
reservation={null}
2023-01-03 16:21:11 +13:00
reservations={reservations}
2023-01-03 04:46:37 +13:00
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
/>
2023-01-02 15:56:24 +13:00
</CardActions>
</Card>
);
};
2023-01-03 14:08:37 +13:00
const ReservationsTable = (props) => {
2023-01-02 15:56:24 +13:00
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
2023-01-03 15:52:20 +13:00
const [dialogReservation, setDialogReservation] = useState(null);
const { subscriptions } = useOutletContext();
const localSubscriptions = Object.assign(
...subscriptions
.filter(s => s.baseUrl === config.base_url)
.map(s => ({[s.topic]: s}))
);
2023-01-02 15:56:24 +13:00
2023-01-03 15:52:20 +13:00
const handleEditClick = (reservation) => {
2023-01-02 15:56:24 +13:00
setDialogKey(prev => prev+1);
2023-01-03 15:52:20 +13:00
setDialogReservation(reservation);
2023-01-02 15:56:24 +13:00
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
2023-01-03 15:52:20 +13:00
const handleDialogSubmit = async (reservation) => {
2023-01-02 15:56:24 +13:00
setDialogOpen(false);
2023-01-03 15:52:20 +13:00
try {
2023-01-15 00:43:44 +13:00
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
2023-01-03 16:21:11 +13:00
await accountApi.sync();
2023-01-03 15:52:20 +13:00
console.debug(`[Preferences] Added topic reservation`, reservation);
} catch (e) {
console.log(`[Preferences] Error topic reservation.`, e);
}
2023-01-05 14:34:22 +13:00
// FIXME handle 401/403/409
2023-01-02 15:56:24 +13:00
};
2023-01-03 15:52:20 +13:00
const handleDeleteClick = async (reservation) => {
try {
2023-01-15 00:43:44 +13:00
await accountApi.deleteReservation(reservation.topic);
2023-01-03 16:21:11 +13:00
await accountApi.sync();
2023-01-03 15:52:20 +13:00
console.debug(`[Preferences] Deleted topic reservation`, reservation);
} catch (e) {
console.log(`[Preferences] Error topic reservation.`, e);
}
// FIXME handle 401/403
2023-01-02 15:56:24 +13:00
};
return (
2023-01-03 14:08:37 +13:00
<Table size="small" aria-label={t("prefs_reservations_table")}>
2023-01-02 15:56:24 +13:00
<TableHead>
<TableRow>
2023-01-03 14:08:37 +13:00
<TableCell sx={{paddingLeft: 0}}>{t("prefs_reservations_table_topic_header")}</TableCell>
<TableCell>{t("prefs_reservations_table_access_header")}</TableCell>
2023-01-02 15:56:24 +13:00
<TableCell/>
</TableRow>
</TableHead>
<TableBody>
2023-01-03 14:08:37 +13:00
{props.reservations.map(reservation => (
2023-01-02 15:56:24 +13:00
<TableRow
2023-01-03 14:08:37 +13:00
key={reservation.topic}
2023-01-02 15:56:24 +13:00
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_reservations_table_topic_header")}>
{reservation.topic}
</TableCell>
2023-01-03 14:08:37 +13:00
<TableCell aria-label={t("prefs_reservations_table_access_header")}>
{reservation.everyone === "read-write" &&
<>
2023-01-03 15:52:20 +13:00
<Public fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_table_everyone_read_write")}
</>
}
{reservation.everyone === "read-only" &&
<>
2023-01-03 15:52:20 +13:00
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_table_everyone_read_only")}
</>
}
{reservation.everyone === "write-only" &&
<>
2023-01-03 15:52:20 +13:00
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_table_everyone_write_only")}
</>
}
{reservation.everyone === "deny-all" &&
<>
2023-01-03 15:52:20 +13:00
<LockIcon fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
2023-01-03 14:08:37 +13:00
{t("prefs_reservations_table_everyone_deny_all")}
</>
}
2023-01-02 15:56:24 +13:00
</TableCell>
<TableCell align="right">
{!localSubscriptions[reservation.topic] &&
<Chip icon={<Info/>} label="Not subscribed" color="primary" variant="outlined"/>
}
2023-01-03 14:08:37 +13:00
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
2023-01-02 15:56:24 +13:00
<EditIcon/>
</IconButton>
2023-01-03 14:08:37 +13:00
<IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}>
2023-01-02 15:56:24 +13:00
<CloseIcon/>
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
2023-01-03 14:08:37 +13:00
<ReservationsDialog
2023-01-03 15:52:20 +13:00
key={`reservationEditDialog${dialogKey}`}
2023-01-02 15:56:24 +13:00
open={dialogOpen}
2023-01-03 15:52:20 +13:00
reservation={dialogReservation}
reservations={props.reservations}
2023-01-02 15:56:24 +13:00
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
2023-01-03 04:46:37 +13:00
/>
2023-01-02 15:56:24 +13:00
</Table>
);
};
2023-01-03 14:08:37 +13:00
const ReservationsDialog = (props) => {
2023-01-03 04:46:37 +13:00
const { t } = useTranslation();
const [topic, setTopic] = useState("");
2023-01-03 15:52:20 +13:00
const [everyone, setEveryone] = useState("deny-all");
2023-01-03 04:46:37 +13:00
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
2023-01-03 15:52:20 +13:00
const editMode = props.reservation !== null;
2023-01-03 04:46:37 +13:00
const addButtonEnabled = (() => {
2023-01-03 15:52:20 +13:00
if (editMode) {
return true;
} else if (!validTopic(topic)) {
return false;
}
return props.reservations
.filter(r => r.topic === topic)
.length === 0;
2023-01-03 04:46:37 +13:00
})();
const handleSubmit = async () => {
props.onSubmit({
2023-01-03 15:52:20 +13:00
topic: (editMode) ? props.reservation.topic : topic,
everyone: everyone
2023-01-03 04:46:37 +13:00
})
};
useEffect(() => {
if (editMode) {
2023-01-03 15:52:20 +13:00
setTopic(props.reservation.topic);
setEveryone(props.reservation.everyone);
2023-01-03 04:46:37 +13:00
}
2023-01-03 15:52:20 +13:00
}, [editMode, props.reservation]);
2023-01-03 04:46:37 +13:00
return (
2023-01-03 14:08:37 +13:00
<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>
2023-01-03 04:46:37 +13:00
<DialogContent>
2023-01-05 14:34:22 +13:00
<DialogContentText>
{t("prefs_reservations_dialog_description")}
</DialogContentText>
2023-01-03 04:46:37 +13:00
{!editMode && <TextField
autoFocus
margin="dense"
id="topic"
2023-01-03 14:08:37 +13:00
label={t("prefs_reservations_dialog_topic_label")}
aria-label={t("prefs_reservations_dialog_topic_label")}
2023-01-03 04:46:37 +13:00
value={topic}
onChange={ev => setTopic(ev.target.value)}
type="url"
fullWidth
variant="standard"
/>}
2023-01-05 14:34:22 +13:00
<ReserveTopicSelect
value={everyone}
onChange={setEveryone}
sx={{mt: 1}}
/>
2023-01-03 04:46:37 +13:00
</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) => {
if (!session.exists()) {
return;
}
try {
2022-12-26 05:59:44 +13:00
await accountApi.updateSettings(payload);
} catch (e) {
console.log(`[Preferences] Error updating account settings`, e);
if ((e instanceof UnauthorizedError)) {
2022-12-26 16:29:55 +13:00
session.resetAndRedirect(routes.login);
}
}
};
2022-03-01 10:56:38 +13:00
export default Preferences;