Preferences.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. import * as React from 'react';
  2. import {useContext, useEffect, useState} from 'react';
  3. import {
  4. Alert,
  5. CardActions,
  6. CardContent,
  7. Chip,
  8. FormControl,
  9. Select,
  10. Stack,
  11. Table,
  12. TableBody,
  13. TableCell,
  14. TableHead,
  15. TableRow,
  16. Tooltip,
  17. useMediaQuery
  18. } from "@mui/material";
  19. import Typography from "@mui/material/Typography";
  20. import prefs from "../app/Prefs";
  21. import {Paragraph} from "./styles";
  22. import EditIcon from '@mui/icons-material/Edit';
  23. import CloseIcon from "@mui/icons-material/Close";
  24. import IconButton from "@mui/material/IconButton";
  25. import PlayArrowIcon from '@mui/icons-material/PlayArrow';
  26. import Container from "@mui/material/Container";
  27. import TextField from "@mui/material/TextField";
  28. import MenuItem from "@mui/material/MenuItem";
  29. import Card from "@mui/material/Card";
  30. import Button from "@mui/material/Button";
  31. import {useLiveQuery} from "dexie-react-hooks";
  32. import theme from "./theme";
  33. import Dialog from "@mui/material/Dialog";
  34. import DialogTitle from "@mui/material/DialogTitle";
  35. import DialogContent from "@mui/material/DialogContent";
  36. import DialogActions from "@mui/material/DialogActions";
  37. import userManager from "../app/UserManager";
  38. import {playSound, shuffle, sounds, validUrl} from "../app/utils";
  39. import {useTranslation} from "react-i18next";
  40. import session from "../app/Session";
  41. import routes from "./routes";
  42. import accountApi, {Permission, Role} from "../app/AccountApi";
  43. import {Pref, PrefGroup} from "./Pref";
  44. import {Info} from "@mui/icons-material";
  45. import {AccountContext} from "./App";
  46. import {useOutletContext} from "react-router-dom";
  47. import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
  48. import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs";
  49. import {UnauthorizedError} from "../app/errors";
  50. import subscriptionManager from "../app/SubscriptionManager";
  51. import {subscribeTopic} from "./SubscribeDialog";
  52. const Preferences = () => {
  53. return (
  54. <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
  55. <Stack spacing={3}>
  56. <Notifications/>
  57. <Reservations/>
  58. <Users/>
  59. <Appearance/>
  60. </Stack>
  61. </Container>
  62. );
  63. };
  64. const Notifications = () => {
  65. const { t } = useTranslation();
  66. return (
  67. <Card sx={{p: 3}} aria-label={t("prefs_notifications_title")}>
  68. <Typography variant="h5" sx={{marginBottom: 2}}>
  69. {t("prefs_notifications_title")}
  70. </Typography>
  71. <PrefGroup>
  72. <Sound/>
  73. <MinPriority/>
  74. <DeleteAfter/>
  75. </PrefGroup>
  76. </Card>
  77. );
  78. };
  79. const Sound = () => {
  80. const { t } = useTranslation();
  81. const labelId = "prefSound";
  82. const sound = useLiveQuery(async () => prefs.sound());
  83. const handleChange = async (ev) => {
  84. await prefs.setSound(ev.target.value);
  85. await maybeUpdateAccountSettings({
  86. notification: {
  87. sound: ev.target.value
  88. }
  89. });
  90. }
  91. if (!sound) {
  92. return null; // While loading
  93. }
  94. let description;
  95. if (sound === "none") {
  96. description = t("prefs_notifications_sound_description_none");
  97. } else {
  98. description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
  99. }
  100. return (
  101. <Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}>
  102. <div style={{ display: 'flex', width: '100%' }}>
  103. <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
  104. <Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
  105. <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
  106. {Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
  107. </Select>
  108. </FormControl>
  109. <IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}>
  110. <PlayArrowIcon />
  111. </IconButton>
  112. </div>
  113. </Pref>
  114. )
  115. };
  116. const MinPriority = () => {
  117. const { t } = useTranslation();
  118. const labelId = "prefMinPriority";
  119. const minPriority = useLiveQuery(async () => prefs.minPriority());
  120. const handleChange = async (ev) => {
  121. await prefs.setMinPriority(ev.target.value);
  122. await maybeUpdateAccountSettings({
  123. notification: {
  124. min_priority: ev.target.value
  125. }
  126. });
  127. }
  128. if (!minPriority) {
  129. return null; // While loading
  130. }
  131. const priorities = {
  132. 1: t("priority_min"),
  133. 2: t("priority_low"),
  134. 3: t("priority_default"),
  135. 4: t("priority_high"),
  136. 5: t("priority_max")
  137. }
  138. let description;
  139. if (minPriority === 1) {
  140. description = t("prefs_notifications_min_priority_description_any");
  141. } else if (minPriority === 5) {
  142. description = t("prefs_notifications_min_priority_description_max");
  143. } else {
  144. description = t("prefs_notifications_min_priority_description_x_or_higher", {
  145. number: minPriority,
  146. name: priorities[minPriority]
  147. });
  148. }
  149. return (
  150. <Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}>
  151. <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
  152. <Select value={minPriority} onChange={handleChange} aria-labelledby={labelId}>
  153. <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
  154. <MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
  155. <MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
  156. <MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem>
  157. <MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem>
  158. </Select>
  159. </FormControl>
  160. </Pref>
  161. )
  162. };
  163. const DeleteAfter = () => {
  164. const { t } = useTranslation();
  165. const labelId = "prefDeleteAfter";
  166. const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
  167. const handleChange = async (ev) => {
  168. await prefs.setDeleteAfter(ev.target.value);
  169. await maybeUpdateAccountSettings({
  170. notification: {
  171. delete_after: ev.target.value
  172. }
  173. });
  174. }
  175. if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
  176. return null; // While loading
  177. }
  178. const description = (() => {
  179. switch (deleteAfter) {
  180. case 0: return t("prefs_notifications_delete_after_never_description");
  181. case 10800: return t("prefs_notifications_delete_after_three_hours_description");
  182. case 86400: return t("prefs_notifications_delete_after_one_day_description");
  183. case 604800: return t("prefs_notifications_delete_after_one_week_description");
  184. case 2592000: return t("prefs_notifications_delete_after_one_month_description");
  185. }
  186. })();
  187. return (
  188. <Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
  189. <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
  190. <Select value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}>
  191. <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
  192. <MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
  193. <MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
  194. <MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem>
  195. <MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem>
  196. </Select>
  197. </FormControl>
  198. </Pref>
  199. )
  200. };
  201. const Users = () => {
  202. const { t } = useTranslation();
  203. const [dialogKey, setDialogKey] = useState(0);
  204. const [dialogOpen, setDialogOpen] = useState(false);
  205. const users = useLiveQuery(() => userManager.all());
  206. const handleAddClick = () => {
  207. setDialogKey(prev => prev+1);
  208. setDialogOpen(true);
  209. };
  210. const handleDialogCancel = () => {
  211. setDialogOpen(false);
  212. };
  213. const handleDialogSubmit = async (user) => {
  214. setDialogOpen(false);
  215. try {
  216. await userManager.save(user);
  217. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
  218. } catch (e) {
  219. console.log(`[Preferences] Error adding user.`, e);
  220. }
  221. };
  222. return (
  223. <Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
  224. <CardContent sx={{ paddingBottom: 1 }}>
  225. <Typography variant="h5" sx={{marginBottom: 2}}>
  226. {t("prefs_users_title")}
  227. </Typography>
  228. <Paragraph>
  229. {t("prefs_users_description")}
  230. {session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>}
  231. </Paragraph>
  232. {users?.length > 0 && <UserTable users={users}/>}
  233. </CardContent>
  234. <CardActions>
  235. <Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
  236. <UserDialog
  237. key={`userAddDialog${dialogKey}`}
  238. open={dialogOpen}
  239. user={null}
  240. users={users}
  241. onCancel={handleDialogCancel}
  242. onSubmit={handleDialogSubmit}
  243. />
  244. </CardActions>
  245. </Card>
  246. );
  247. };
  248. const UserTable = (props) => {
  249. const { t } = useTranslation();
  250. const [dialogKey, setDialogKey] = useState(0);
  251. const [dialogOpen, setDialogOpen] = useState(false);
  252. const [dialogUser, setDialogUser] = useState(null);
  253. const handleEditClick = (user) => {
  254. setDialogKey(prev => prev+1);
  255. setDialogUser(user);
  256. setDialogOpen(true);
  257. };
  258. const handleDialogCancel = () => {
  259. setDialogOpen(false);
  260. };
  261. const handleDialogSubmit = async (user) => {
  262. setDialogOpen(false);
  263. try {
  264. await userManager.save(user);
  265. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
  266. } catch (e) {
  267. console.log(`[Preferences] Error updating user.`, e);
  268. }
  269. };
  270. const handleDeleteClick = async (user) => {
  271. try {
  272. await userManager.delete(user.baseUrl);
  273. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
  274. } catch (e) {
  275. console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
  276. }
  277. };
  278. return (
  279. <Table size="small" aria-label={t("prefs_users_table")}>
  280. <TableHead>
  281. <TableRow>
  282. <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
  283. <TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
  284. <TableCell/>
  285. </TableRow>
  286. </TableHead>
  287. <TableBody>
  288. {props.users?.map(user => (
  289. <TableRow
  290. key={user.baseUrl}
  291. sx={{'&:last-child td, &:last-child th': {border: 0}}}
  292. >
  293. <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
  294. <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
  295. <TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
  296. {(!session.exists() || user.baseUrl !== config.base_url) &&
  297. <>
  298. <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
  299. <EditIcon/>
  300. </IconButton>
  301. <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
  302. <CloseIcon/>
  303. </IconButton>
  304. </>
  305. }
  306. {session.exists() && user.baseUrl === config.base_url &&
  307. <Tooltip title={t("prefs_users_table_cannot_delete_or_edit")}>
  308. <span>
  309. <IconButton disabled><EditIcon/></IconButton>
  310. <IconButton disabled><CloseIcon/></IconButton>
  311. </span>
  312. </Tooltip>
  313. }
  314. </TableCell>
  315. </TableRow>
  316. ))}
  317. </TableBody>
  318. <UserDialog
  319. key={`userEditDialog${dialogKey}`}
  320. open={dialogOpen}
  321. user={dialogUser}
  322. users={props.users}
  323. onCancel={handleDialogCancel}
  324. onSubmit={handleDialogSubmit}
  325. />
  326. </Table>
  327. );
  328. };
  329. const UserDialog = (props) => {
  330. const { t } = useTranslation();
  331. const [baseUrl, setBaseUrl] = useState("");
  332. const [username, setUsername] = useState("");
  333. const [password, setPassword] = useState("");
  334. const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
  335. const editMode = props.user !== null;
  336. const addButtonEnabled = (() => {
  337. if (editMode) {
  338. return username.length > 0 && password.length > 0;
  339. }
  340. const baseUrlValid = validUrl(baseUrl);
  341. const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl);
  342. return baseUrlValid
  343. && !baseUrlExists
  344. && username.length > 0
  345. && password.length > 0;
  346. })();
  347. const handleSubmit = async () => {
  348. props.onSubmit({
  349. baseUrl: baseUrl,
  350. username: username,
  351. password: password
  352. })
  353. };
  354. useEffect(() => {
  355. if (editMode) {
  356. setBaseUrl(props.user.baseUrl);
  357. setUsername(props.user.username);
  358. setPassword(props.user.password);
  359. }
  360. }, [editMode, props.user]);
  361. return (
  362. <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
  363. <DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle>
  364. <DialogContent>
  365. {!editMode && <TextField
  366. autoFocus
  367. margin="dense"
  368. id="baseUrl"
  369. label={t("prefs_users_dialog_base_url_label")}
  370. aria-label={t("prefs_users_dialog_base_url_label")}
  371. value={baseUrl}
  372. onChange={ev => setBaseUrl(ev.target.value)}
  373. type="url"
  374. fullWidth
  375. variant="standard"
  376. />}
  377. <TextField
  378. autoFocus={editMode}
  379. margin="dense"
  380. id="username"
  381. label={t("prefs_users_dialog_username_label")}
  382. aria-label={t("prefs_users_dialog_username_label")}
  383. value={username}
  384. onChange={ev => setUsername(ev.target.value)}
  385. type="text"
  386. fullWidth
  387. variant="standard"
  388. />
  389. <TextField
  390. margin="dense"
  391. id="password"
  392. label={t("prefs_users_dialog_password_label")}
  393. aria-label={t("prefs_users_dialog_password_label")}
  394. type="password"
  395. value={password}
  396. onChange={ev => setPassword(ev.target.value)}
  397. fullWidth
  398. variant="standard"
  399. />
  400. </DialogContent>
  401. <DialogActions>
  402. <Button onClick={props.onCancel}>{t("common_cancel")}</Button>
  403. <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? t("common_save") : t("common_add")}</Button>
  404. </DialogActions>
  405. </Dialog>
  406. );
  407. };
  408. const Appearance = () => {
  409. const { t } = useTranslation();
  410. return (
  411. <Card sx={{p: 3}} aria-label={t("prefs_appearance_title")}>
  412. <Typography variant="h5" sx={{marginBottom: 2}}>
  413. {t("prefs_appearance_title")}
  414. </Typography>
  415. <PrefGroup>
  416. <Language/>
  417. </PrefGroup>
  418. </Card>
  419. );
  420. };
  421. const Language = () => {
  422. const { t, i18n } = useTranslation();
  423. const labelId = "prefLanguage";
  424. const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
  425. const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
  426. const lang = i18n.language ?? "en";
  427. const handleChange = async (ev) => {
  428. await i18n.changeLanguage(ev.target.value);
  429. await maybeUpdateAccountSettings({
  430. language: ev.target.value
  431. });
  432. };
  433. // Remember: Flags are not languages. Don't put flags next to the language in the list.
  434. // Languages names from: https://www.omniglot.com/language/names.htm
  435. // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
  436. return (
  437. <Pref labelId={labelId} title={title}>
  438. <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
  439. <Select value={lang} onChange={handleChange} aria-labelledby={labelId}>
  440. <MenuItem value="en">English</MenuItem>
  441. <MenuItem value="ar">العربية</MenuItem>
  442. <MenuItem value="id">Bahasa Indonesia</MenuItem>
  443. <MenuItem value="bg">Български</MenuItem>
  444. <MenuItem value="cs">Čeština</MenuItem>
  445. <MenuItem value="zh_Hans">中文</MenuItem>
  446. <MenuItem value="da">Dansk</MenuItem>
  447. <MenuItem value="de">Deutsch</MenuItem>
  448. <MenuItem value="es">Español</MenuItem>
  449. <MenuItem value="fr">Français</MenuItem>
  450. <MenuItem value="it">Italiano</MenuItem>
  451. <MenuItem value="hu">Magyar</MenuItem>
  452. <MenuItem value="ko">한국어</MenuItem>
  453. <MenuItem value="ja">日本語</MenuItem>
  454. <MenuItem value="nl">Nederlands</MenuItem>
  455. <MenuItem value="nb_NO">Norsk bokmål</MenuItem>
  456. <MenuItem value="uk">Українська</MenuItem>
  457. <MenuItem value="pt">Português</MenuItem>
  458. <MenuItem value="pt_BR">Português (Brasil)</MenuItem>
  459. <MenuItem value="pl">Polski</MenuItem>
  460. <MenuItem value="ru">Русский</MenuItem>
  461. <MenuItem value="tr">Türkçe</MenuItem>
  462. </Select>
  463. </FormControl>
  464. </Pref>
  465. )
  466. };
  467. const Reservations = () => {
  468. const { t } = useTranslation();
  469. const { account } = useContext(AccountContext);
  470. const [dialogKey, setDialogKey] = useState(0);
  471. const [dialogOpen, setDialogOpen] = useState(false);
  472. if (!config.enable_reservations || !session.exists() || !account) {
  473. return <></>;
  474. }
  475. const reservations = account.reservations || [];
  476. const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0;
  477. const handleAddClick = () => {
  478. setDialogKey(prev => prev+1);
  479. setDialogOpen(true);
  480. };
  481. return (
  482. <Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
  483. <CardContent sx={{ paddingBottom: 1 }}>
  484. <Typography variant="h5" sx={{marginBottom: 2}}>
  485. {t("prefs_reservations_title")}
  486. </Typography>
  487. <Paragraph>
  488. {t("prefs_reservations_description")}
  489. </Paragraph>
  490. {reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
  491. {limitReached && <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>}
  492. </CardContent>
  493. <CardActions>
  494. <Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
  495. <ReserveAddDialog
  496. key={`reservationAddDialog${dialogKey}`}
  497. open={dialogOpen}
  498. reservations={reservations}
  499. onClose={() => setDialogOpen(false)}
  500. />
  501. </CardActions>
  502. </Card>
  503. );
  504. };
  505. const ReservationsTable = (props) => {
  506. const { t } = useTranslation();
  507. const [dialogKey, setDialogKey] = useState(0);
  508. const [dialogReservation, setDialogReservation] = useState(null);
  509. const [editDialogOpen, setEditDialogOpen] = useState(false);
  510. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  511. const { subscriptions } = useOutletContext();
  512. const localSubscriptions = (subscriptions?.length > 0)
  513. ? Object.assign(...subscriptions.filter(s => s.baseUrl === config.base_url).map(s => ({[s.topic]: s})))
  514. : [];
  515. const handleEditClick = (reservation) => {
  516. setDialogKey(prev => prev+1);
  517. setDialogReservation(reservation);
  518. setEditDialogOpen(true);
  519. };
  520. const handleDeleteClick = async (reservation) => {
  521. setDialogKey(prev => prev+1);
  522. setDialogReservation(reservation);
  523. setDeleteDialogOpen(true);
  524. };
  525. const handleSubscribeClick = async (reservation) => {
  526. await subscribeTopic(config.base_url, reservation.topic);
  527. };
  528. return (
  529. <Table size="small" aria-label={t("prefs_reservations_table")}>
  530. <TableHead>
  531. <TableRow>
  532. <TableCell sx={{paddingLeft: 0}}>{t("prefs_reservations_table_topic_header")}</TableCell>
  533. <TableCell>{t("prefs_reservations_table_access_header")}</TableCell>
  534. <TableCell/>
  535. </TableRow>
  536. </TableHead>
  537. <TableBody>
  538. {props.reservations.map(reservation => (
  539. <TableRow
  540. key={reservation.topic}
  541. sx={{'&:last-child td, &:last-child th': { border: 0 }}}
  542. >
  543. <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_reservations_table_topic_header")}>
  544. {reservation.topic}
  545. </TableCell>
  546. <TableCell aria-label={t("prefs_reservations_table_access_header")}>
  547. {reservation.everyone === Permission.READ_WRITE &&
  548. <>
  549. <PermissionReadWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
  550. {t("prefs_reservations_table_everyone_read_write")}
  551. </>
  552. }
  553. {reservation.everyone === Permission.READ_ONLY &&
  554. <>
  555. <PermissionRead size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
  556. {t("prefs_reservations_table_everyone_read_only")}
  557. </>
  558. }
  559. {reservation.everyone === Permission.WRITE_ONLY &&
  560. <>
  561. <PermissionWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
  562. {t("prefs_reservations_table_everyone_write_only")}
  563. </>
  564. }
  565. {reservation.everyone === Permission.DENY_ALL &&
  566. <>
  567. <PermissionDenyAll size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }}/>
  568. {t("prefs_reservations_table_everyone_deny_all")}
  569. </>
  570. }
  571. </TableCell>
  572. <TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
  573. {!localSubscriptions[reservation.topic] &&
  574. <Tooltip title={t("prefs_reservations_table_click_to_subscribe")}>
  575. <Chip icon={<Info/>} onClick={() => handleSubscribeClick(reservation)} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/>
  576. </Tooltip>
  577. }
  578. <IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
  579. <EditIcon/>
  580. </IconButton>
  581. <IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}>
  582. <CloseIcon/>
  583. </IconButton>
  584. </TableCell>
  585. </TableRow>
  586. ))}
  587. </TableBody>
  588. <ReserveEditDialog
  589. key={`reservationEditDialog${dialogKey}`}
  590. open={editDialogOpen}
  591. reservation={dialogReservation}
  592. reservations={props.reservations}
  593. onClose={() => setEditDialogOpen(false)}
  594. />
  595. <ReserveDeleteDialog
  596. key={`reservationDeleteDialog${dialogKey}`}
  597. open={deleteDialogOpen}
  598. topic={dialogReservation?.topic}
  599. onClose={() => setDeleteDialogOpen(false)}
  600. />
  601. </Table>
  602. );
  603. };
  604. const maybeUpdateAccountSettings = async (payload) => {
  605. if (!session.exists()) {
  606. return;
  607. }
  608. try {
  609. await accountApi.updateSettings(payload);
  610. } catch (e) {
  611. console.log(`[Preferences] Error updating account settings`, e);
  612. if (e instanceof UnauthorizedError) {
  613. session.resetAndRedirect(routes.login);
  614. }
  615. }
  616. };
  617. export default Preferences;