SubscribeDialog.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import * as React from 'react';
  2. import {useState} from 'react';
  3. import Button from '@mui/material/Button';
  4. import TextField from '@mui/material/TextField';
  5. import Dialog from '@mui/material/Dialog';
  6. import DialogActions from '@mui/material/DialogActions';
  7. import DialogContent from '@mui/material/DialogContent';
  8. import DialogContentText from '@mui/material/DialogContentText';
  9. import DialogTitle from '@mui/material/DialogTitle';
  10. import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
  11. import theme from "./theme";
  12. import api from "../app/Api";
  13. import {topicUrl, validTopic, validUrl} from "../app/utils";
  14. import Box from "@mui/material/Box";
  15. import userManager from "../app/UserManager";
  16. import subscriptionManager from "../app/SubscriptionManager";
  17. import poller from "../app/Poller";
  18. const publicBaseUrl = "https://ntfy.sh";
  19. const SubscribeDialog = (props) => {
  20. const [baseUrl, setBaseUrl] = useState("");
  21. const [topic, setTopic] = useState("");
  22. const [showLoginPage, setShowLoginPage] = useState(false);
  23. const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
  24. const handleSuccess = async () => {
  25. const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin;
  26. const subscription = await subscriptionManager.add(actualBaseUrl, topic);
  27. poller.pollInBackground(subscription); // Dangle!
  28. props.onSuccess(subscription);
  29. }
  30. return (
  31. <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
  32. {!showLoginPage && <SubscribePage
  33. baseUrl={baseUrl}
  34. setBaseUrl={setBaseUrl}
  35. topic={topic}
  36. setTopic={setTopic}
  37. subscriptions={props.subscriptions}
  38. onCancel={props.onCancel}
  39. onNeedsLogin={() => setShowLoginPage(true)}
  40. onSuccess={handleSuccess}
  41. />}
  42. {showLoginPage && <LoginPage
  43. baseUrl={baseUrl}
  44. topic={topic}
  45. onBack={() => setShowLoginPage(false)}
  46. onSuccess={handleSuccess}
  47. />}
  48. </Dialog>
  49. );
  50. };
  51. const SubscribePage = (props) => {
  52. const [anotherServerVisible, setAnotherServerVisible] = useState(false);
  53. const [errorText, setErrorText] = useState("");
  54. const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
  55. const topic = props.topic;
  56. const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
  57. const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
  58. .filter(s => s !== window.location.origin);
  59. const handleSubscribe = async () => {
  60. const user = await userManager.get(baseUrl); // May be undefined
  61. const username = (user) ? user.username : "anonymous";
  62. const success = await api.auth(baseUrl, topic, user);
  63. if (!success) {
  64. console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
  65. if (user) {
  66. setErrorText(`User ${username} not authorized`);
  67. return;
  68. } else {
  69. props.onNeedsLogin();
  70. return;
  71. }
  72. }
  73. console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
  74. props.onSuccess();
  75. };
  76. const handleUseAnotherChanged = (e) => {
  77. props.setBaseUrl("");
  78. setAnotherServerVisible(e.target.checked);
  79. };
  80. const subscribeButtonEnabled = (() => {
  81. if (anotherServerVisible) {
  82. const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
  83. return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
  84. } else {
  85. const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(window.location.origin, topic));
  86. return validTopic(topic) && !isExistingTopicUrl;
  87. }
  88. })();
  89. return (
  90. <>
  91. <DialogTitle>Subscribe to topic</DialogTitle>
  92. <DialogContent>
  93. <DialogContentText>
  94. Topics may not be password-protected, so choose a name that's not easy to guess.
  95. Once subscribed, you can PUT/POST notifications.
  96. </DialogContentText>
  97. <TextField
  98. autoFocus
  99. margin="dense"
  100. id="topic"
  101. placeholder="Topic name, e.g. phil_alerts"
  102. inputProps={{ maxLength: 64 }}
  103. value={props.topic}
  104. onChange={ev => props.setTopic(ev.target.value)}
  105. type="text"
  106. fullWidth
  107. variant="standard"
  108. />
  109. <FormControlLabel
  110. sx={{pt: 1}}
  111. control={<Checkbox onChange={handleUseAnotherChanged}/>}
  112. label="Use another server" />
  113. {anotherServerVisible && <Autocomplete
  114. freeSolo
  115. options={existingBaseUrls}
  116. sx={{ maxWidth: 400 }}
  117. inputValue={props.baseUrl}
  118. onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
  119. renderInput={ (params) =>
  120. <TextField {...params} placeholder={window.location.origin} variant="standard"/>
  121. }
  122. />}
  123. </DialogContent>
  124. <DialogFooter status={errorText}>
  125. <Button onClick={props.onCancel}>Cancel</Button>
  126. <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>Subscribe</Button>
  127. </DialogFooter>
  128. </>
  129. );
  130. };
  131. const LoginPage = (props) => {
  132. const [username, setUsername] = useState("");
  133. const [password, setPassword] = useState("");
  134. const [errorText, setErrorText] = useState("");
  135. const baseUrl = (props.baseUrl) ? props.baseUrl : window.location.origin;
  136. const topic = props.topic;
  137. const handleLogin = async () => {
  138. const user = {baseUrl, username, password};
  139. const success = await api.auth(baseUrl, topic, user);
  140. if (!success) {
  141. console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
  142. setErrorText(`User ${username} not authorized`);
  143. return;
  144. }
  145. console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
  146. await userManager.save(user);
  147. props.onSuccess();
  148. };
  149. return (
  150. <>
  151. <DialogTitle>Login required</DialogTitle>
  152. <DialogContent>
  153. <DialogContentText>
  154. This topic is password-protected. Please enter username and
  155. password to subscribe.
  156. </DialogContentText>
  157. <TextField
  158. autoFocus
  159. margin="dense"
  160. id="username"
  161. label="Username, e.g. phil"
  162. value={username}
  163. onChange={ev => setUsername(ev.target.value)}
  164. type="text"
  165. fullWidth
  166. variant="standard"
  167. />
  168. <TextField
  169. margin="dense"
  170. id="password"
  171. label="Password"
  172. type="password"
  173. value={password}
  174. onChange={ev => setPassword(ev.target.value)}
  175. fullWidth
  176. variant="standard"
  177. />
  178. </DialogContent>
  179. <DialogFooter status={errorText}>
  180. <Button onClick={props.onBack}>Back</Button>
  181. <Button onClick={handleLogin}>Login</Button>
  182. </DialogFooter>
  183. </>
  184. );
  185. };
  186. const DialogFooter = (props) => {
  187. return (
  188. <Box sx={{
  189. display: 'flex',
  190. flexDirection: 'row',
  191. justifyContent: 'space-between',
  192. paddingLeft: '24px',
  193. paddingTop: '8px 24px',
  194. paddingBottom: '8px 24px',
  195. }}>
  196. <DialogContentText sx={{
  197. margin: '0px',
  198. paddingTop: '8px',
  199. }}>
  200. {props.status}
  201. </DialogContentText>
  202. <DialogActions>
  203. {props.children}
  204. </DialogActions>
  205. </Box>
  206. );
  207. };
  208. export default SubscribeDialog;