Preferences.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import * as React from 'react';
  2. import {useEffect, useState} from 'react';
  3. import {
  4. CardActions,
  5. CardContent,
  6. FormControl,
  7. Select,
  8. Stack,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableHead,
  13. TableRow,
  14. useMediaQuery
  15. } from "@mui/material";
  16. import Typography from "@mui/material/Typography";
  17. import prefs from "../app/Prefs";
  18. import {Paragraph} from "./styles";
  19. import EditIcon from '@mui/icons-material/Edit';
  20. import CloseIcon from "@mui/icons-material/Close";
  21. import IconButton from "@mui/material/IconButton";
  22. import Container from "@mui/material/Container";
  23. import TextField from "@mui/material/TextField";
  24. import MenuItem from "@mui/material/MenuItem";
  25. import Card from "@mui/material/Card";
  26. import Button from "@mui/material/Button";
  27. import {useLiveQuery} from "dexie-react-hooks";
  28. import theme from "./theme";
  29. import Dialog from "@mui/material/Dialog";
  30. import DialogTitle from "@mui/material/DialogTitle";
  31. import DialogContent from "@mui/material/DialogContent";
  32. import DialogActions from "@mui/material/DialogActions";
  33. import userManager from "../app/UserManager";
  34. const Preferences = () => {
  35. return (
  36. <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
  37. <Stack spacing={3}>
  38. <Notifications/>
  39. <Users/>
  40. </Stack>
  41. </Container>
  42. );
  43. };
  44. const Notifications = () => {
  45. return (
  46. <Card sx={{p: 3}}>
  47. <Typography variant="h5">
  48. Notifications
  49. </Typography>
  50. <PrefGroup>
  51. <MinPriority/>
  52. <DeleteAfter/>
  53. </PrefGroup>
  54. </Card>
  55. );
  56. };
  57. const MinPriority = () => {
  58. const minPriority = useLiveQuery(() => prefs.minPriority());
  59. const handleChange = async (ev) => {
  60. await prefs.setMinPriority(ev.target.value);
  61. }
  62. if (!minPriority) {
  63. return null; // While loading
  64. }
  65. return (
  66. <Pref title="Minimum priority">
  67. <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
  68. <Select value={minPriority} onChange={handleChange}>
  69. <MenuItem value={1}><em>Any priority</em></MenuItem>
  70. <MenuItem value={2}>Low priority and higher</MenuItem>
  71. <MenuItem value={3}>Default priority and higher</MenuItem>
  72. <MenuItem value={4}>High priority and higher</MenuItem>
  73. <MenuItem value={5}>Only max priority</MenuItem>
  74. </Select>
  75. </FormControl>
  76. </Pref>
  77. )
  78. };
  79. const DeleteAfter = () => {
  80. const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
  81. const handleChange = async (ev) => {
  82. await prefs.setDeleteAfter(ev.target.value);
  83. }
  84. if (!deleteAfter) {
  85. return null; // While loading
  86. }
  87. return (
  88. <Pref title="Delete notifications">
  89. <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
  90. <Select value={deleteAfter} onChange={handleChange}>
  91. <MenuItem value={0}>Never</MenuItem>
  92. <MenuItem value={10800}>After three hours</MenuItem>
  93. <MenuItem value={86400}>After one day</MenuItem>
  94. <MenuItem value={604800}>After one week</MenuItem>
  95. <MenuItem value={2592000}>After one month</MenuItem>
  96. </Select>
  97. </FormControl>
  98. </Pref>
  99. )
  100. };
  101. const PrefGroup = (props) => {
  102. return (
  103. <div style={{
  104. display: 'flex',
  105. flexWrap: 'wrap'
  106. }}>
  107. {props.children}
  108. </div>
  109. )
  110. };
  111. const Pref = (props) => {
  112. return (
  113. <>
  114. <div style={{
  115. flex: '1 0 30%',
  116. display: 'inline-flex',
  117. flexDirection: 'column',
  118. minHeight: '60px',
  119. justifyContent: 'center'
  120. }}>
  121. <b>{props.title}</b>
  122. </div>
  123. <div style={{
  124. flex: '1 0 calc(70% - 50px)',
  125. display: 'inline-flex',
  126. flexDirection: 'column',
  127. minHeight: '60px',
  128. justifyContent: 'center'
  129. }}>
  130. {props.children}
  131. </div>
  132. </>
  133. );
  134. };
  135. const Users = () => {
  136. const [dialogKey, setDialogKey] = useState(0);
  137. const [dialogOpen, setDialogOpen] = useState(false);
  138. const users = useLiveQuery(() => userManager.all());
  139. const handleAddClick = () => {
  140. setDialogKey(prev => prev+1);
  141. setDialogOpen(true);
  142. };
  143. const handleDialogCancel = () => {
  144. setDialogOpen(false);
  145. };
  146. const handleDialogSubmit = async (user) => {
  147. setDialogOpen(false);
  148. try {
  149. await userManager.save(user);
  150. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
  151. } catch (e) {
  152. console.log(`[Preferences] Error adding user.`, e);
  153. }
  154. };
  155. return (
  156. <Card sx={{ padding: 1 }}>
  157. <CardContent>
  158. <Typography variant="h5">
  159. Manage users
  160. </Typography>
  161. <Paragraph>
  162. Add/remove users for your protected topics here. Please note that username and password are
  163. stored in the browser's local storage.
  164. </Paragraph>
  165. {users?.length > 0 && <UserTable users={users}/>}
  166. </CardContent>
  167. <CardActions>
  168. <Button onClick={handleAddClick}>Add user</Button>
  169. <UserDialog
  170. key={`userAddDialog${dialogKey}`}
  171. open={dialogOpen}
  172. user={null}
  173. users={users}
  174. onCancel={handleDialogCancel}
  175. onSubmit={handleDialogSubmit}
  176. />
  177. </CardActions>
  178. </Card>
  179. );
  180. };
  181. const UserTable = (props) => {
  182. const [dialogKey, setDialogKey] = useState(0);
  183. const [dialogOpen, setDialogOpen] = useState(false);
  184. const [dialogUser, setDialogUser] = useState(null);
  185. const handleEditClick = (user) => {
  186. setDialogKey(prev => prev+1);
  187. setDialogUser(user);
  188. setDialogOpen(true);
  189. };
  190. const handleDialogCancel = () => {
  191. setDialogOpen(false);
  192. };
  193. const handleDialogSubmit = async (user) => {
  194. setDialogOpen(false);
  195. try {
  196. await userManager.save(user);
  197. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
  198. } catch (e) {
  199. console.log(`[Preferences] Error updating user.`, e);
  200. }
  201. };
  202. const handleDeleteClick = async (user) => {
  203. try {
  204. await userManager.delete(user.baseUrl);
  205. console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
  206. } catch (e) {
  207. console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
  208. }
  209. };
  210. return (
  211. <Table size="small">
  212. <TableHead>
  213. <TableRow>
  214. <TableCell>User</TableCell>
  215. <TableCell>Service URL</TableCell>
  216. <TableCell/>
  217. </TableRow>
  218. </TableHead>
  219. <TableBody>
  220. {props.users?.map(user => (
  221. <TableRow
  222. key={user.baseUrl}
  223. sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
  224. >
  225. <TableCell component="th" scope="row">{user.username}</TableCell>
  226. <TableCell>{user.baseUrl}</TableCell>
  227. <TableCell align="right">
  228. <IconButton onClick={() => handleEditClick(user)}>
  229. <EditIcon/>
  230. </IconButton>
  231. <IconButton onClick={() => handleDeleteClick(user)}>
  232. <CloseIcon />
  233. </IconButton>
  234. </TableCell>
  235. </TableRow>
  236. ))}
  237. </TableBody>
  238. <UserDialog
  239. key={`userEditDialog${dialogKey}`}
  240. open={dialogOpen}
  241. user={dialogUser}
  242. users={props.users}
  243. onCancel={handleDialogCancel}
  244. onSubmit={handleDialogSubmit}
  245. />
  246. </Table>
  247. );
  248. };
  249. const UserDialog = (props) => {
  250. const [baseUrl, setBaseUrl] = useState("");
  251. const [username, setUsername] = useState("");
  252. const [password, setPassword] = useState("");
  253. const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
  254. const editMode = props.user !== null;
  255. const addButtonEnabled = (() => {
  256. if (editMode) {
  257. return username.length > 0 && password.length > 0;
  258. }
  259. const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl);
  260. return !baseUrlExists && username.length > 0 && password.length > 0;
  261. })();
  262. const handleSubmit = async () => {
  263. props.onSubmit({
  264. baseUrl: baseUrl,
  265. username: username,
  266. password: password
  267. })
  268. };
  269. useEffect(() => {
  270. if (editMode) {
  271. setBaseUrl(props.user.baseUrl);
  272. setUsername(props.user.username);
  273. setPassword(props.user.password);
  274. }
  275. }, []);
  276. return (
  277. <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
  278. <DialogTitle>{editMode ? "Edit user" : "Add user"}</DialogTitle>
  279. <DialogContent>
  280. {!editMode && <TextField
  281. autoFocus
  282. margin="dense"
  283. id="baseUrl"
  284. label="Service URL, e.g. https://ntfy.sh"
  285. value={baseUrl}
  286. onChange={ev => setBaseUrl(ev.target.value)}
  287. type="url"
  288. fullWidth
  289. variant="standard"
  290. />}
  291. <TextField
  292. autoFocus={editMode}
  293. margin="dense"
  294. id="username"
  295. label="Username, e.g. phil"
  296. value={username}
  297. onChange={ev => setUsername(ev.target.value)}
  298. type="text"
  299. fullWidth
  300. variant="standard"
  301. />
  302. <TextField
  303. margin="dense"
  304. id="password"
  305. label="Password"
  306. type="password"
  307. value={password}
  308. onChange={ev => setPassword(ev.target.value)}
  309. fullWidth
  310. variant="standard"
  311. />
  312. </DialogContent>
  313. <DialogActions>
  314. <Button onClick={props.onCancel}>Cancel</Button>
  315. <Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? "Save" : "Add"}</Button>
  316. </DialogActions>
  317. </Dialog>
  318. );
  319. };
  320. export default Preferences;