SubscribeDialog.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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 DialogContent from '@mui/material/DialogContent';
  7. import DialogContentText from '@mui/material/DialogContentText';
  8. import DialogTitle from '@mui/material/DialogTitle';
  9. import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
  10. import theme from "./theme";
  11. import api from "../app/Api";
  12. import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
  13. import userManager from "../app/UserManager";
  14. import subscriptionManager from "../app/SubscriptionManager";
  15. import poller from "../app/Poller";
  16. import DialogFooter from "./DialogFooter";
  17. import {useTranslation} from "react-i18next";
  18. import session from "../app/Session";
  19. const publicBaseUrl = "https://ntfy.sh";
  20. const SubscribeDialog = (props) => {
  21. const [baseUrl, setBaseUrl] = useState("");
  22. const [topic, setTopic] = useState("");
  23. const [showLoginPage, setShowLoginPage] = useState(false);
  24. const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
  25. const handleSuccess = async () => {
  26. const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin;
  27. const subscription = await subscriptionManager.add(actualBaseUrl, topic);
  28. if (session.exists()) {
  29. const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), {
  30. base_url: actualBaseUrl,
  31. topic: topic
  32. });
  33. await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
  34. }
  35. poller.pollInBackground(subscription); // Dangle!
  36. props.onSuccess(subscription);
  37. }
  38. return (
  39. <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
  40. {!showLoginPage && <SubscribePage
  41. baseUrl={baseUrl}
  42. setBaseUrl={setBaseUrl}
  43. topic={topic}
  44. setTopic={setTopic}
  45. subscriptions={props.subscriptions}
  46. onCancel={props.onCancel}
  47. onNeedsLogin={() => setShowLoginPage(true)}
  48. onSuccess={handleSuccess}
  49. />}
  50. {showLoginPage && <LoginPage
  51. baseUrl={baseUrl}
  52. topic={topic}
  53. onBack={() => setShowLoginPage(false)}
  54. onSuccess={handleSuccess}
  55. />}
  56. </Dialog>
  57. );
  58. };
  59. const SubscribePage = (props) => {
  60. const { t } = useTranslation();
  61. const [anotherServerVisible, setAnotherServerVisible] = useState(false);
  62. const [errorText, setErrorText] = useState("");
  63. const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
  64. const topic = props.topic;
  65. const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
  66. const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
  67. .filter(s => s !== window.location.origin);
  68. const handleSubscribe = async () => {
  69. const user = await userManager.get(baseUrl); // May be undefined
  70. const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
  71. const success = await api.topicAuth(baseUrl, topic, user);
  72. if (!success) {
  73. console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
  74. if (user) {
  75. setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
  76. return;
  77. } else {
  78. props.onNeedsLogin();
  79. return;
  80. }
  81. }
  82. console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
  83. props.onSuccess();
  84. };
  85. const handleUseAnotherChanged = (e) => {
  86. props.setBaseUrl("");
  87. setAnotherServerVisible(e.target.checked);
  88. };
  89. const subscribeButtonEnabled = (() => {
  90. if (anotherServerVisible) {
  91. const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
  92. return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
  93. } else {
  94. const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(window.location.origin, topic));
  95. return validTopic(topic) && !isExistingTopicUrl;
  96. }
  97. })();
  98. const updateBaseUrl = (ev, newVal) => {
  99. if (validUrl(newVal)) {
  100. props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?://
  101. } else {
  102. props.setBaseUrl(newVal);
  103. }
  104. };
  105. return (
  106. <>
  107. <DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
  108. <DialogContent>
  109. <DialogContentText>
  110. {t("subscribe_dialog_subscribe_description")}
  111. </DialogContentText>
  112. <div style={{display: 'flex'}} role="row">
  113. <TextField
  114. autoFocus
  115. margin="dense"
  116. id="topic"
  117. placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
  118. value={props.topic}
  119. onChange={ev => props.setTopic(ev.target.value)}
  120. type="text"
  121. fullWidth
  122. variant="standard"
  123. inputProps={{
  124. maxLength: 64,
  125. "aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
  126. }}
  127. />
  128. <Button onClick={() => {props.setTopic(randomAlphanumericString(16))}} style={{flexShrink: "0", marginTop: "0.5em"}}>
  129. {t("subscribe_dialog_subscribe_button_generate_topic_name")}
  130. </Button>
  131. </div>
  132. <FormControlLabel
  133. sx={{pt: 1}}
  134. control={
  135. <Checkbox
  136. onChange={handleUseAnotherChanged}
  137. inputProps={{
  138. "aria-label": t("subscribe_dialog_subscribe_use_another_label")
  139. }}
  140. />
  141. }
  142. label={t("subscribe_dialog_subscribe_use_another_label")} />
  143. {anotherServerVisible && <Autocomplete
  144. freeSolo
  145. options={existingBaseUrls}
  146. sx={{ maxWidth: 400 }}
  147. inputValue={props.baseUrl}
  148. onInputChange={updateBaseUrl}
  149. renderInput={ (params) =>
  150. <TextField
  151. {...params}
  152. placeholder={window.location.origin}
  153. variant="standard"
  154. aria-label={t("subscribe_dialog_subscribe_base_url_label")}
  155. />
  156. }
  157. />}
  158. </DialogContent>
  159. <DialogFooter status={errorText}>
  160. <Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
  161. <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>{t("subscribe_dialog_subscribe_button_subscribe")}</Button>
  162. </DialogFooter>
  163. </>
  164. );
  165. };
  166. const LoginPage = (props) => {
  167. const { t } = useTranslation();
  168. const [username, setUsername] = useState("");
  169. const [password, setPassword] = useState("");
  170. const [errorText, setErrorText] = useState("");
  171. const baseUrl = (props.baseUrl) ? props.baseUrl : window.location.origin;
  172. const topic = props.topic;
  173. const handleLogin = async () => {
  174. const user = {baseUrl, username, password};
  175. const success = await api.topicAuth(baseUrl, topic, user);
  176. if (!success) {
  177. console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
  178. setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
  179. return;
  180. }
  181. console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
  182. await userManager.save(user);
  183. props.onSuccess();
  184. };
  185. return (
  186. <>
  187. <DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle>
  188. <DialogContent>
  189. <DialogContentText>
  190. {t("subscribe_dialog_login_description")}
  191. </DialogContentText>
  192. <TextField
  193. autoFocus
  194. margin="dense"
  195. id="username"
  196. label={t("subscribe_dialog_login_username_label")}
  197. value={username}
  198. onChange={ev => setUsername(ev.target.value)}
  199. type="text"
  200. fullWidth
  201. variant="standard"
  202. inputProps={{
  203. "aria-label": t("subscribe_dialog_login_username_label")
  204. }}
  205. />
  206. <TextField
  207. margin="dense"
  208. id="password"
  209. label={t("subscribe_dialog_login_password_label")}
  210. type="password"
  211. value={password}
  212. onChange={ev => setPassword(ev.target.value)}
  213. fullWidth
  214. variant="standard"
  215. inputProps={{
  216. "aria-label": t("subscribe_dialog_login_password_label")
  217. }}
  218. />
  219. </DialogContent>
  220. <DialogFooter status={errorText}>
  221. <Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
  222. <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
  223. </DialogFooter>
  224. </>
  225. );
  226. };
  227. export default SubscribeDialog;