Line width
This commit is contained in:
		
							parent
							
								
									2e27f58963
								
							
						
					
					
						commit
						ca5d736a71
					
				
					 33 changed files with 521 additions and 2033 deletions
				
			
		|  | @ -38,18 +38,8 @@ import DialogContent from "@mui/material/DialogContent"; | |||
| import TextField from "@mui/material/TextField"; | ||||
| import routes from "./routes"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import { | ||||
|   formatBytes, | ||||
|   formatShortDate, | ||||
|   formatShortDateTime, | ||||
|   openUrl, | ||||
| } from "../app/utils"; | ||||
| import accountApi, { | ||||
|   LimitBasis, | ||||
|   Role, | ||||
|   SubscriptionInterval, | ||||
|   SubscriptionStatus, | ||||
| } from "../app/AccountApi"; | ||||
| import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; | ||||
| import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; | ||||
| import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; | ||||
| import { Pref, PrefGroup } from "./Pref"; | ||||
| import db from "../app/db"; | ||||
|  | @ -108,11 +98,7 @@ const Username = () => { | |||
|   const labelId = "prefUsername"; | ||||
| 
 | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("account_basics_username_title")} | ||||
|       description={t("account_basics_username_description")} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}> | ||||
|       <div aria-labelledby={labelId}> | ||||
|         {session.username()} | ||||
|         {account?.role === Role.ADMIN ? ( | ||||
|  | @ -146,30 +132,16 @@ const ChangePassword = () => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("account_basics_password_title")} | ||||
|       description={t("account_basics_password_description")} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("account_basics_password_title")} description={t("account_basics_password_description")}> | ||||
|       <div aria-labelledby={labelId}> | ||||
|         <Typography | ||||
|           color="gray" | ||||
|           sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }} | ||||
|         > | ||||
|         <Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}> | ||||
|           ⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ | ||||
|         </Typography> | ||||
|         <IconButton | ||||
|           onClick={handleDialogOpen} | ||||
|           aria-label={t("account_basics_password_description")} | ||||
|         > | ||||
|         <IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}> | ||||
|           <EditIcon /> | ||||
|         </IconButton> | ||||
|       </div> | ||||
|       <ChangePasswordDialog | ||||
|         key={`changePasswordDialog${dialogKey}`} | ||||
|         open={dialogOpen} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> | ||||
|     </Pref> | ||||
|   ); | ||||
| }; | ||||
|  | @ -190,9 +162,7 @@ const ChangePasswordDialog = (props) => { | |||
|     } catch (e) { | ||||
|       console.log(`[Account] Error changing password`, e); | ||||
|       if (e instanceof IncorrectPasswordError) { | ||||
|         setError( | ||||
|           t("account_basics_password_dialog_current_password_incorrect") | ||||
|         ); | ||||
|         setError(t("account_basics_password_dialog_current_password_incorrect")); | ||||
|       } else if (e instanceof UnauthorizedError) { | ||||
|         session.resetAndRedirect(routes.login); | ||||
|       } else { | ||||
|  | @ -209,9 +179,7 @@ const ChangePasswordDialog = (props) => { | |||
|           margin="dense" | ||||
|           id="current-password" | ||||
|           label={t("account_basics_password_dialog_current_password_label")} | ||||
|           aria-label={t( | ||||
|             "account_basics_password_dialog_current_password_label" | ||||
|           )} | ||||
|           aria-label={t("account_basics_password_dialog_current_password_label")} | ||||
|           type="password" | ||||
|           value={currentPassword} | ||||
|           onChange={(ev) => setCurrentPassword(ev.target.value)} | ||||
|  | @ -233,9 +201,7 @@ const ChangePasswordDialog = (props) => { | |||
|           margin="dense" | ||||
|           id="confirm" | ||||
|           label={t("account_basics_password_dialog_confirm_password_label")} | ||||
|           aria-label={t( | ||||
|             "account_basics_password_dialog_confirm_password_label" | ||||
|           )} | ||||
|           aria-label={t("account_basics_password_dialog_confirm_password_label")} | ||||
|           type="password" | ||||
|           value={confirmPassword} | ||||
|           onChange={(ev) => setConfirmPassword(ev.target.value)} | ||||
|  | @ -245,14 +211,7 @@ const ChangePasswordDialog = (props) => { | |||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onClose}>{t("common_cancel")}</Button> | ||||
|         <Button | ||||
|           onClick={handleDialogSubmit} | ||||
|           disabled={ | ||||
|             newPassword.length === 0 || | ||||
|             currentPassword.length === 0 || | ||||
|             newPassword !== confirmPassword | ||||
|           } | ||||
|         > | ||||
|         <Button onClick={handleDialogSubmit} disabled={newPassword.length === 0 || currentPassword.length === 0 || newPassword !== confirmPassword}> | ||||
|           {t("account_basics_password_dialog_button_submit")} | ||||
|         </Button> | ||||
|       </DialogFooter> | ||||
|  | @ -299,9 +258,7 @@ const AccountType = () => { | |||
|       : t("account_basics_tier_admin_suffix_no_tier"); | ||||
|     accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; | ||||
|   } else if (!account.tier) { | ||||
|     accountType = config.enable_payments | ||||
|       ? t("account_basics_tier_free") | ||||
|       : t("account_basics_tier_basic"); | ||||
|     accountType = config.enable_payments ? t("account_basics_tier_free") : t("account_basics_tier_basic"); | ||||
|   } else { | ||||
|     accountType = account.tier.name; | ||||
|     if (account.billing?.interval === SubscriptionInterval.MONTH) { | ||||
|  | @ -313,10 +270,7 @@ const AccountType = () => { | |||
| 
 | ||||
|   return ( | ||||
|     <Pref | ||||
|       alignTop={ | ||||
|         account.billing?.status === SubscriptionStatus.PAST_DUE || | ||||
|         account.billing?.cancel_at > 0 | ||||
|       } | ||||
|       alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0} | ||||
|       title={t("account_basics_tier_title")} | ||||
|       description={t("account_basics_tier_description")} | ||||
|     > | ||||
|  | @ -333,49 +287,23 @@ const AccountType = () => { | |||
|             </span> | ||||
|           </Tooltip> | ||||
|         )} | ||||
|         {config.enable_payments && | ||||
|           account.role === Role.USER && | ||||
|           !account.billing?.subscription && ( | ||||
|             <Button | ||||
|               variant="outlined" | ||||
|               size="small" | ||||
|               startIcon={<CelebrationIcon sx={{ color: "#55b86e" }} />} | ||||
|               onClick={handleUpgradeClick} | ||||
|               sx={{ ml: 1 }} | ||||
|             > | ||||
|               {t("account_basics_tier_upgrade_button")} | ||||
|             </Button> | ||||
|           )} | ||||
|         {config.enable_payments && | ||||
|           account.role === Role.USER && | ||||
|           account.billing?.subscription && ( | ||||
|             <Button | ||||
|               variant="outlined" | ||||
|               size="small" | ||||
|               onClick={handleUpgradeClick} | ||||
|               sx={{ ml: 1 }} | ||||
|             > | ||||
|               {t("account_basics_tier_change_button")} | ||||
|             </Button> | ||||
|           )} | ||||
|         {config.enable_payments && | ||||
|           account.role === Role.USER && | ||||
|           account.billing?.customer && ( | ||||
|             <Button | ||||
|               variant="outlined" | ||||
|               size="small" | ||||
|               onClick={handleManageBilling} | ||||
|               sx={{ ml: 1 }} | ||||
|             > | ||||
|               {t("account_basics_tier_manage_billing_button")} | ||||
|             </Button> | ||||
|           )} | ||||
|         {config.enable_payments && account.role === Role.USER && !account.billing?.subscription && ( | ||||
|           <Button variant="outlined" size="small" startIcon={<CelebrationIcon sx={{ color: "#55b86e" }} />} onClick={handleUpgradeClick} sx={{ ml: 1 }}> | ||||
|             {t("account_basics_tier_upgrade_button")} | ||||
|           </Button> | ||||
|         )} | ||||
|         {config.enable_payments && account.role === Role.USER && account.billing?.subscription && ( | ||||
|           <Button variant="outlined" size="small" onClick={handleUpgradeClick} sx={{ ml: 1 }}> | ||||
|             {t("account_basics_tier_change_button")} | ||||
|           </Button> | ||||
|         )} | ||||
|         {config.enable_payments && account.role === Role.USER && account.billing?.customer && ( | ||||
|           <Button variant="outlined" size="small" onClick={handleManageBilling} sx={{ ml: 1 }}> | ||||
|             {t("account_basics_tier_manage_billing_button")} | ||||
|           </Button> | ||||
|         )} | ||||
|         {config.enable_payments && ( | ||||
|           <UpgradeDialog | ||||
|             key={`upgradeDialogFromAccount${upgradeDialogKey}`} | ||||
|             open={upgradeDialogOpen} | ||||
|             onCancel={() => setUpgradeDialogOpen(false)} | ||||
|           /> | ||||
|           <UpgradeDialog key={`upgradeDialogFromAccount${upgradeDialogKey}`} open={upgradeDialogOpen} onCancel={() => setUpgradeDialogOpen(false)} /> | ||||
|         )} | ||||
|       </div> | ||||
|       {account.billing?.status === SubscriptionStatus.PAST_DUE && ( | ||||
|  | @ -456,11 +384,7 @@ const PhoneNumbers = () => { | |||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("account_basics_phone_numbers_title")} | ||||
|       description={t("account_basics_phone_numbers_description")} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}> | ||||
|       <div aria-labelledby={labelId}> | ||||
|         {account?.phone_numbers?.map((phoneNumber) => ( | ||||
|           <Chip | ||||
|  | @ -474,18 +398,12 @@ const PhoneNumbers = () => { | |||
|             onDelete={() => handleDelete(phoneNumber)} | ||||
|           /> | ||||
|         ))} | ||||
|         {!account?.phone_numbers && ( | ||||
|           <em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em> | ||||
|         )} | ||||
|         {!account?.phone_numbers && <em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em>} | ||||
|         <IconButton onClick={handleDialogOpen}> | ||||
|           <AddIcon /> | ||||
|         </IconButton> | ||||
|       </div> | ||||
|       <AddPhoneNumberDialog | ||||
|         key={`addPhoneNumberDialog${dialogKey}`} | ||||
|         open={dialogOpen} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <AddPhoneNumberDialog key={`addPhoneNumberDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> | ||||
|       <Portal> | ||||
|         <Snackbar | ||||
|           open={snackOpen} | ||||
|  | @ -561,22 +479,16 @@ const AddPhoneNumberDialog = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||
|       <DialogTitle> | ||||
|         {t("account_basics_phone_numbers_dialog_title")} | ||||
|       </DialogTitle> | ||||
|       <DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("account_basics_phone_numbers_dialog_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("account_basics_phone_numbers_dialog_description")}</DialogContentText> | ||||
|         {!verificationCodeSent && ( | ||||
|           <div style={{ display: "flex" }}> | ||||
|             <TextField | ||||
|               margin="dense" | ||||
|               label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|               aria-label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|               placeholder={t( | ||||
|                 "account_basics_phone_numbers_dialog_number_placeholder" | ||||
|               )} | ||||
|               placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")} | ||||
|               type="tel" | ||||
|               value={phoneNumber} | ||||
|               onChange={(ev) => setPhoneNumber(ev.target.value)} | ||||
|  | @ -585,28 +497,15 @@ const AddPhoneNumberDialog = (props) => { | |||
|               sx={{ flexGrow: 1 }} | ||||
|             /> | ||||
|             <FormControl sx={{ flexWrap: "nowrap" }}> | ||||
|               <RadioGroup | ||||
|                 row | ||||
|                 sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }} | ||||
|               > | ||||
|               <RadioGroup row sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}> | ||||
|                 <FormControlLabel | ||||
|                   value="sms" | ||||
|                   control={ | ||||
|                     <Radio | ||||
|                       checked={channel === "sms"} | ||||
|                       onChange={(e) => setChannel(e.target.value)} | ||||
|                     /> | ||||
|                   } | ||||
|                   control={<Radio checked={channel === "sms"} onChange={(e) => setChannel(e.target.value)} />} | ||||
|                   label={t("account_basics_phone_numbers_dialog_channel_sms")} | ||||
|                 /> | ||||
|                 <FormControlLabel | ||||
|                   value="call" | ||||
|                   control={ | ||||
|                     <Radio | ||||
|                       checked={channel === "call"} | ||||
|                       onChange={(e) => setChannel(e.target.value)} | ||||
|                     /> | ||||
|                   } | ||||
|                   control={<Radio checked={channel === "call"} onChange={(e) => setChannel(e.target.value)} />} | ||||
|                   label={t("account_basics_phone_numbers_dialog_channel_call")} | ||||
|                   sx={{ marginRight: 0 }} | ||||
|                 /> | ||||
|  | @ -619,9 +518,7 @@ const AddPhoneNumberDialog = (props) => { | |||
|             margin="dense" | ||||
|             label={t("account_basics_phone_numbers_dialog_code_label")} | ||||
|             aria-label={t("account_basics_phone_numbers_dialog_code_label")} | ||||
|             placeholder={t( | ||||
|               "account_basics_phone_numbers_dialog_code_placeholder" | ||||
|             )} | ||||
|             placeholder={t("account_basics_phone_numbers_dialog_code_placeholder")} | ||||
|             type="text" | ||||
|             value={code} | ||||
|             onChange={(ev) => setCode(ev.target.value)} | ||||
|  | @ -632,21 +529,11 @@ const AddPhoneNumberDialog = (props) => { | |||
|         )} | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={handleCancel}> | ||||
|           {verificationCodeSent ? t("common_back") : t("common_cancel")} | ||||
|         </Button> | ||||
|         <Button | ||||
|           onClick={handleDialogSubmit} | ||||
|           disabled={sending || !/^\+\d+$/.test(phoneNumber)} | ||||
|         > | ||||
|           {!verificationCodeSent && | ||||
|             channel === "sms" && | ||||
|             t("account_basics_phone_numbers_dialog_verify_button_sms")} | ||||
|           {!verificationCodeSent && | ||||
|             channel === "call" && | ||||
|             t("account_basics_phone_numbers_dialog_verify_button_call")} | ||||
|           {verificationCodeSent && | ||||
|             t("account_basics_phone_numbers_dialog_check_verification_button")} | ||||
|         <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> | ||||
|         <Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}> | ||||
|           {!verificationCodeSent && channel === "sms" && t("account_basics_phone_numbers_dialog_verify_button_sms")} | ||||
|           {!verificationCodeSent && channel === "call" && t("account_basics_phone_numbers_dialog_verify_button_call")} | ||||
|           {verificationCodeSent && t("account_basics_phone_numbers_dialog_check_verification_button")} | ||||
|         </Button> | ||||
|       </DialogFooter> | ||||
|     </Dialog> | ||||
|  | @ -687,14 +574,7 @@ const Stats = () => { | |||
|             </div> | ||||
|             <LinearProgress | ||||
|               variant="determinate" | ||||
|               value={ | ||||
|                 account.role === Role.USER && account.limits.reservations > 0 | ||||
|                   ? normalize( | ||||
|                       account.stats.reservations, | ||||
|                       account.limits.reservations | ||||
|                     ) | ||||
|                   : 100 | ||||
|               } | ||||
|               value={account.role === Role.USER && account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} | ||||
|             /> | ||||
|           </Pref> | ||||
|         )} | ||||
|  | @ -722,14 +602,7 @@ const Stats = () => { | |||
|                 : t("account_usage_unlimited")} | ||||
|             </Typography> | ||||
|           </div> | ||||
|           <LinearProgress | ||||
|             variant="determinate" | ||||
|             value={ | ||||
|               account.role === Role.USER | ||||
|                 ? normalize(account.stats.messages, account.limits.messages) | ||||
|                 : 100 | ||||
|             } | ||||
|           /> | ||||
|           <LinearProgress variant="determinate" value={account.role === Role.USER ? normalize(account.stats.messages, account.limits.messages) : 100} /> | ||||
|         </Pref> | ||||
|         {config.enable_emails && ( | ||||
|           <Pref | ||||
|  | @ -756,64 +629,49 @@ const Stats = () => { | |||
|                   : t("account_usage_unlimited")} | ||||
|               </Typography> | ||||
|             </div> | ||||
|             <LinearProgress variant="determinate" value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100} /> | ||||
|           </Pref> | ||||
|         )} | ||||
|         {config.enable_calls && (account.role === Role.ADMIN || account.limits.calls > 0) && ( | ||||
|           <Pref | ||||
|             title={ | ||||
|               <> | ||||
|                 {t("account_usage_calls_title")} | ||||
|                 <Tooltip title={t("account_usage_limits_reset_daily")}> | ||||
|                   <span> | ||||
|                     <InfoIcon /> | ||||
|                   </span> | ||||
|                 </Tooltip> | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <div> | ||||
|               <Typography variant="body2" sx={{ float: "left" }}> | ||||
|                 {account.stats.calls.toLocaleString()} | ||||
|               </Typography> | ||||
|               <Typography variant="body2" sx={{ float: "right" }}> | ||||
|                 {account.role === Role.USER | ||||
|                   ? t("account_usage_of_limit", { | ||||
|                       limit: account.limits.calls.toLocaleString(), | ||||
|                     }) | ||||
|                   : t("account_usage_unlimited")} | ||||
|               </Typography> | ||||
|             </div> | ||||
|             <LinearProgress | ||||
|               variant="determinate" | ||||
|               value={ | ||||
|                 account.role === Role.USER | ||||
|                   ? normalize(account.stats.emails, account.limits.emails) | ||||
|                   : 100 | ||||
|               } | ||||
|               value={account.role === Role.USER && account.limits.calls > 0 ? normalize(account.stats.calls, account.limits.calls) : 100} | ||||
|             /> | ||||
|           </Pref> | ||||
|         )} | ||||
|         {config.enable_calls && | ||||
|           (account.role === Role.ADMIN || account.limits.calls > 0) && ( | ||||
|             <Pref | ||||
|               title={ | ||||
|                 <> | ||||
|                   {t("account_usage_calls_title")} | ||||
|                   <Tooltip title={t("account_usage_limits_reset_daily")}> | ||||
|                     <span> | ||||
|                       <InfoIcon /> | ||||
|                     </span> | ||||
|                   </Tooltip> | ||||
|                 </> | ||||
|               } | ||||
|             > | ||||
|               <div> | ||||
|                 <Typography variant="body2" sx={{ float: "left" }}> | ||||
|                   {account.stats.calls.toLocaleString()} | ||||
|                 </Typography> | ||||
|                 <Typography variant="body2" sx={{ float: "right" }}> | ||||
|                   {account.role === Role.USER | ||||
|                     ? t("account_usage_of_limit", { | ||||
|                         limit: account.limits.calls.toLocaleString(), | ||||
|                       }) | ||||
|                     : t("account_usage_unlimited")} | ||||
|                 </Typography> | ||||
|               </div> | ||||
|               <LinearProgress | ||||
|                 variant="determinate" | ||||
|                 value={ | ||||
|                   account.role === Role.USER && account.limits.calls > 0 | ||||
|                     ? normalize(account.stats.calls, account.limits.calls) | ||||
|                     : 100 | ||||
|                 } | ||||
|               /> | ||||
|             </Pref> | ||||
|           )} | ||||
|         <Pref | ||||
|           alignTop | ||||
|           title={t("account_usage_attachment_storage_title")} | ||||
|           description={t("account_usage_attachment_storage_description", { | ||||
|             filesize: formatBytes(account.limits.attachment_file_size), | ||||
|             expiry: humanizeDuration( | ||||
|               account.limits.attachment_expiry_duration * 1000, | ||||
|               { | ||||
|                 language: i18n.resolvedLanguage, | ||||
|                 fallbacks: ["en"], | ||||
|               } | ||||
|             ), | ||||
|             expiry: humanizeDuration(account.limits.attachment_expiry_duration * 1000, { | ||||
|               language: i18n.resolvedLanguage, | ||||
|               fallbacks: ["en"], | ||||
|             }), | ||||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|  | @ -830,49 +688,36 @@ const Stats = () => { | |||
|           </div> | ||||
|           <LinearProgress | ||||
|             variant="determinate" | ||||
|             value={ | ||||
|               account.role === Role.USER | ||||
|                 ? normalize( | ||||
|                     account.stats.attachment_total_size, | ||||
|                     account.limits.attachment_total_size | ||||
|                   ) | ||||
|                 : 100 | ||||
|             } | ||||
|             value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} | ||||
|           /> | ||||
|         </Pref> | ||||
|         {config.enable_reservations && | ||||
|           account.role === Role.USER && | ||||
|           account.limits.reservations === 0 && ( | ||||
|             <Pref | ||||
|               title={ | ||||
|                 <> | ||||
|                   {t("account_usage_reservations_title")} | ||||
|                   {config.enable_payments && <ProChip />} | ||||
|                 </> | ||||
|               } | ||||
|             > | ||||
|               <em>{t("account_usage_reservations_none")}</em> | ||||
|             </Pref> | ||||
|           )} | ||||
|         {config.enable_calls && | ||||
|           account.role === Role.USER && | ||||
|           account.limits.calls === 0 && ( | ||||
|             <Pref | ||||
|               title={ | ||||
|                 <> | ||||
|                   {t("account_usage_calls_title")} | ||||
|                   {config.enable_payments && <ProChip />} | ||||
|                 </> | ||||
|               } | ||||
|             > | ||||
|               <em>{t("account_usage_calls_none")}</em> | ||||
|             </Pref> | ||||
|           )} | ||||
|         {config.enable_reservations && account.role === Role.USER && account.limits.reservations === 0 && ( | ||||
|           <Pref | ||||
|             title={ | ||||
|               <> | ||||
|                 {t("account_usage_reservations_title")} | ||||
|                 {config.enable_payments && <ProChip />} | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <em>{t("account_usage_reservations_none")}</em> | ||||
|           </Pref> | ||||
|         )} | ||||
|         {config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && ( | ||||
|           <Pref | ||||
|             title={ | ||||
|               <> | ||||
|                 {t("account_usage_calls_title")} | ||||
|                 {config.enable_payments && <ProChip />} | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <em>{t("account_usage_calls_none")}</em> | ||||
|           </Pref> | ||||
|         )} | ||||
|       </PrefGroup> | ||||
|       {account.role === Role.USER && account.limits.basis === LimitBasis.IP && ( | ||||
|         <Typography variant="body1"> | ||||
|           {t("account_usage_basis_ip_description")} | ||||
|         </Typography> | ||||
|         <Typography variant="body1">{t("account_usage_basis_ip_description")}</Typography> | ||||
|       )} | ||||
|     </Card> | ||||
|   ); | ||||
|  | @ -928,15 +773,9 @@ const Tokens = () => { | |||
|         {tokens?.length > 0 && <TokensTable tokens={tokens} />} | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         <Button onClick={handleCreateClick}> | ||||
|           {t("account_tokens_table_create_token_button")} | ||||
|         </Button> | ||||
|         <Button onClick={handleCreateClick}>{t("account_tokens_table_create_token_button")}</Button> | ||||
|       </CardActions> | ||||
|       <TokenDialog | ||||
|         key={`tokenDialogCreate${dialogKey}`} | ||||
|         open={dialogOpen} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <TokenDialog key={`tokenDialogCreate${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
|  | @ -984,9 +823,7 @@ const TokensTable = (props) => { | |||
|     <Table size="small" aria-label={t("account_tokens_title")}> | ||||
|       <TableHead> | ||||
|         <TableRow> | ||||
|           <TableCell sx={{ paddingLeft: 0 }}> | ||||
|             {t("account_tokens_table_token_header")} | ||||
|           </TableCell> | ||||
|           <TableCell sx={{ paddingLeft: 0 }}>{t("account_tokens_table_token_header")}</TableCell> | ||||
|           <TableCell>{t("account_tokens_table_label_header")}</TableCell> | ||||
|           <TableCell>{t("account_tokens_table_expires_header")}</TableCell> | ||||
|           <TableCell>{t("account_tokens_table_last_access_header")}</TableCell> | ||||
|  | @ -995,25 +832,12 @@ const TokensTable = (props) => { | |||
|       </TableHead> | ||||
|       <TableBody> | ||||
|         {tokens.map((token) => ( | ||||
|           <TableRow | ||||
|             key={token.token} | ||||
|             sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|           > | ||||
|             <TableCell | ||||
|               component="th" | ||||
|               scope="row" | ||||
|               sx={{ paddingLeft: 0, whiteSpace: "nowrap" }} | ||||
|               aria-label={t("account_tokens_table_token_header")} | ||||
|             > | ||||
|           <TableRow key={token.token} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}> | ||||
|             <TableCell component="th" scope="row" sx={{ paddingLeft: 0, whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_token_header")}> | ||||
|               <span> | ||||
|                 <span style={{ fontFamily: "Monospace", fontSize: "0.9rem" }}> | ||||
|                   {token.token.slice(0, 12)} | ||||
|                 </span> | ||||
|                 <span style={{ fontFamily: "Monospace", fontSize: "0.9rem" }}>{token.token.slice(0, 12)}</span> | ||||
|                 ... | ||||
|                 <Tooltip | ||||
|                   title={t("common_copy_to_clipboard")} | ||||
|                   placement="right" | ||||
|                 > | ||||
|                 <Tooltip title={t("common_copy_to_clipboard")} placement="right"> | ||||
|                   <IconButton onClick={() => handleCopy(token.token)}> | ||||
|                     <ContentCopy /> | ||||
|                   </IconButton> | ||||
|  | @ -1021,25 +845,13 @@ const TokensTable = (props) => { | |||
|               </span> | ||||
|             </TableCell> | ||||
|             <TableCell aria-label={t("account_tokens_table_label_header")}> | ||||
|               {token.token === session.token() && ( | ||||
|                 <em>{t("account_tokens_table_current_session")}</em> | ||||
|               )} | ||||
|               {token.token === session.token() && <em>{t("account_tokens_table_current_session")}</em>} | ||||
|               {token.token !== session.token() && (token.label || "-")} | ||||
|             </TableCell> | ||||
|             <TableCell | ||||
|               sx={{ whiteSpace: "nowrap" }} | ||||
|               aria-label={t("account_tokens_table_expires_header")} | ||||
|             > | ||||
|               {token.expires ? ( | ||||
|                 formatShortDateTime(token.expires) | ||||
|               ) : ( | ||||
|                 <em>{t("account_tokens_table_never_expires")}</em> | ||||
|               )} | ||||
|             <TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_expires_header")}> | ||||
|               {token.expires ? formatShortDateTime(token.expires) : <em>{t("account_tokens_table_never_expires")}</em>} | ||||
|             </TableCell> | ||||
|             <TableCell | ||||
|               sx={{ whiteSpace: "nowrap" }} | ||||
|               aria-label={t("account_tokens_table_last_access_header")} | ||||
|             > | ||||
|             <TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_last_access_header")}> | ||||
|               <div style={{ display: "flex", alignItems: "center" }}> | ||||
|                 <span>{formatShortDateTime(token.last_access)}</span> | ||||
|                 <Tooltip | ||||
|  | @ -1047,13 +859,7 @@ const TokensTable = (props) => { | |||
|                     ip: token.last_origin, | ||||
|                   })} | ||||
|                 > | ||||
|                   <IconButton | ||||
|                     onClick={() => | ||||
|                       openUrl( | ||||
|                         `https://whatismyipaddress.com/ip/${token.last_origin}` | ||||
|                       ) | ||||
|                     } | ||||
|                   > | ||||
|                   <IconButton onClick={() => openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}> | ||||
|                     <Public /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|  | @ -1062,24 +868,16 @@ const TokensTable = (props) => { | |||
|             <TableCell align="right" sx={{ whiteSpace: "nowrap" }}> | ||||
|               {token.token !== session.token() && ( | ||||
|                 <> | ||||
|                   <IconButton | ||||
|                     onClick={() => handleEditClick(token)} | ||||
|                     aria-label={t("account_tokens_dialog_title_edit")} | ||||
|                   > | ||||
|                   <IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> | ||||
|                     <EditIcon /> | ||||
|                   </IconButton> | ||||
|                   <IconButton | ||||
|                     onClick={() => handleDeleteClick(token)} | ||||
|                     aria-label={t("account_tokens_dialog_title_delete")} | ||||
|                   > | ||||
|                   <IconButton onClick={() => handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}> | ||||
|                     <CloseIcon /> | ||||
|                   </IconButton> | ||||
|                 </> | ||||
|               )} | ||||
|               {token.token === session.token() && ( | ||||
|                 <Tooltip | ||||
|                   title={t("account_tokens_table_cannot_delete_or_edit")} | ||||
|                 > | ||||
|                 <Tooltip title={t("account_tokens_table_cannot_delete_or_edit")}> | ||||
|                   <span> | ||||
|                     <IconButton disabled> | ||||
|                       <EditIcon /> | ||||
|  | @ -1095,24 +893,10 @@ const TokensTable = (props) => { | |||
|         ))} | ||||
|       </TableBody> | ||||
|       <Portal> | ||||
|         <Snackbar | ||||
|           open={snackOpen} | ||||
|           autoHideDuration={3000} | ||||
|           onClose={() => setSnackOpen(false)} | ||||
|           message={t("account_tokens_table_copied_to_clipboard")} | ||||
|         /> | ||||
|         <Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("account_tokens_table_copied_to_clipboard")} /> | ||||
|       </Portal> | ||||
|       <TokenDialog | ||||
|         key={`tokenDialogEdit${upsertDialogKey}`} | ||||
|         open={upsertDialogOpen} | ||||
|         token={selectedToken} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <TokenDeleteDialog | ||||
|         open={deleteDialogOpen} | ||||
|         token={selectedToken} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <TokenDialog key={`tokenDialogEdit${upsertDialogKey}`} open={upsertDialogOpen} token={selectedToken} onClose={handleDialogClose} /> | ||||
|       <TokenDeleteDialog open={deleteDialogOpen} token={selectedToken} onClose={handleDialogClose} /> | ||||
|     </Table> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1144,18 +928,8 @@ const TokenDialog = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onClose} | ||||
|       maxWidth="sm" | ||||
|       fullWidth | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|       <DialogTitle> | ||||
|         {editMode | ||||
|           ? t("account_tokens_dialog_title_edit") | ||||
|           : t("account_tokens_dialog_title_create")} | ||||
|       </DialogTitle> | ||||
|     <Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> | ||||
|       <DialogTitle>{editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <TextField | ||||
|           margin="dense" | ||||
|  | @ -1169,52 +943,22 @@ const TokenDialog = (props) => { | |||
|           variant="standard" | ||||
|         /> | ||||
|         <FormControl fullWidth variant="standard" sx={{ mt: 1 }}> | ||||
|           <Select | ||||
|             value={expires} | ||||
|             onChange={(ev) => setExpires(ev.target.value)} | ||||
|             aria-label={t("account_tokens_dialog_expires_label")} | ||||
|           > | ||||
|             {editMode && ( | ||||
|               <MenuItem value={-1}> | ||||
|                 {t("account_tokens_dialog_expires_unchanged")} | ||||
|               </MenuItem> | ||||
|             )} | ||||
|             <MenuItem value={0}> | ||||
|               {t("account_tokens_dialog_expires_never")} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={21600}> | ||||
|               {t("account_tokens_dialog_expires_x_hours", { hours: 6 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={43200}> | ||||
|               {t("account_tokens_dialog_expires_x_hours", { hours: 12 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={259200}> | ||||
|               {t("account_tokens_dialog_expires_x_days", { days: 3 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={604800}> | ||||
|               {t("account_tokens_dialog_expires_x_days", { days: 7 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={2592000}> | ||||
|               {t("account_tokens_dialog_expires_x_days", { days: 30 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={7776000}> | ||||
|               {t("account_tokens_dialog_expires_x_days", { days: 90 })} | ||||
|             </MenuItem> | ||||
|             <MenuItem value={15552000}> | ||||
|               {t("account_tokens_dialog_expires_x_days", { days: 180 })} | ||||
|             </MenuItem> | ||||
|           <Select value={expires} onChange={(ev) => setExpires(ev.target.value)} aria-label={t("account_tokens_dialog_expires_label")}> | ||||
|             {editMode && <MenuItem value={-1}>{t("account_tokens_dialog_expires_unchanged")}</MenuItem>} | ||||
|             <MenuItem value={0}>{t("account_tokens_dialog_expires_never")}</MenuItem> | ||||
|             <MenuItem value={21600}>{t("account_tokens_dialog_expires_x_hours", { hours: 6 })}</MenuItem> | ||||
|             <MenuItem value={43200}>{t("account_tokens_dialog_expires_x_hours", { hours: 12 })}</MenuItem> | ||||
|             <MenuItem value={259200}>{t("account_tokens_dialog_expires_x_days", { days: 3 })}</MenuItem> | ||||
|             <MenuItem value={604800}>{t("account_tokens_dialog_expires_x_days", { days: 7 })}</MenuItem> | ||||
|             <MenuItem value={2592000}>{t("account_tokens_dialog_expires_x_days", { days: 30 })}</MenuItem> | ||||
|             <MenuItem value={7776000}>{t("account_tokens_dialog_expires_x_days", { days: 90 })}</MenuItem> | ||||
|             <MenuItem value={15552000}>{t("account_tokens_dialog_expires_x_days", { days: 180 })}</MenuItem> | ||||
|           </Select> | ||||
|         </FormControl> | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onClose}> | ||||
|           {t("account_tokens_dialog_button_cancel")} | ||||
|         </Button> | ||||
|         <Button onClick={handleSubmit}> | ||||
|           {editMode | ||||
|             ? t("account_tokens_dialog_button_update") | ||||
|             : t("account_tokens_dialog_button_create")} | ||||
|         </Button> | ||||
|         <Button onClick={props.onClose}>{t("account_tokens_dialog_button_cancel")}</Button> | ||||
|         <Button onClick={handleSubmit}>{editMode ? t("account_tokens_dialog_button_update") : t("account_tokens_dialog_button_create")}</Button> | ||||
|       </DialogFooter> | ||||
|     </Dialog> | ||||
|   ); | ||||
|  | @ -1285,26 +1029,13 @@ const DeleteAccount = () => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Pref | ||||
|       title={t("account_delete_title")} | ||||
|       description={t("account_delete_description")} | ||||
|     > | ||||
|     <Pref title={t("account_delete_title")} description={t("account_delete_description")}> | ||||
|       <div> | ||||
|         <Button | ||||
|           fullWidth={false} | ||||
|           variant="outlined" | ||||
|           color="error" | ||||
|           startIcon={<DeleteOutlineIcon />} | ||||
|           onClick={handleDialogOpen} | ||||
|         > | ||||
|         <Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}> | ||||
|           {t("account_delete_title")} | ||||
|         </Button> | ||||
|       </div> | ||||
|       <DeleteAccountDialog | ||||
|         key={`deleteAccountDialog${dialogKey}`} | ||||
|         open={dialogOpen} | ||||
|         onClose={handleDialogClose} | ||||
|       /> | ||||
|       <DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> | ||||
|     </Pref> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1325,9 +1056,7 @@ const DeleteAccountDialog = (props) => { | |||
|     } catch (e) { | ||||
|       console.log(`[Account] Error deleting account`, e); | ||||
|       if (e instanceof IncorrectPasswordError) { | ||||
|         setError( | ||||
|           t("account_basics_password_dialog_current_password_incorrect") | ||||
|         ); | ||||
|         setError(t("account_basics_password_dialog_current_password_incorrect")); | ||||
|       } else if (e instanceof UnauthorizedError) { | ||||
|         session.resetAndRedirect(routes.login); | ||||
|       } else { | ||||
|  | @ -1340,9 +1069,7 @@ const DeleteAccountDialog = (props) => { | |||
|     <Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}> | ||||
|       <DialogTitle>{t("account_delete_title")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <Typography variant="body1"> | ||||
|           {t("account_delete_dialog_description")} | ||||
|         </Typography> | ||||
|         <Typography variant="body1">{t("account_delete_dialog_description")}</Typography> | ||||
|         <TextField | ||||
|           margin="dense" | ||||
|           id="account-delete-confirm" | ||||
|  | @ -1361,14 +1088,8 @@ const DeleteAccountDialog = (props) => { | |||
|         )} | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onClose}> | ||||
|           {t("account_delete_dialog_button_cancel")} | ||||
|         </Button> | ||||
|         <Button | ||||
|           onClick={handleSubmit} | ||||
|           color="error" | ||||
|           disabled={password.length === 0} | ||||
|         > | ||||
|         <Button onClick={props.onClose}>{t("account_delete_dialog_button_cancel")}</Button> | ||||
|         <Button onClick={handleSubmit} color="error" disabled={password.length === 0}> | ||||
|           {t("account_delete_dialog_button_submit")} | ||||
|         </Button> | ||||
|       </DialogFooter> | ||||
|  |  | |||
|  | @ -51,8 +51,7 @@ const ActionBar = (props) => { | |||
|       <Toolbar | ||||
|         sx={{ | ||||
|           pr: "24px", | ||||
|           background: | ||||
|             "linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%)", | ||||
|           background: "linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%)", | ||||
|         }} | ||||
|       > | ||||
|         <IconButton | ||||
|  | @ -77,12 +76,7 @@ const ActionBar = (props) => { | |||
|         <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}> | ||||
|           {title} | ||||
|         </Typography> | ||||
|         {props.selected && ( | ||||
|           <SettingsIcons | ||||
|             subscription={props.selected} | ||||
|             onUnsubscribe={props.onUnsubscribe} | ||||
|           /> | ||||
|         )} | ||||
|         {props.selected && <SettingsIcons subscription={props.selected} onUnsubscribe={props.onUnsubscribe} />} | ||||
|         <ProfileIcon /> | ||||
|       </Toolbar> | ||||
|     </AppBar> | ||||
|  | @ -101,34 +95,13 @@ const SettingsIcons = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <IconButton | ||||
|         color="inherit" | ||||
|         size="large" | ||||
|         edge="end" | ||||
|         onClick={handleToggleMute} | ||||
|         aria-label={t("action_bar_toggle_mute")} | ||||
|       > | ||||
|         {subscription.mutedUntil ? ( | ||||
|           <NotificationsOffIcon /> | ||||
|         ) : ( | ||||
|           <NotificationsIcon /> | ||||
|         )} | ||||
|       <IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} aria-label={t("action_bar_toggle_mute")}> | ||||
|         {subscription.mutedUntil ? <NotificationsOffIcon /> : <NotificationsIcon />} | ||||
|       </IconButton> | ||||
|       <IconButton | ||||
|         color="inherit" | ||||
|         size="large" | ||||
|         edge="end" | ||||
|         onClick={(ev) => setAnchorEl(ev.currentTarget)} | ||||
|         aria-label={t("action_bar_toggle_action_menu")} | ||||
|       > | ||||
|       <IconButton color="inherit" size="large" edge="end" onClick={(ev) => setAnchorEl(ev.currentTarget)} aria-label={t("action_bar_toggle_action_menu")}> | ||||
|         <MoreVertIcon /> | ||||
|       </IconButton> | ||||
|       <SubscriptionPopup | ||||
|         subscription={subscription} | ||||
|         anchor={anchorEl} | ||||
|         placement="right" | ||||
|         onClose={() => setAnchorEl(null)} | ||||
|       /> | ||||
|       <SubscriptionPopup subscription={subscription} anchor={anchorEl} placement="right" onClose={() => setAnchorEl(null)} /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | @ -159,43 +132,21 @@ const ProfileIcon = () => { | |||
|   return ( | ||||
|     <> | ||||
|       {session.exists() && ( | ||||
|         <IconButton | ||||
|           color="inherit" | ||||
|           size="large" | ||||
|           edge="end" | ||||
|           onClick={handleClick} | ||||
|           aria-label={t("action_bar_profile_title")} | ||||
|         > | ||||
|         <IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("action_bar_profile_title")}> | ||||
|           <AccountCircleIcon /> | ||||
|         </IconButton> | ||||
|       )} | ||||
|       {!session.exists() && config.enable_login && ( | ||||
|         <Button | ||||
|           color="inherit" | ||||
|           variant="text" | ||||
|           onClick={() => navigate(routes.login)} | ||||
|           sx={{ m: 1 }} | ||||
|           aria-label={t("action_bar_sign_in")} | ||||
|         > | ||||
|         <Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{ m: 1 }} aria-label={t("action_bar_sign_in")}> | ||||
|           {t("action_bar_sign_in")} | ||||
|         </Button> | ||||
|       )} | ||||
|       {!session.exists() && config.enable_signup && ( | ||||
|         <Button | ||||
|           color="inherit" | ||||
|           variant="outlined" | ||||
|           onClick={() => navigate(routes.signup)} | ||||
|           aria-label={t("action_bar_sign_up")} | ||||
|         > | ||||
|         <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)} aria-label={t("action_bar_sign_up")}> | ||||
|           {t("action_bar_sign_up")} | ||||
|         </Button> | ||||
|       )} | ||||
|       <PopupMenu | ||||
|         horizontal="right" | ||||
|         anchorEl={anchorEl} | ||||
|         open={open} | ||||
|         onClose={handleClose} | ||||
|       > | ||||
|       <PopupMenu horizontal="right" anchorEl={anchorEl} open={open} onClose={handleClose}> | ||||
|         <MenuItem onClick={() => navigate(routes.account)}> | ||||
|           <ListItemIcon> | ||||
|             <Person /> | ||||
|  |  | |||
|  | @ -1,11 +1,5 @@ | |||
| import * as React from "react"; | ||||
| import { | ||||
|   createContext, | ||||
|   Suspense, | ||||
|   useContext, | ||||
|   useEffect, | ||||
|   useState, | ||||
| } from "react"; | ||||
| import { createContext, Suspense, useContext, useEffect, useState } from "react"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import { ThemeProvider } from "@mui/material/styles"; | ||||
| import CssBaseline from "@mui/material/CssBaseline"; | ||||
|  | @ -19,21 +13,11 @@ import Preferences from "./Preferences"; | |||
| import { useLiveQuery } from "dexie-react-hooks"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import userManager from "../app/UserManager"; | ||||
| import { | ||||
|   BrowserRouter, | ||||
|   Outlet, | ||||
|   Route, | ||||
|   Routes, | ||||
|   useParams, | ||||
| } from "react-router-dom"; | ||||
| import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; | ||||
| import { expandUrl } from "../app/utils"; | ||||
| import ErrorBoundary from "./ErrorBoundary"; | ||||
| import routes from "./routes"; | ||||
| import { | ||||
|   useAccountListener, | ||||
|   useBackgroundProcesses, | ||||
|   useConnectionListeners, | ||||
| } from "./hooks"; | ||||
| import { useAccountListener, useBackgroundProcesses, useConnectionListeners } from "./hooks"; | ||||
| import PublishDialog from "./PublishDialog"; | ||||
| import Messaging from "./Messaging"; | ||||
| import "./i18n"; // Translations!
 | ||||
|  | @ -60,14 +44,8 @@ const App = () => { | |||
|                   <Route path={routes.app} element={<AllSubscriptions />} /> | ||||
|                   <Route path={routes.account} element={<Account />} /> | ||||
|                   <Route path={routes.settings} element={<Preferences />} /> | ||||
|                   <Route | ||||
|                     path={routes.subscription} | ||||
|                     element={<SingleSubscription />} | ||||
|                   /> | ||||
|                   <Route | ||||
|                     path={routes.subscriptionExternal} | ||||
|                     element={<SingleSubscription />} | ||||
|                   /> | ||||
|                   <Route path={routes.subscription} element={<SingleSubscription />} /> | ||||
|                   <Route path={routes.subscriptionExternal} element={<SingleSubscription />} /> | ||||
|                 </Route> | ||||
|               </Routes> | ||||
|             </ErrorBoundary> | ||||
|  | @ -82,22 +60,15 @@ const Layout = () => { | |||
|   const params = useParams(); | ||||
|   const { account, setAccount } = useContext(AccountContext); | ||||
|   const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); | ||||
|   const [notificationsGranted, setNotificationsGranted] = useState( | ||||
|     notifier.granted() | ||||
|   ); | ||||
|   const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); | ||||
|   const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); | ||||
|   const users = useLiveQuery(() => userManager.all()); | ||||
|   const subscriptions = useLiveQuery(() => subscriptionManager.all()); | ||||
|   const subscriptionsWithoutInternal = subscriptions?.filter( | ||||
|     (s) => !s.internal | ||||
|   ); | ||||
|   const newNotificationsCount = | ||||
|     subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; | ||||
|   const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); | ||||
|   const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; | ||||
|   const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { | ||||
|     return ( | ||||
|       (params.baseUrl && | ||||
|         expandUrl(params.baseUrl).includes(s.baseUrl) && | ||||
|         params.topic === s.topic) || | ||||
|       (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || | ||||
|       (config.base_url === s.baseUrl && params.topic === s.topic) | ||||
|     ); | ||||
|   }); | ||||
|  | @ -109,10 +80,7 @@ const Layout = () => { | |||
| 
 | ||||
|   return ( | ||||
|     <Box sx={{ display: "flex" }}> | ||||
|       <ActionBar | ||||
|         selected={selected} | ||||
|         onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|       /> | ||||
|       <ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> | ||||
|       <Navigation | ||||
|         subscriptions={subscriptionsWithoutInternal} | ||||
|         selectedSubscription={selected} | ||||
|  | @ -120,9 +88,7 @@ const Layout = () => { | |||
|         mobileDrawerOpen={mobileDrawerOpen} | ||||
|         onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|         onNotificationGranted={setNotificationsGranted} | ||||
|         onPublishMessageClick={() => | ||||
|           setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT) | ||||
|         } | ||||
|         onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)} | ||||
|       /> | ||||
|       <Main> | ||||
|         <Toolbar /> | ||||
|  | @ -133,11 +99,7 @@ const Layout = () => { | |||
|           }} | ||||
|         /> | ||||
|       </Main> | ||||
|       <Messaging | ||||
|         selected={selected} | ||||
|         dialogOpenMode={sendDialogOpenMode} | ||||
|         onDialogOpenModeChange={setSendDialogOpenMode} | ||||
|       /> | ||||
|       <Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | @ -155,10 +117,7 @@ const Main = (props) => { | |||
|         width: { sm: `calc(100% - ${Navigation.width}px)` }, | ||||
|         height: "100vh", | ||||
|         overflow: "auto", | ||||
|         backgroundColor: (theme) => | ||||
|           theme.palette.mode === "light" | ||||
|             ? theme.palette.grey[100] | ||||
|             : theme.palette.grey[900], | ||||
|         backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), | ||||
|       }} | ||||
|     > | ||||
|       {props.children} | ||||
|  | @ -171,10 +130,7 @@ const Loader = () => ( | |||
|     open={true} | ||||
|     sx={{ | ||||
|       zIndex: 100000, | ||||
|       backgroundColor: (theme) => | ||||
|         theme.palette.mode === "light" | ||||
|           ? theme.palette.grey[100] | ||||
|           : theme.palette.grey[900], | ||||
|       backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), | ||||
|     }} | ||||
|   > | ||||
|     <CircularProgress color="success" disableShrink /> | ||||
|  | @ -182,8 +138,7 @@ const Loader = () => ( | |||
| ); | ||||
| 
 | ||||
| const updateTitle = (newNotificationsCount) => { | ||||
|   document.title = | ||||
|     newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy"; | ||||
|   document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy"; | ||||
| }; | ||||
| 
 | ||||
| export default App; | ||||
|  |  | |||
|  | @ -16,11 +16,7 @@ const AvatarBox = (props) => { | |||
|         height: "100vh", | ||||
|       }} | ||||
|     > | ||||
|       <Avatar | ||||
|         sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} | ||||
|         src={logo} | ||||
|         variant="rounded" | ||||
|       /> | ||||
|       <Avatar sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} src={logo} variant="rounded" /> | ||||
|       {props.children} | ||||
|     </Box> | ||||
|   ); | ||||
|  |  | |||
|  | @ -17,8 +17,7 @@ import { useTranslation } from "react-i18next"; | |||
| // This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported.
 | ||||
| 
 | ||||
| const emojisByCategory = {}; | ||||
| const isDesktopChrome = | ||||
|   /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); | ||||
| const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); | ||||
| const maxSupportedVersionForDesktopChrome = 11; | ||||
| rawEmojis.forEach((emoji) => { | ||||
|   if (!emojisByCategory[emoji.category]) { | ||||
|  | @ -26,12 +25,9 @@ rawEmojis.forEach((emoji) => { | |||
|   } | ||||
|   try { | ||||
|     const unicodeVersion = parseFloat(emoji.unicode_version); | ||||
|     const supportedEmoji = | ||||
|       unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; | ||||
|     const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; | ||||
|     if (supportedEmoji) { | ||||
|       const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join( | ||||
|         " " | ||||
|       )} ${emoji.tags.join(" ")}`;
 | ||||
|       const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; | ||||
|       const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; | ||||
|       emojisByCategory[emoji.category].push(emojiWithSearchBase); | ||||
|     } | ||||
|  | @ -53,13 +49,7 @@ const EmojiPicker = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Popper | ||||
|       open={open} | ||||
|       anchorEl={props.anchorEl} | ||||
|       placement="bottom-start" | ||||
|       sx={{ zIndex: 10005 }} | ||||
|       transition | ||||
|     > | ||||
|     <Popper open={open} anchorEl={props.anchorEl} placement="bottom-start" sx={{ zIndex: 10005 }} transition> | ||||
|       {({ TransitionProps }) => ( | ||||
|         <ClickAwayListener onClickAway={props.onClose}> | ||||
|           <Fade {...TransitionProps} timeout={350}> | ||||
|  | @ -92,16 +82,8 @@ const EmojiPicker = (props) => { | |||
|                 }} | ||||
|                 InputProps={{ | ||||
|                   endAdornment: ( | ||||
|                     <InputAdornment | ||||
|                       position="end" | ||||
|                       sx={{ display: search ? "" : "none" }} | ||||
|                     > | ||||
|                       <IconButton | ||||
|                         size="small" | ||||
|                         onClick={handleSearchClear} | ||||
|                         edge="end" | ||||
|                         aria-label={t("emoji_picker_search_clear")} | ||||
|                       > | ||||
|                     <InputAdornment position="end" sx={{ display: search ? "" : "none" }}> | ||||
|                       <IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}> | ||||
|                         <Close /> | ||||
|                       </IconButton> | ||||
|                     </InputAdornment> | ||||
|  | @ -117,13 +99,7 @@ const EmojiPicker = (props) => { | |||
|                 }} | ||||
|               > | ||||
|                 {Object.keys(emojisByCategory).map((category) => ( | ||||
|                   <Category | ||||
|                     key={category} | ||||
|                     title={category} | ||||
|                     emojis={emojisByCategory[category]} | ||||
|                     search={searchFields} | ||||
|                     onPick={props.onEmojiPick} | ||||
|                   /> | ||||
|                   <Category key={category} title={category} emojis={emojisByCategory[category]} search={searchFields} onPick={props.onEmojiPick} /> | ||||
|                 ))} | ||||
|               </Box> | ||||
|             </Box> | ||||
|  | @ -144,12 +120,7 @@ const Category = (props) => { | |||
|         </Typography> | ||||
|       )} | ||||
|       {props.emojis.map((emoji) => ( | ||||
|         <Emoji | ||||
|           key={emoji.aliases[0]} | ||||
|           emoji={emoji} | ||||
|           search={props.search} | ||||
|           onClick={() => props.onPick(emoji.aliases[0])} | ||||
|         /> | ||||
|         <Emoji key={emoji.aliases[0]} emoji={emoji} search={props.search} onClick={() => props.onPick(emoji.aliases[0])} /> | ||||
|       ))} | ||||
|     </> | ||||
|   ); | ||||
|  | @ -160,12 +131,7 @@ const Emoji = (props) => { | |||
|   const matches = emojiMatches(emoji, props.search); | ||||
|   const title = `${emoji.description} (${emoji.aliases[0]})`; | ||||
|   return ( | ||||
|     <EmojiDiv | ||||
|       onClick={props.onClick} | ||||
|       title={title} | ||||
|       aria-label={title} | ||||
|       style={{ display: matches ? "" : "none" }} | ||||
|     > | ||||
|     <EmojiDiv onClick={props.onClick} title={title} aria-label={title} style={{ display: matches ? "" : "none" }}> | ||||
|       {props.emoji.emoji} | ||||
|     </EmojiDiv> | ||||
|   ); | ||||
|  |  | |||
|  | @ -22,9 +22,7 @@ class ErrorBoundaryImpl extends React.Component { | |||
|     // - https://github.com/dexie/Dexie.js/issues/312
 | ||||
|     // - https://bugzilla.mozilla.org/show_bug.cgi?id=781982
 | ||||
|     const isUnsupportedIndexedDB = | ||||
|       error?.name === "InvalidStateError" || | ||||
|       (error?.name === "DatabaseClosedError" && | ||||
|         error?.message?.indexOf("InvalidStateError") !== -1); | ||||
|       error?.name === "InvalidStateError" || (error?.name === "DatabaseClosedError" && error?.message?.indexOf("InvalidStateError") !== -1); | ||||
| 
 | ||||
|     if (isUnsupportedIndexedDB) { | ||||
|       this.handleUnsupportedIndexedDB(); | ||||
|  | @ -48,14 +46,7 @@ class ErrorBoundaryImpl extends React.Component { | |||
|     // Fetch additional info and a better stack trace
 | ||||
|     StackTrace.fromError(error).then((stack) => { | ||||
|       console.error("[ErrorBoundary] Stacktrace fetched", stack); | ||||
|       const niceStack = | ||||
|         `${error.toString()}\n` + | ||||
|         stack | ||||
|           .map( | ||||
|             (el) => | ||||
|               `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})` | ||||
|           ) | ||||
|           .join("\n"); | ||||
|       const niceStack = `${error.toString()}\n` + stack.map((el) => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); | ||||
|       this.setState({ niceStack }); | ||||
|     }); | ||||
|   } | ||||
|  | @ -96,9 +87,7 @@ class ErrorBoundaryImpl extends React.Component { | |||
|           <Trans | ||||
|             i18nKey="error_boundary_unsupported_indexeddb_description" | ||||
|             components={{ | ||||
|               githubLink: ( | ||||
|                 <Link href="https://github.com/binwiederhier/ntfy/issues/208" /> | ||||
|               ), | ||||
|               githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues/208" />, | ||||
|               discordLink: <Link href="https://discord.gg/cT7ECsZj9w" />, | ||||
|               matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org" />, | ||||
|             }} | ||||
|  | @ -117,9 +106,7 @@ class ErrorBoundaryImpl extends React.Component { | |||
|           <Trans | ||||
|             i18nKey="error_boundary_description" | ||||
|             components={{ | ||||
|               githubLink: ( | ||||
|                 <Link href="https://github.com/binwiederhier/ntfy/issues" /> | ||||
|               ), | ||||
|               githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues" />, | ||||
|               discordLink: <Link href="https://discord.gg/cT7ECsZj9w" />, | ||||
|               matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org" />, | ||||
|             }} | ||||
|  | @ -135,11 +122,7 @@ class ErrorBoundaryImpl extends React.Component { | |||
|           <pre>{this.state.niceStack}</pre> | ||||
|         ) : ( | ||||
|           <> | ||||
|             <CircularProgress | ||||
|               size="20px" | ||||
|               sx={{ verticalAlign: "text-bottom" }} | ||||
|             />{" "} | ||||
|             {t("error_boundary_gathering_info")} | ||||
|             <CircularProgress size="20px" sx={{ verticalAlign: "text-bottom" }} /> {t("error_boundary_gathering_info")} | ||||
|           </> | ||||
|         )} | ||||
|         <pre>{this.state.originalStack}</pre> | ||||
|  |  | |||
|  | @ -28,9 +28,7 @@ const Login = () => { | |||
|     const user = { username, password }; | ||||
|     try { | ||||
|       const token = await accountApi.login(user); | ||||
|       console.log( | ||||
|         `[Login] User auth for user ${user.username} successful, token is ${token}` | ||||
|       ); | ||||
|       console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`); | ||||
|       session.store(user.username, token); | ||||
|       window.location.href = routes.app; | ||||
|     } catch (e) { | ||||
|  | @ -52,12 +50,7 @@ const Login = () => { | |||
|   return ( | ||||
|     <AvatarBox> | ||||
|       <Typography sx={{ typography: "h6" }}>{t("login_title")}</Typography> | ||||
|       <Box | ||||
|         component="form" | ||||
|         onSubmit={handleSubmit} | ||||
|         noValidate | ||||
|         sx={{ mt: 1, maxWidth: 400 }} | ||||
|       > | ||||
|       <Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, maxWidth: 400 }}> | ||||
|         <TextField | ||||
|           margin="dense" | ||||
|           required | ||||
|  | @ -95,13 +88,7 @@ const Login = () => { | |||
|             ), | ||||
|           }} | ||||
|         /> | ||||
|         <Button | ||||
|           type="submit" | ||||
|           fullWidth | ||||
|           variant="contained" | ||||
|           disabled={username === "" || password === ""} | ||||
|           sx={{ mt: 2, mb: 2 }} | ||||
|         > | ||||
|         <Button type="submit" fullWidth variant="contained" disabled={username === "" || password === ""} sx={{ mt: 2, mb: 2 }}> | ||||
|           {t("login_form_button_submit")} | ||||
|         </Button> | ||||
|         {error && ( | ||||
|  |  | |||
|  | @ -29,14 +29,7 @@ const Messaging = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {subscription && ( | ||||
|         <MessageBar | ||||
|           subscription={subscription} | ||||
|           message={message} | ||||
|           onMessageChange={setMessage} | ||||
|           onOpenDialogClick={handleOpenDialogClick} | ||||
|         /> | ||||
|       )} | ||||
|       {subscription && <MessageBar subscription={subscription} message={message} onMessageChange={setMessage} onOpenDialogClick={handleOpenDialogClick} />} | ||||
|       <PublishDialog | ||||
|         key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
 | ||||
|         openMode={dialogOpenMode} | ||||
|  | @ -44,14 +37,8 @@ const Messaging = (props) => { | |||
|         topic={subscription?.topic ?? ""} | ||||
|         message={message} | ||||
|         onClose={handleDialogClose} | ||||
|         onDragEnter={() => | ||||
|           props.onDialogOpenModeChange((prev) => | ||||
|             prev ? prev : PublishDialog.OPEN_MODE_DRAG | ||||
|           ) | ||||
|         } // Only update if not already open
 | ||||
|         onResetOpenMode={() => | ||||
|           props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT) | ||||
|         } | ||||
|         onDragEnter={() => props.onDialogOpenModeChange((prev) => (prev ? prev : PublishDialog.OPEN_MODE_DRAG))} // Only update if not already open
 | ||||
|         onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
|  | @ -63,11 +50,7 @@ const MessageBar = (props) => { | |||
|   const [snackOpen, setSnackOpen] = useState(false); | ||||
|   const handleSendClick = async () => { | ||||
|     try { | ||||
|       await api.publish( | ||||
|         subscription.baseUrl, | ||||
|         subscription.topic, | ||||
|         props.message | ||||
|       ); | ||||
|       await api.publish(subscription.baseUrl, subscription.topic, props.message); | ||||
|     } catch (e) { | ||||
|       console.log(`[MessageBar] Error publishing message`, e); | ||||
|       setSnackOpen(true); | ||||
|  | @ -84,19 +67,10 @@ const MessageBar = (props) => { | |||
|         right: 0, | ||||
|         padding: 2, | ||||
|         width: { xs: "100%", sm: `calc(100% - ${Navigation.width}px)` }, | ||||
|         backgroundColor: (theme) => | ||||
|           theme.palette.mode === "light" | ||||
|             ? theme.palette.grey[100] | ||||
|             : theme.palette.grey[900], | ||||
|         backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), | ||||
|       }} | ||||
|     > | ||||
|       <IconButton | ||||
|         color="inherit" | ||||
|         size="large" | ||||
|         edge="start" | ||||
|         onClick={props.onOpenDialogClick} | ||||
|         aria-label={t("message_bar_show_dialog")} | ||||
|       > | ||||
|       <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick} aria-label={t("message_bar_show_dialog")}> | ||||
|         <KeyboardArrowUpIcon /> | ||||
|       </IconButton> | ||||
|       <TextField | ||||
|  | @ -117,22 +91,11 @@ const MessageBar = (props) => { | |||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       <IconButton | ||||
|         color="inherit" | ||||
|         size="large" | ||||
|         edge="end" | ||||
|         onClick={handleSendClick} | ||||
|         aria-label={t("message_bar_publish")} | ||||
|       > | ||||
|       <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick} aria-label={t("message_bar_publish")}> | ||||
|         <SendIcon /> | ||||
|       </IconButton> | ||||
|       <Portal> | ||||
|         <Snackbar | ||||
|           open={snackOpen} | ||||
|           autoHideDuration={3000} | ||||
|           onClose={() => setSnackOpen(false)} | ||||
|           message={t("message_bar_error_publishing")} | ||||
|         /> | ||||
|         <Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("message_bar_error_publishing")} /> | ||||
|       </Portal> | ||||
|     </Paper> | ||||
|   ); | ||||
|  |  | |||
|  | @ -12,16 +12,7 @@ import List from "@mui/material/List"; | |||
| import SettingsIcon from "@mui/icons-material/Settings"; | ||||
| import AddIcon from "@mui/icons-material/Add"; | ||||
| import SubscribeDialog from "./SubscribeDialog"; | ||||
| import { | ||||
|   Alert, | ||||
|   AlertTitle, | ||||
|   Badge, | ||||
|   CircularProgress, | ||||
|   Link, | ||||
|   ListSubheader, | ||||
|   Portal, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; | ||||
|  | @ -29,12 +20,7 @@ import routes from "./routes"; | |||
| import { ConnectionState } from "../app/Connection"; | ||||
| import { useLocation, useNavigate } from "react-router-dom"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import { | ||||
|   ChatBubble, | ||||
|   MoreVert, | ||||
|   NotificationsOffOutlined, | ||||
|   Send, | ||||
| } from "@mui/icons-material"; | ||||
| import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import notifier from "../app/Notifier"; | ||||
| import config from "../app/config"; | ||||
|  | @ -45,12 +31,7 @@ import accountApi, { Permission, Role } from "../app/AccountApi"; | |||
| import CelebrationIcon from "@mui/icons-material/Celebration"; | ||||
| import UpgradeDialog from "./UpgradeDialog"; | ||||
| import { AccountContext } from "./App"; | ||||
| import { | ||||
|   PermissionDenyAll, | ||||
|   PermissionRead, | ||||
|   PermissionReadWrite, | ||||
|   PermissionWrite, | ||||
| } from "./ReserveIcons"; | ||||
| import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import { SubscriptionPopup } from "./SubscriptionPopup"; | ||||
| 
 | ||||
|  | @ -59,11 +40,7 @@ const navWidth = 280; | |||
| const Navigation = (props) => { | ||||
|   const navigationList = <NavList {...props} />; | ||||
|   return ( | ||||
|     <Box | ||||
|       component="nav" | ||||
|       role="navigation" | ||||
|       sx={{ width: { sm: Navigation.width }, flexShrink: { sm: 0 } }} | ||||
|     > | ||||
|     <Box component="nav" role="navigation" sx={{ width: { sm: Navigation.width }, flexShrink: { sm: 0 } }}> | ||||
|       {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} | ||||
|       <Drawer | ||||
|         variant="temporary" | ||||
|  | @ -109,19 +86,14 @@ const NavList = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   const handleSubscribeSubmit = (subscription) => { | ||||
|     console.log( | ||||
|       `[Navigation] New subscription: ${subscription.id}`, | ||||
|       subscription | ||||
|     ); | ||||
|     console.log(`[Navigation] New subscription: ${subscription.id}`, subscription); | ||||
|     handleSubscribeReset(); | ||||
|     navigate(routes.forSubscription(subscription)); | ||||
|     handleRequestNotificationPermission(); | ||||
|   }; | ||||
| 
 | ||||
|   const handleRequestNotificationPermission = () => { | ||||
|     notifier.maybeRequestPermission((granted) => | ||||
|       props.onNotificationGranted(granted) | ||||
|     ); | ||||
|     notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted)); | ||||
|   }; | ||||
| 
 | ||||
|   const handleAccountClick = () => { | ||||
|  | @ -134,39 +106,19 @@ const NavList = (props) => { | |||
|   const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; | ||||
|   const showSubscriptionsList = props.subscriptions?.length > 0; | ||||
|   const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); | ||||
|   const showNotificationContextNotSupportedBox = | ||||
|     notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
 | ||||
|   const showNotificationGrantBox = | ||||
|     notifier.supported() && | ||||
|     props.subscriptions?.length > 0 && | ||||
|     !props.notificationsGranted; | ||||
|   const navListPadding = | ||||
|     showNotificationGrantBox || | ||||
|     showNotificationBrowserNotSupportedBox || | ||||
|     showNotificationContextNotSupportedBox | ||||
|       ? "0" | ||||
|       : ""; | ||||
|   const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
 | ||||
|   const showNotificationGrantBox = notifier.supported() && props.subscriptions?.length > 0 && !props.notificationsGranted; | ||||
|   const navListPadding = showNotificationGrantBox || showNotificationBrowserNotSupportedBox || showNotificationContextNotSupportedBox ? "0" : ""; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Toolbar sx={{ display: { xs: "none", sm: "block" } }} /> | ||||
|       <List component="nav" sx={{ paddingTop: navListPadding }}> | ||||
|         {showNotificationBrowserNotSupportedBox && ( | ||||
|           <NotificationBrowserNotSupportedAlert /> | ||||
|         )} | ||||
|         {showNotificationContextNotSupportedBox && ( | ||||
|           <NotificationContextNotSupportedAlert /> | ||||
|         )} | ||||
|         {showNotificationGrantBox && ( | ||||
|           <NotificationGrantAlert | ||||
|             onRequestPermissionClick={handleRequestNotificationPermission} | ||||
|           /> | ||||
|         )} | ||||
|         {showNotificationBrowserNotSupportedBox && <NotificationBrowserNotSupportedAlert />} | ||||
|         {showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert />} | ||||
|         {showNotificationGrantBox && <NotificationGrantAlert onRequestPermissionClick={handleRequestNotificationPermission} />} | ||||
|         {!showSubscriptionsList && ( | ||||
|           <ListItemButton | ||||
|             onClick={() => navigate(routes.app)} | ||||
|             selected={location.pathname === config.app_root} | ||||
|           > | ||||
|           <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}> | ||||
|             <ListItemIcon> | ||||
|               <ChatBubble /> | ||||
|             </ListItemIcon> | ||||
|  | @ -176,37 +128,25 @@ const NavList = (props) => { | |||
|         {showSubscriptionsList && ( | ||||
|           <> | ||||
|             <ListSubheader>{t("nav_topics_title")}</ListSubheader> | ||||
|             <ListItemButton | ||||
|               onClick={() => navigate(routes.app)} | ||||
|               selected={location.pathname === config.app_root} | ||||
|             > | ||||
|             <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}> | ||||
|               <ListItemIcon> | ||||
|                 <ChatBubble /> | ||||
|               </ListItemIcon> | ||||
|               <ListItemText primary={t("nav_button_all_notifications")} /> | ||||
|             </ListItemButton> | ||||
|             <SubscriptionList | ||||
|               subscriptions={props.subscriptions} | ||||
|               selectedSubscription={props.selectedSubscription} | ||||
|             /> | ||||
|             <SubscriptionList subscriptions={props.subscriptions} selectedSubscription={props.selectedSubscription} /> | ||||
|             <Divider sx={{ my: 1 }} /> | ||||
|           </> | ||||
|         )} | ||||
|         {session.exists() && ( | ||||
|           <ListItemButton | ||||
|             onClick={handleAccountClick} | ||||
|             selected={location.pathname === routes.account} | ||||
|           > | ||||
|           <ListItemButton onClick={handleAccountClick} selected={location.pathname === routes.account}> | ||||
|             <ListItemIcon> | ||||
|               <Person /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary={t("nav_button_account")} /> | ||||
|           </ListItemButton> | ||||
|         )} | ||||
|         <ListItemButton | ||||
|           onClick={() => navigate(routes.settings)} | ||||
|           selected={location.pathname === routes.settings} | ||||
|         > | ||||
|         <ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}> | ||||
|           <ListItemIcon> | ||||
|             <SettingsIcon /> | ||||
|           </ListItemIcon> | ||||
|  | @ -260,8 +200,7 @@ const UpgradeBanner = () => { | |||
|         width: `${Navigation.width - 1}px`, | ||||
|         bottom: 0, | ||||
|         mt: "auto", | ||||
|         background: | ||||
|           "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)", | ||||
|         background: "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)", | ||||
|       }} | ||||
|     > | ||||
|       <Divider /> | ||||
|  | @ -277,8 +216,7 @@ const UpgradeBanner = () => { | |||
|             style: { | ||||
|               fontWeight: 500, | ||||
|               fontSize: "1.1rem", | ||||
|               background: | ||||
|                 "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)", | ||||
|               background: "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)", | ||||
|               WebkitBackgroundClip: "text", | ||||
|               WebkitTextFillColor: "transparent", | ||||
|             }, | ||||
|  | @ -290,11 +228,7 @@ const UpgradeBanner = () => { | |||
|           }} | ||||
|         /> | ||||
|       </ListItemButton> | ||||
|       <UpgradeDialog | ||||
|         key={`upgradeDialog${dialogKey}`} | ||||
|         open={dialogOpen} | ||||
|         onCancel={() => setDialogOpen(false)} | ||||
|       /> | ||||
|       <UpgradeDialog key={`upgradeDialog${dialogKey}`} open={dialogOpen} onCancel={() => setDialogOpen(false)} /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | @ -303,9 +237,7 @@ const SubscriptionList = (props) => { | |||
|   const sortedSubscriptions = props.subscriptions | ||||
|     .filter((s) => !s.internal) | ||||
|     .sort((a, b) => { | ||||
|       return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) | ||||
|         ? -1 | ||||
|         : 1; | ||||
|       return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1; | ||||
|     }); | ||||
|   return ( | ||||
|     <> | ||||
|  | @ -313,10 +245,7 @@ const SubscriptionList = (props) => { | |||
|         <SubscriptionItem | ||||
|           key={subscription.id} | ||||
|           subscription={subscription} | ||||
|           selected={ | ||||
|             props.selectedSubscription && | ||||
|             props.selectedSubscription.id === subscription.id | ||||
|           } | ||||
|           selected={props.selectedSubscription && props.selectedSubscription.id === subscription.id} | ||||
|         /> | ||||
|       ))} | ||||
|     </> | ||||
|  | @ -331,19 +260,12 @@ const SubscriptionItem = (props) => { | |||
|   const subscription = props.subscription; | ||||
|   const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; | ||||
|   const displayName = topicDisplayName(subscription); | ||||
|   const ariaLabel = | ||||
|     subscription.state === ConnectionState.Connecting | ||||
|       ? `${displayName} (${t("nav_button_connecting")})` | ||||
|       : displayName; | ||||
|   const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; | ||||
|   const icon = | ||||
|     subscription.state === ConnectionState.Connecting ? ( | ||||
|       <CircularProgress size="24px" /> | ||||
|     ) : ( | ||||
|       <Badge | ||||
|         badgeContent={iconBadge} | ||||
|         invisible={subscription.new === 0} | ||||
|         color="primary" | ||||
|       > | ||||
|       <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"> | ||||
|         <ChatBubbleOutlineIcon /> | ||||
|       </Badge> | ||||
|     ); | ||||
|  | @ -355,12 +277,7 @@ const SubscriptionItem = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ListItemButton | ||||
|         onClick={handleClick} | ||||
|         selected={props.selected} | ||||
|         aria-label={ariaLabel} | ||||
|         aria-live="polite" | ||||
|       > | ||||
|       <ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite"> | ||||
|         <ListItemIcon>{icon}</ListItemIcon> | ||||
|         <ListItemText | ||||
|           primary={displayName} | ||||
|  | @ -371,9 +288,7 @@ const SubscriptionItem = (props) => { | |||
|         {subscription.reservation?.everyone && ( | ||||
|           <ListItemIcon edge="end" sx={{ minWidth: "26px" }}> | ||||
|             {subscription.reservation?.everyone === Permission.READ_WRITE && ( | ||||
|               <Tooltip | ||||
|                 title={t("prefs_reservations_table_everyone_read_write")} | ||||
|               > | ||||
|               <Tooltip title={t("prefs_reservations_table_everyone_read_write")}> | ||||
|                 <PermissionReadWrite size="small" /> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | @ -383,9 +298,7 @@ const SubscriptionItem = (props) => { | |||
|               </Tooltip> | ||||
|             )} | ||||
|             {subscription.reservation?.everyone === Permission.WRITE_ONLY && ( | ||||
|               <Tooltip | ||||
|                 title={t("prefs_reservations_table_everyone_write_only")} | ||||
|               > | ||||
|               <Tooltip title={t("prefs_reservations_table_everyone_write_only")}> | ||||
|                 <PermissionWrite size="small" /> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | @ -397,11 +310,7 @@ const SubscriptionItem = (props) => { | |||
|           </ListItemIcon> | ||||
|         )} | ||||
|         {subscription.mutedUntil > 0 && ( | ||||
|           <ListItemIcon | ||||
|             edge="end" | ||||
|             sx={{ minWidth: "26px" }} | ||||
|             aria-label={t("nav_button_muted")} | ||||
|           > | ||||
|           <ListItemIcon edge="end" sx={{ minWidth: "26px" }} aria-label={t("nav_button_muted")}> | ||||
|             <Tooltip title={t("nav_button_muted")}> | ||||
|               <NotificationsOffOutlined /> | ||||
|             </Tooltip> | ||||
|  | @ -421,11 +330,7 @@ const SubscriptionItem = (props) => { | |||
|         </ListItemIcon> | ||||
|       </ListItemButton> | ||||
|       <Portal> | ||||
|         <SubscriptionPopup | ||||
|           subscription={subscription} | ||||
|           anchor={menuAnchorEl} | ||||
|           onClose={() => setMenuAnchorEl(null)} | ||||
|         /> | ||||
|         <SubscriptionPopup subscription={subscription} anchor={menuAnchorEl} onClose={() => setMenuAnchorEl(null)} /> | ||||
|       </Portal> | ||||
|     </> | ||||
|   ); | ||||
|  | @ -438,12 +343,7 @@ const NotificationGrantAlert = (props) => { | |||
|       <Alert severity="warning" sx={{ paddingTop: 2 }}> | ||||
|         <AlertTitle>{t("alert_grant_title")}</AlertTitle> | ||||
|         <Typography gutterBottom>{t("alert_grant_description")}</Typography> | ||||
|         <Button | ||||
|           sx={{ float: "right" }} | ||||
|           color="inherit" | ||||
|           size="small" | ||||
|           onClick={props.onRequestPermissionClick} | ||||
|         > | ||||
|         <Button sx={{ float: "right" }} color="inherit" size="small" onClick={props.onRequestPermissionClick}> | ||||
|           {t("alert_grant_button")} | ||||
|         </Button> | ||||
|       </Alert> | ||||
|  | @ -458,9 +358,7 @@ const NotificationBrowserNotSupportedAlert = () => { | |||
|     <> | ||||
|       <Alert severity="warning" sx={{ paddingTop: 2 }}> | ||||
|         <AlertTitle>{t("alert_not_supported_title")}</AlertTitle> | ||||
|         <Typography gutterBottom> | ||||
|           {t("alert_not_supported_description")} | ||||
|         </Typography> | ||||
|         <Typography gutterBottom>{t("alert_not_supported_description")}</Typography> | ||||
|       </Alert> | ||||
|       <Divider /> | ||||
|     </> | ||||
|  | @ -477,13 +375,7 @@ const NotificationContextNotSupportedAlert = () => { | |||
|           <Trans | ||||
|             i18nKey="alert_not_supported_context_description" | ||||
|             components={{ | ||||
|               mdnLink: ( | ||||
|                 <Link | ||||
|                   href="https://developer.mozilla.org/en-US/docs/Web/API/notification" | ||||
|                   target="_blank" | ||||
|                   rel="noopener" | ||||
|                 /> | ||||
|               ), | ||||
|               mdnLink: <Link href="https://developer.mozilla.org/en-US/docs/Web/API/notification" target="_blank" rel="noopener" />, | ||||
|             }} | ||||
|           /> | ||||
|         </Typography> | ||||
|  |  | |||
|  | @ -1,16 +1,5 @@ | |||
| import Container from "@mui/material/Container"; | ||||
| import { | ||||
|   ButtonBase, | ||||
|   CardActions, | ||||
|   CardContent, | ||||
|   CircularProgress, | ||||
|   Fade, | ||||
|   Link, | ||||
|   Modal, | ||||
|   Snackbar, | ||||
|   Stack, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { ButtonBase, CardActions, CardContent, CircularProgress, Fade, Link, Modal, Snackbar, Stack, Tooltip } from "@mui/material"; | ||||
| import Card from "@mui/material/Card"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import * as React from "react"; | ||||
|  | @ -29,11 +18,7 @@ import { | |||
| import IconButton from "@mui/material/IconButton"; | ||||
| import CheckIcon from "@mui/icons-material/Check"; | ||||
| import CloseIcon from "@mui/icons-material/Close"; | ||||
| import { | ||||
|   LightboxBackdrop, | ||||
|   Paragraph, | ||||
|   VerticallyCenteredContainer, | ||||
| } from "./styles"; | ||||
| import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; | ||||
| import { useLiveQuery } from "dexie-react-hooks"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import Button from "@mui/material/Button"; | ||||
|  | @ -68,10 +53,7 @@ export const SingleSubscription = () => { | |||
| 
 | ||||
| const AllSubscriptionsList = (props) => { | ||||
|   const subscriptions = props.subscriptions; | ||||
|   const notifications = useLiveQuery( | ||||
|     () => subscriptionManager.getAllNotifications(), | ||||
|     [] | ||||
|   ); | ||||
|   const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); | ||||
|   if (notifications === null || notifications === undefined) { | ||||
|     return <Loading />; | ||||
|   } else if (subscriptions.length === 0) { | ||||
|  | @ -79,33 +61,18 @@ const AllSubscriptionsList = (props) => { | |||
|   } else if (notifications.length === 0) { | ||||
|     return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />; | ||||
|   } | ||||
|   return ( | ||||
|     <NotificationList | ||||
|       key="all" | ||||
|       notifications={notifications} | ||||
|       messageBar={false} | ||||
|     /> | ||||
|   ); | ||||
|   return <NotificationList key="all" notifications={notifications} messageBar={false} />; | ||||
| }; | ||||
| 
 | ||||
| const SingleSubscriptionList = (props) => { | ||||
|   const subscription = props.subscription; | ||||
|   const notifications = useLiveQuery( | ||||
|     () => subscriptionManager.getNotifications(subscription.id), | ||||
|     [subscription] | ||||
|   ); | ||||
|   const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); | ||||
|   if (notifications === null || notifications === undefined) { | ||||
|     return <Loading />; | ||||
|   } else if (notifications.length === 0) { | ||||
|     return <NoNotifications subscription={subscription} />; | ||||
|   } | ||||
|   return ( | ||||
|     <NotificationList | ||||
|       id={subscription.id} | ||||
|       notifications={notifications} | ||||
|       messageBar={true} | ||||
|     /> | ||||
|   ); | ||||
|   return <NotificationList id={subscription.id} notifications={notifications} messageBar={true} />; | ||||
| }; | ||||
| 
 | ||||
| const NotificationList = (props) => { | ||||
|  | @ -146,18 +113,9 @@ const NotificationList = (props) => { | |||
|       > | ||||
|         <Stack spacing={3}> | ||||
|           {notifications.slice(0, count).map((notification) => ( | ||||
|             <NotificationItem | ||||
|               key={notification.id} | ||||
|               notification={notification} | ||||
|               onShowSnack={() => setSnackOpen(true)} | ||||
|             /> | ||||
|             <NotificationItem key={notification.id} notification={notification} onShowSnack={() => setSnackOpen(true)} /> | ||||
|           ))} | ||||
|           <Snackbar | ||||
|             open={snackOpen} | ||||
|             autoHideDuration={3000} | ||||
|             onClose={() => setSnackOpen(false)} | ||||
|             message={t("notifications_copied_to_clipboard")} | ||||
|           /> | ||||
|           <Snackbar open={snackOpen} autoHideDuration={3000} onClose={() => setSnackOpen(false)} message={t("notifications_copied_to_clipboard")} /> | ||||
|         </Stack> | ||||
|       </Container> | ||||
|     </InfiniteScroll> | ||||
|  | @ -176,45 +134,29 @@ const NotificationItem = (props) => { | |||
|     await subscriptionManager.deleteNotification(notification.id); | ||||
|   }; | ||||
|   const handleMarkRead = async () => { | ||||
|     console.log( | ||||
|       `[Notifications] Marking notification ${notification.id} as read` | ||||
|     ); | ||||
|     console.log(`[Notifications] Marking notification ${notification.id} as read`); | ||||
|     await subscriptionManager.markNotificationRead(notification.id); | ||||
|   }; | ||||
|   const handleCopy = (s) => { | ||||
|     navigator.clipboard.writeText(s); | ||||
|     props.onShowSnack(); | ||||
|   }; | ||||
|   const expired = | ||||
|     attachment && attachment.expires && attachment.expires < Date.now() / 1000; | ||||
|   const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000; | ||||
|   const hasAttachmentActions = attachment && !expired; | ||||
|   const hasClickAction = notification.click; | ||||
|   const hasUserActions = | ||||
|     notification.actions && notification.actions.length > 0; | ||||
|   const hasUserActions = notification.actions && notification.actions.length > 0; | ||||
|   const showActions = hasAttachmentActions || hasClickAction || hasUserActions; | ||||
|   return ( | ||||
|     <Card | ||||
|       sx={{ minWidth: 275, padding: 1 }} | ||||
|       role="listitem" | ||||
|       aria-label={t("notifications_list_item")} | ||||
|     > | ||||
|     <Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}> | ||||
|       <CardContent> | ||||
|         <Tooltip title={t("notifications_delete")} enterDelay={500}> | ||||
|           <IconButton | ||||
|             onClick={handleDelete} | ||||
|             sx={{ float: "right", marginRight: -1, marginTop: -1 }} | ||||
|             aria-label={t("notifications_delete")} | ||||
|           > | ||||
|           <IconButton onClick={handleDelete} sx={{ float: "right", marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}> | ||||
|             <CloseIcon /> | ||||
|           </IconButton> | ||||
|         </Tooltip> | ||||
|         {notification.new === 1 && ( | ||||
|           <Tooltip title={t("notifications_mark_read")} enterDelay={500}> | ||||
|             <IconButton | ||||
|               onClick={handleMarkRead} | ||||
|               sx={{ float: "right", marginRight: -0.5, marginTop: -1 }} | ||||
|               aria-label={t("notifications_mark_read")} | ||||
|             > | ||||
|             <IconButton onClick={handleMarkRead} sx={{ float: "right", marginRight: -0.5, marginTop: -1 }} aria-label={t("notifications_mark_read")}> | ||||
|               <CheckIcon /> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|  | @ -247,9 +189,7 @@ const NotificationItem = (props) => { | |||
|           </Typography> | ||||
|         )} | ||||
|         <Typography variant="body1" sx={{ whiteSpace: "pre-line" }}> | ||||
|           {autolink( | ||||
|             maybeAppendActionErrors(formatMessage(notification), notification) | ||||
|           )} | ||||
|           {autolink(maybeAppendActionErrors(formatMessage(notification), notification))} | ||||
|         </Typography> | ||||
|         {attachment && <Attachment attachment={attachment} />} | ||||
|         {tags && ( | ||||
|  | @ -263,36 +203,28 @@ const NotificationItem = (props) => { | |||
|           {hasAttachmentActions && ( | ||||
|             <> | ||||
|               <Tooltip title={t("notifications_attachment_copy_url_title")}> | ||||
|                 <Button onClick={() => handleCopy(attachment.url)}> | ||||
|                   {t("notifications_attachment_copy_url_button")} | ||||
|                 </Button> | ||||
|                 <Button onClick={() => handleCopy(attachment.url)}>{t("notifications_attachment_copy_url_button")}</Button> | ||||
|               </Tooltip> | ||||
|               <Tooltip | ||||
|                 title={t("notifications_attachment_open_title", { | ||||
|                   url: attachment.url, | ||||
|                 })} | ||||
|               > | ||||
|                 <Button onClick={() => openUrl(attachment.url)}> | ||||
|                   {t("notifications_attachment_open_button")} | ||||
|                 </Button> | ||||
|                 <Button onClick={() => openUrl(attachment.url)}>{t("notifications_attachment_open_button")}</Button> | ||||
|               </Tooltip> | ||||
|             </> | ||||
|           )} | ||||
|           {hasClickAction && ( | ||||
|             <> | ||||
|               <Tooltip title={t("notifications_click_copy_url_title")}> | ||||
|                 <Button onClick={() => handleCopy(notification.click)}> | ||||
|                   {t("notifications_click_copy_url_button")} | ||||
|                 </Button> | ||||
|                 <Button onClick={() => handleCopy(notification.click)}>{t("notifications_click_copy_url_button")}</Button> | ||||
|               </Tooltip> | ||||
|               <Tooltip | ||||
|                 title={t("notifications_actions_open_url_title", { | ||||
|                   url: notification.click, | ||||
|                 })} | ||||
|               > | ||||
|                 <Button onClick={() => openUrl(notification.click)}> | ||||
|                   {t("notifications_click_open_button")} | ||||
|                 </Button> | ||||
|                 <Button onClick={() => openUrl(notification.click)}>{t("notifications_click_open_button")}</Button> | ||||
|               </Tooltip> | ||||
|             </> | ||||
|           )} | ||||
|  | @ -311,18 +243,10 @@ const NotificationItem = (props) => { | |||
|  * [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
 | ||||
|  */ | ||||
| const autolink = (s) => { | ||||
|   const parts = s.split( | ||||
|     /(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi | ||||
|   ); | ||||
|   const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi); | ||||
|   for (let i = 1; i < parts.length; i += 2) { | ||||
|     parts[i] = ( | ||||
|       <Link | ||||
|         key={i} | ||||
|         href={parts[i]} | ||||
|         underline="hover" | ||||
|         target="_blank" | ||||
|         rel="noreferrer,noopener" | ||||
|       > | ||||
|       <Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener"> | ||||
|         {shortUrl(parts[i])} | ||||
|       </Link> | ||||
|     ); | ||||
|  | @ -342,8 +266,7 @@ const Attachment = (props) => { | |||
|   const attachment = props.attachment; | ||||
|   const expired = attachment.expires && attachment.expires < Date.now() / 1000; | ||||
|   const expires = attachment.expires && attachment.expires > Date.now() / 1000; | ||||
|   const displayableImage = | ||||
|     !expired && attachment.type && attachment.type.startsWith("image/"); | ||||
|   const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); | ||||
| 
 | ||||
|   // Unexpired image
 | ||||
|   if (displayableImage) { | ||||
|  | @ -386,10 +309,7 @@ const Attachment = (props) => { | |||
|         }} | ||||
|       > | ||||
|         <AttachmentIcon type={attachment.type} /> | ||||
|         <Typography | ||||
|           variant="body2" | ||||
|           sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }} | ||||
|         > | ||||
|         <Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}> | ||||
|           <b>{attachment.name}</b> | ||||
|           {maybeInfoText} | ||||
|         </Typography> | ||||
|  | @ -420,10 +340,7 @@ const Attachment = (props) => { | |||
|         }} | ||||
|       > | ||||
|         <AttachmentIcon type={attachment.type} /> | ||||
|         <Typography | ||||
|           variant="body2" | ||||
|           sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }} | ||||
|         > | ||||
|         <Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}> | ||||
|           <b>{attachment.name}</b> | ||||
|           {maybeInfoText} | ||||
|         </Typography> | ||||
|  | @ -453,11 +370,7 @@ const Image = (props) => { | |||
|           cursor: "pointer", | ||||
|         }} | ||||
|       /> | ||||
|       <Modal | ||||
|         open={open} | ||||
|         onClose={() => setOpen(false)} | ||||
|         BackdropComponent={LightboxBackdrop} | ||||
|       > | ||||
|       <Modal open={open} onClose={() => setOpen(false)} BackdropComponent={LightboxBackdrop}> | ||||
|         <Fade in={open}> | ||||
|           <Box | ||||
|             component="img" | ||||
|  | @ -484,11 +397,7 @@ const UserActions = (props) => { | |||
|   return ( | ||||
|     <> | ||||
|       {props.notification.actions.map((action) => ( | ||||
|         <UserAction | ||||
|           key={action.id} | ||||
|           notification={props.notification} | ||||
|           action={action} | ||||
|         /> | ||||
|         <UserAction key={action.id} notification={props.notification} action={action} /> | ||||
|       ))} | ||||
|     </> | ||||
|   ); | ||||
|  | @ -502,10 +411,7 @@ const UserAction = (props) => { | |||
|     return ( | ||||
|       <Tooltip title={t("notifications_actions_not_supported")}> | ||||
|         <span> | ||||
|           <Button | ||||
|             disabled | ||||
|             aria-label={t("notifications_actions_not_supported")} | ||||
|           > | ||||
|           <Button disabled aria-label={t("notifications_actions_not_supported")}> | ||||
|             {action.label} | ||||
|           </Button> | ||||
|         </span> | ||||
|  | @ -513,9 +419,7 @@ const UserAction = (props) => { | |||
|     ); | ||||
|   } else if (action.action === "view") { | ||||
|     return ( | ||||
|       <Tooltip | ||||
|         title={t("notifications_actions_open_url_title", { url: action.url })} | ||||
|       > | ||||
|       <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}> | ||||
|         <Button | ||||
|           onClick={() => openUrl(action.url)} | ||||
|           aria-label={t("notifications_actions_open_url_title", { | ||||
|  | @ -528,8 +432,7 @@ const UserAction = (props) => { | |||
|     ); | ||||
|   } else if (action.action === "http") { | ||||
|     const method = action.method ?? "POST"; | ||||
|     const label = | ||||
|       action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); | ||||
|     const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); | ||||
|     return ( | ||||
|       <Tooltip | ||||
|         title={t("notifications_actions_http_request_title", { | ||||
|  | @ -568,21 +471,11 @@ const performHttpAction = async (notification, action) => { | |||
|     if (success) { | ||||
|       updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); | ||||
|     } else { | ||||
|       updateActionStatus( | ||||
|         notification, | ||||
|         action, | ||||
|         ACTION_PROGRESS_FAILED, | ||||
|         `${action.label}: Unexpected response HTTP ${response.status}` | ||||
|       ); | ||||
|       updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     console.log(`[Notifications] HTTP action failed`, e); | ||||
|     updateActionStatus( | ||||
|       notification, | ||||
|       action, | ||||
|       ACTION_PROGRESS_FAILED, | ||||
|       `${action.label}: ${e} Check developer console for details.` | ||||
|     ); | ||||
|     updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
|  | @ -608,19 +501,11 @@ const ACTION_LABEL_SUFFIX = { | |||
| 
 | ||||
| const NoNotifications = (props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const shortUrl = topicShortUrl( | ||||
|     props.subscription.baseUrl, | ||||
|     props.subscription.topic | ||||
|   ); | ||||
|   const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic); | ||||
|   return ( | ||||
|     <VerticallyCenteredContainer maxWidth="xs"> | ||||
|       <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> | ||||
|         <img | ||||
|           src={logoOutline} | ||||
|           height="64" | ||||
|           width="64" | ||||
|           alt={t("action_bar_logo_alt")} | ||||
|         /> | ||||
|         <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} /> | ||||
|         <br /> | ||||
|         {t("notifications_none_for_topic_title")} | ||||
|       </Typography> | ||||
|  | @ -643,12 +528,7 @@ const NoNotificationsWithoutSubscription = (props) => { | |||
|   return ( | ||||
|     <VerticallyCenteredContainer maxWidth="xs"> | ||||
|       <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> | ||||
|         <img | ||||
|           src={logoOutline} | ||||
|           height="64" | ||||
|           width="64" | ||||
|           alt={t("action_bar_logo_alt")} | ||||
|         /> | ||||
|         <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} /> | ||||
|         <br /> | ||||
|         {t("notifications_none_for_any_title")} | ||||
|       </Typography> | ||||
|  | @ -669,12 +549,7 @@ const NoSubscriptions = () => { | |||
|   return ( | ||||
|     <VerticallyCenteredContainer maxWidth="xs"> | ||||
|       <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> | ||||
|         <img | ||||
|           src={logoOutline} | ||||
|           height="64" | ||||
|           width="64" | ||||
|           alt={t("action_bar_logo_alt")} | ||||
|         /> | ||||
|         <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} /> | ||||
|         <br /> | ||||
|         {t("notifications_no_subscriptions_title")} | ||||
|       </Typography> | ||||
|  | @ -695,12 +570,8 @@ const ForMoreDetails = () => { | |||
|     <Trans | ||||
|       i18nKey="notifications_more_details" | ||||
|       components={{ | ||||
|         websiteLink: ( | ||||
|           <Link href="https://ntfy.sh" target="_blank" rel="noopener" /> | ||||
|         ), | ||||
|         docsLink: ( | ||||
|           <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" /> | ||||
|         ), | ||||
|         websiteLink: <Link href="https://ntfy.sh" target="_blank" rel="noopener" />, | ||||
|         docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />, | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
|  | @ -710,12 +581,7 @@ const Loading = () => { | |||
|   const { t } = useTranslation(); | ||||
|   return ( | ||||
|     <VerticallyCenteredContainer> | ||||
|       <Typography | ||||
|         variant="h5" | ||||
|         color="text.secondary" | ||||
|         align="center" | ||||
|         sx={{ paddingBottom: 1 }} | ||||
|       > | ||||
|       <Typography variant="h5" color="text.secondary" align="center" sx={{ paddingBottom: 1 }}> | ||||
|         <CircularProgress disableShrink sx={{ marginBottom: 1 }} /> | ||||
|         <br /> | ||||
|         {t("notifications_loading")} | ||||
|  |  | |||
|  | @ -44,17 +44,8 @@ import { Pref, PrefGroup } from "./Pref"; | |||
| import { Info } from "@mui/icons-material"; | ||||
| import { AccountContext } from "./App"; | ||||
| import { useOutletContext } from "react-router-dom"; | ||||
| import { | ||||
|   PermissionDenyAll, | ||||
|   PermissionRead, | ||||
|   PermissionReadWrite, | ||||
|   PermissionWrite, | ||||
| } from "./ReserveIcons"; | ||||
| import { | ||||
|   ReserveAddDialog, | ||||
|   ReserveDeleteDialog, | ||||
|   ReserveEditDialog, | ||||
| } from "./ReserveDialogs"; | ||||
| import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | ||||
| import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | ||||
| import { UnauthorizedError } from "../app/errors"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import { subscribeTopic } from "./SubscribeDialog"; | ||||
|  | @ -112,21 +103,11 @@ const Sound = () => { | |||
|     }); | ||||
|   } | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("prefs_notifications_sound_title")} | ||||
|       description={description} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}> | ||||
|       <div style={{ display: "flex", width: "100%" }}> | ||||
|         <FormControl fullWidth variant="standard" sx={{ margin: 1 }}> | ||||
|           <Select | ||||
|             value={sound} | ||||
|             onChange={handleChange} | ||||
|             aria-labelledby={labelId} | ||||
|           > | ||||
|             <MenuItem value={"none"}> | ||||
|               {t("prefs_notifications_sound_no_sound")} | ||||
|             </MenuItem> | ||||
|           <Select value={sound} onChange={handleChange} aria-labelledby={labelId}> | ||||
|             <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} | ||||
|  | @ -134,11 +115,7 @@ const Sound = () => { | |||
|             ))} | ||||
|           </Select> | ||||
|         </FormControl> | ||||
|         <IconButton | ||||
|           onClick={() => playSound(sound)} | ||||
|           disabled={sound === "none"} | ||||
|           aria-label={t("prefs_notifications_sound_play")} | ||||
|         > | ||||
|         <IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}> | ||||
|           <PlayArrowIcon /> | ||||
|         </IconButton> | ||||
|       </div> | ||||
|  | @ -174,41 +151,20 @@ const MinPriority = () => { | |||
|   } 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], | ||||
|       } | ||||
|     ); | ||||
|     description = t("prefs_notifications_min_priority_description_x_or_higher", { | ||||
|       number: minPriority, | ||||
|       name: priorities[minPriority], | ||||
|     }); | ||||
|   } | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("prefs_notifications_min_priority_title")} | ||||
|       description={description} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}> | ||||
|       <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|         <Select | ||||
|           value={minPriority} | ||||
|           onChange={handleChange} | ||||
|           aria-labelledby={labelId} | ||||
|         > | ||||
|           <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 value={minPriority} onChange={handleChange} aria-labelledby={labelId}> | ||||
|           <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> | ||||
|  | @ -246,32 +202,14 @@ const DeleteAfter = () => { | |||
|     } | ||||
|   })(); | ||||
|   return ( | ||||
|     <Pref | ||||
|       labelId={labelId} | ||||
|       title={t("prefs_notifications_delete_after_title")} | ||||
|       description={description} | ||||
|     > | ||||
|     <Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}> | ||||
|       <FormControl fullWidth variant="standard" sx={{ m: 1 }}> | ||||
|         <Select | ||||
|           value={deleteAfter} | ||||
|           onChange={handleChange} | ||||
|           aria-labelledby={labelId} | ||||
|         > | ||||
|           <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 value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}> | ||||
|           <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> | ||||
|  | @ -294,9 +232,7 @@ const Users = () => { | |||
|     setDialogOpen(false); | ||||
|     try { | ||||
|       await userManager.save(user); | ||||
|       console.debug( | ||||
|         `[Preferences] User ${user.username} for ${user.baseUrl} added` | ||||
|       ); | ||||
|       console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`); | ||||
|     } catch (e) { | ||||
|       console.log(`[Preferences] Error adding user.`, e); | ||||
|     } | ||||
|  | @ -309,22 +245,13 @@ const Users = () => { | |||
|         </Typography> | ||||
|         <Paragraph> | ||||
|           {t("prefs_users_description")} | ||||
|           {session.exists() && ( | ||||
|             <>{" " + t("prefs_users_description_no_sync")}</> | ||||
|           )} | ||||
|           {session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>} | ||||
|         </Paragraph> | ||||
|         {users?.length > 0 && <UserTable users={users} />} | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         <Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button> | ||||
|         <UserDialog | ||||
|           key={`userAddDialog${dialogKey}`} | ||||
|           open={dialogOpen} | ||||
|           user={null} | ||||
|           users={users} | ||||
|           onCancel={handleDialogCancel} | ||||
|           onSubmit={handleDialogSubmit} | ||||
|         /> | ||||
|         <UserDialog key={`userAddDialog${dialogKey}`} open={dialogOpen} user={null} users={users} onCancel={handleDialogCancel} onSubmit={handleDialogSubmit} /> | ||||
|       </CardActions> | ||||
|     </Card> | ||||
|   ); | ||||
|  | @ -350,9 +277,7 @@ const UserTable = (props) => { | |||
|     setDialogOpen(false); | ||||
|     try { | ||||
|       await userManager.save(user); | ||||
|       console.debug( | ||||
|         `[Preferences] User ${user.username} for ${user.baseUrl} updated` | ||||
|       ); | ||||
|       console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`); | ||||
|     } catch (e) { | ||||
|       console.log(`[Preferences] Error updating user.`, e); | ||||
|     } | ||||
|  | @ -361,9 +286,7 @@ const UserTable = (props) => { | |||
|   const handleDeleteClick = async (user) => { | ||||
|     try { | ||||
|       await userManager.delete(user.baseUrl); | ||||
|       console.debug( | ||||
|         `[Preferences] User ${user.username} for ${user.baseUrl} deleted` | ||||
|       ); | ||||
|       console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`); | ||||
|     } catch (e) { | ||||
|       console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); | ||||
|     } | ||||
|  | @ -373,43 +296,25 @@ const UserTable = (props) => { | |||
|     <Table size="small" aria-label={t("prefs_users_table")}> | ||||
|       <TableHead> | ||||
|         <TableRow> | ||||
|           <TableCell sx={{ paddingLeft: 0 }}> | ||||
|             {t("prefs_users_table_user_header")} | ||||
|           </TableCell> | ||||
|           <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")} | ||||
|             > | ||||
|           <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 aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell> | ||||
|             <TableCell align="right" sx={{ whiteSpace: "nowrap" }}> | ||||
|               {(!session.exists() || user.baseUrl !== config.base_url) && ( | ||||
|                 <> | ||||
|                   <IconButton | ||||
|                     onClick={() => handleEditClick(user)} | ||||
|                     aria-label={t("prefs_users_edit_button")} | ||||
|                   > | ||||
|                   <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> | ||||
|                     <EditIcon /> | ||||
|                   </IconButton> | ||||
|                   <IconButton | ||||
|                     onClick={() => handleDeleteClick(user)} | ||||
|                     aria-label={t("prefs_users_delete_button")} | ||||
|                   > | ||||
|                   <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> | ||||
|                     <CloseIcon /> | ||||
|                   </IconButton> | ||||
|                 </> | ||||
|  | @ -454,15 +359,8 @@ const UserDialog = (props) => { | |||
|       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 baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl); | ||||
|     return baseUrlValid && !baseUrlExists && username.length > 0 && password.length > 0; | ||||
|   })(); | ||||
|   const handleSubmit = async () => { | ||||
|     props.onSubmit({ | ||||
|  | @ -480,11 +378,7 @@ const UserDialog = (props) => { | |||
|   }, [editMode, props.user]); | ||||
|   return ( | ||||
|     <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||
|       <DialogTitle> | ||||
|         {editMode | ||||
|           ? t("prefs_users_dialog_title_edit") | ||||
|           : t("prefs_users_dialog_title_add")} | ||||
|       </DialogTitle> | ||||
|       <DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         {!editMode && ( | ||||
|           <TextField | ||||
|  | @ -555,26 +449,7 @@ const Language = () => { | |||
| 
 | ||||
|   // Country flags are displayed using emoji. Emoji rendering is handled by platform fonts.
 | ||||
|   // Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows.
 | ||||
|   const randomFlags = shuffle([ | ||||
|     "🇬🇧", | ||||
|     "🇺🇸", | ||||
|     "🇪🇸", | ||||
|     "🇫🇷", | ||||
|     "🇧🇬", | ||||
|     "🇨🇿", | ||||
|     "🇩🇪", | ||||
|     "🇵🇱", | ||||
|     "🇺🇦", | ||||
|     "🇨🇳", | ||||
|     "🇮🇹", | ||||
|     "🇭🇺", | ||||
|     "🇧🇷", | ||||
|     "🇳🇱", | ||||
|     "🇮🇩", | ||||
|     "🇯🇵", | ||||
|     "🇷🇺", | ||||
|     "🇹🇷", | ||||
|   ]).slice(0, 3); | ||||
|   const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); | ||||
|   const showFlags = !navigator.userAgent.includes("Windows"); | ||||
|   let title = t("prefs_appearance_language_title"); | ||||
|   if (showFlags) { | ||||
|  | @ -635,8 +510,7 @@ const Reservations = () => { | |||
|     return <></>; | ||||
|   } | ||||
|   const reservations = account.reservations || []; | ||||
|   const limitReached = | ||||
|     account.role === Role.USER && account.stats.reservations_remaining === 0; | ||||
|   const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0; | ||||
| 
 | ||||
|   const handleAddClick = () => { | ||||
|     setDialogKey((prev) => prev + 1); | ||||
|  | @ -650,23 +524,14 @@ const Reservations = () => { | |||
|           {t("prefs_reservations_title")} | ||||
|         </Typography> | ||||
|         <Paragraph>{t("prefs_reservations_description")}</Paragraph> | ||||
|         {reservations.length > 0 && ( | ||||
|           <ReservationsTable reservations={reservations} /> | ||||
|         )} | ||||
|         {limitReached && ( | ||||
|           <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert> | ||||
|         )} | ||||
|         {reservations.length > 0 && <ReservationsTable reservations={reservations} />} | ||||
|         {limitReached && <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>} | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         <Button onClick={handleAddClick} disabled={limitReached}> | ||||
|           {t("prefs_reservations_add_button")} | ||||
|         </Button> | ||||
|         <ReserveAddDialog | ||||
|           key={`reservationAddDialog${dialogKey}`} | ||||
|           open={dialogOpen} | ||||
|           reservations={reservations} | ||||
|           onClose={() => setDialogOpen(false)} | ||||
|         /> | ||||
|         <ReserveAddDialog key={`reservationAddDialog${dialogKey}`} open={dialogOpen} reservations={reservations} onClose={() => setDialogOpen(false)} /> | ||||
|       </CardActions> | ||||
|     </Card> | ||||
|   ); | ||||
|  | @ -680,14 +545,7 @@ const ReservationsTable = (props) => { | |||
|   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | ||||
|   const { subscriptions } = useOutletContext(); | ||||
|   const localSubscriptions = | ||||
|     subscriptions?.length > 0 | ||||
|       ? Object.assign( | ||||
|           {}, | ||||
|           ...subscriptions | ||||
|             .filter((s) => s.baseUrl === config.base_url) | ||||
|             .map((s) => ({ [s.topic]: s })) | ||||
|         ) | ||||
|       : {}; | ||||
|     subscriptions?.length > 0 ? Object.assign({}, ...subscriptions.filter((s) => s.baseUrl === config.base_url).map((s) => ({ [s.topic]: s }))) : {}; | ||||
| 
 | ||||
|   const handleEditClick = (reservation) => { | ||||
|     setDialogKey((prev) => prev + 1); | ||||
|  | @ -709,70 +567,46 @@ const ReservationsTable = (props) => { | |||
|     <Table size="small" aria-label={t("prefs_reservations_table")}> | ||||
|       <TableHead> | ||||
|         <TableRow> | ||||
|           <TableCell sx={{ paddingLeft: 0 }}> | ||||
|             {t("prefs_reservations_table_topic_header")} | ||||
|           </TableCell> | ||||
|           <TableCell sx={{ paddingLeft: 0 }}>{t("prefs_reservations_table_topic_header")}</TableCell> | ||||
|           <TableCell>{t("prefs_reservations_table_access_header")}</TableCell> | ||||
|           <TableCell /> | ||||
|         </TableRow> | ||||
|       </TableHead> | ||||
|       <TableBody> | ||||
|         {props.reservations.map((reservation) => ( | ||||
|           <TableRow | ||||
|             key={reservation.topic} | ||||
|             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")} | ||||
|             > | ||||
|           <TableRow key={reservation.topic} 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> | ||||
|             <TableCell aria-label={t("prefs_reservations_table_access_header")}> | ||||
|               {reservation.everyone === Permission.READ_WRITE && ( | ||||
|                 <> | ||||
|                   <PermissionReadWrite | ||||
|                     size="small" | ||||
|                     sx={{ verticalAlign: "bottom", mr: 1.5 }} | ||||
|                   /> | ||||
|                   <PermissionReadWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} /> | ||||
|                   {t("prefs_reservations_table_everyone_read_write")} | ||||
|                 </> | ||||
|               )} | ||||
|               {reservation.everyone === Permission.READ_ONLY && ( | ||||
|                 <> | ||||
|                   <PermissionRead | ||||
|                     size="small" | ||||
|                     sx={{ verticalAlign: "bottom", mr: 1.5 }} | ||||
|                   /> | ||||
|                   <PermissionRead size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} /> | ||||
|                   {t("prefs_reservations_table_everyone_read_only")} | ||||
|                 </> | ||||
|               )} | ||||
|               {reservation.everyone === Permission.WRITE_ONLY && ( | ||||
|                 <> | ||||
|                   <PermissionWrite | ||||
|                     size="small" | ||||
|                     sx={{ verticalAlign: "bottom", mr: 1.5 }} | ||||
|                   /> | ||||
|                   <PermissionWrite size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} /> | ||||
|                   {t("prefs_reservations_table_everyone_write_only")} | ||||
|                 </> | ||||
|               )} | ||||
|               {reservation.everyone === Permission.DENY_ALL && ( | ||||
|                 <> | ||||
|                   <PermissionDenyAll | ||||
|                     size="small" | ||||
|                     sx={{ verticalAlign: "bottom", mr: 1.5 }} | ||||
|                   /> | ||||
|                   <PermissionDenyAll size="small" sx={{ verticalAlign: "bottom", mr: 1.5 }} /> | ||||
|                   {t("prefs_reservations_table_everyone_deny_all")} | ||||
|                 </> | ||||
|               )} | ||||
|             </TableCell> | ||||
|             <TableCell align="right" sx={{ whiteSpace: "nowrap" }}> | ||||
|               {!localSubscriptions[reservation.topic] && ( | ||||
|                 <Tooltip | ||||
|                   title={t("prefs_reservations_table_click_to_subscribe")} | ||||
|                 > | ||||
|                 <Tooltip title={t("prefs_reservations_table_click_to_subscribe")}> | ||||
|                   <Chip | ||||
|                     icon={<Info />} | ||||
|                     onClick={() => handleSubscribeClick(reservation)} | ||||
|  | @ -782,16 +616,10 @@ const ReservationsTable = (props) => { | |||
|                   /> | ||||
|                 </Tooltip> | ||||
|               )} | ||||
|               <IconButton | ||||
|                 onClick={() => handleEditClick(reservation)} | ||||
|                 aria-label={t("prefs_reservations_edit_button")} | ||||
|               > | ||||
|               <IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}> | ||||
|                 <EditIcon /> | ||||
|               </IconButton> | ||||
|               <IconButton | ||||
|                 onClick={() => handleDeleteClick(reservation)} | ||||
|                 aria-label={t("prefs_reservations_delete_button")} | ||||
|               > | ||||
|               <IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}> | ||||
|                 <CloseIcon /> | ||||
|               </IconButton> | ||||
|             </TableCell> | ||||
|  |  | |||
|  | @ -1,17 +1,7 @@ | |||
| import * as React from "react"; | ||||
| import { useContext, useEffect, useRef, useState } from "react"; | ||||
| import theme from "./theme"; | ||||
| import { | ||||
|   Checkbox, | ||||
|   Chip, | ||||
|   FormControl, | ||||
|   FormControlLabel, | ||||
|   InputLabel, | ||||
|   Link, | ||||
|   Select, | ||||
|   Tooltip, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, Tooltip, useMediaQuery } from "@mui/material"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import priority1 from "../img/priority-1.svg"; | ||||
| import priority2 from "../img/priority-2.svg"; | ||||
|  | @ -27,14 +17,7 @@ import IconButton from "@mui/material/IconButton"; | |||
| import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; | ||||
| import { Close } from "@mui/icons-material"; | ||||
| import MenuItem from "@mui/material/MenuItem"; | ||||
| import { | ||||
|   formatBytes, | ||||
|   maybeWithAuth, | ||||
|   topicShortUrl, | ||||
|   topicUrl, | ||||
|   validTopic, | ||||
|   validUrl, | ||||
| } from "../app/utils"; | ||||
| import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import AttachmentIcon from "./AttachmentIcon"; | ||||
| import DialogFooter from "./DialogFooter"; | ||||
|  | @ -152,10 +135,7 @@ const PublishDialog = (props) => { | |||
|       url.searchParams.append("delay", delay.trim()); | ||||
|     } | ||||
|     if (attachFile && message.trim()) { | ||||
|       url.searchParams.append( | ||||
|         "message", | ||||
|         message.replaceAll("\n", "\\n").trim() | ||||
|       ); | ||||
|       url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); | ||||
|     } | ||||
|     const body = attachFile ? attachFile : message; | ||||
|     try { | ||||
|  | @ -184,11 +164,7 @@ const PublishDialog = (props) => { | |||
|         setActiveRequest(null); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       setStatus( | ||||
|         <Typography sx={{ color: "error.main", maxWidth: "400px" }}> | ||||
|           {e} | ||||
|         </Typography> | ||||
|       ); | ||||
|       setStatus(<Typography sx={{ color: "error.main", maxWidth: "400px" }}>{e}</Typography>); | ||||
|       setActiveRequest(null); | ||||
|     } | ||||
|   }; | ||||
|  | @ -198,8 +174,7 @@ const PublishDialog = (props) => { | |||
|       const account = await accountApi.get(); | ||||
|       const fileSizeLimit = account.limits.attachment_file_size ?? 0; | ||||
|       const remainingBytes = account.stats.attachment_total_size_remaining; | ||||
|       const fileSizeLimitReached = | ||||
|         fileSizeLimit > 0 && file.size > fileSizeLimit; | ||||
|       const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; | ||||
|       const quotaReached = remainingBytes > 0 && file.size > remainingBytes; | ||||
|       if (fileSizeLimitReached && quotaReached) { | ||||
|         return setAttachFileError( | ||||
|  | @ -282,18 +257,8 @@ const PublishDialog = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {dropZone && ( | ||||
|         <DropArea | ||||
|           onDrop={handleAttachFileDrop} | ||||
|           onDragLeave={handleAttachFileDragLeave} | ||||
|         /> | ||||
|       )} | ||||
|       <Dialog | ||||
|         maxWidth="md" | ||||
|         open={open} | ||||
|         onClose={props.onCancel} | ||||
|         fullScreen={fullScreen} | ||||
|       > | ||||
|       {dropZone && <DropArea onDrop={handleAttachFileDrop} onDragLeave={handleAttachFileDragLeave} />} | ||||
|       <Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||
|         <DialogTitle> | ||||
|           {baseUrl && topic | ||||
|             ? t("publish_dialog_title_topic", { | ||||
|  | @ -377,16 +342,8 @@ const PublishDialog = (props) => { | |||
|             }} | ||||
|           /> | ||||
|           <div style={{ display: "flex" }}> | ||||
|             <EmojiPicker | ||||
|               anchorEl={emojiPickerAnchorEl} | ||||
|               onEmojiPick={handleEmojiPick} | ||||
|               onClose={handleEmojiClose} | ||||
|             /> | ||||
|             <DialogIconButton | ||||
|               disabled={disabled} | ||||
|               onClick={handleEmojiClick} | ||||
|               aria-label={t("publish_dialog_emoji_picker_show")} | ||||
|             > | ||||
|             <EmojiPicker anchorEl={emojiPickerAnchorEl} onEmojiPick={handleEmojiPick} onClose={handleEmojiClose} /> | ||||
|             <DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}> | ||||
|               <InsertEmoticonIcon /> | ||||
|             </DialogIconButton> | ||||
|             <TextField | ||||
|  | @ -403,11 +360,7 @@ const PublishDialog = (props) => { | |||
|                 "aria-label": t("publish_dialog_tags_label"), | ||||
|               }} | ||||
|             /> | ||||
|             <FormControl | ||||
|               variant="standard" | ||||
|               margin="dense" | ||||
|               sx={{ minWidth: 170, maxWidth: 300, flexGrow: 1 }} | ||||
|             > | ||||
|             <FormControl variant="standard" margin="dense" sx={{ minWidth: 170, maxWidth: 300, flexGrow: 1 }}> | ||||
|               <InputLabel /> | ||||
|               <Select | ||||
|                 label={t("publish_dialog_priority_label")} | ||||
|  | @ -514,11 +467,7 @@ const PublishDialog = (props) => { | |||
|                   }} | ||||
|                 > | ||||
|                   {account?.phone_numbers?.map((phoneNumber, i) => ( | ||||
|                     <MenuItem | ||||
|                       key={`phoneNumberMenuItem${i}`} | ||||
|                       value={phoneNumber} | ||||
|                       aria-label={phoneNumber} | ||||
|                     > | ||||
|                     <MenuItem key={`phoneNumberMenuItem${i}`} value={phoneNumber} aria-label={phoneNumber}> | ||||
|                       {t("publish_dialog_call_item", { number: phoneNumber })} | ||||
|                     </MenuItem> | ||||
|                   ))} | ||||
|  | @ -584,13 +533,7 @@ const PublishDialog = (props) => { | |||
|               /> | ||||
|             </ClosableRow> | ||||
|           )} | ||||
|           <input | ||||
|             type="file" | ||||
|             ref={attachFileInput} | ||||
|             onChange={handleAttachFileChanged} | ||||
|             style={{ display: "none" }} | ||||
|             aria-hidden={true} | ||||
|           /> | ||||
|           <input type="file" ref={attachFileInput} onChange={handleAttachFileChanged} style={{ display: "none" }} aria-hidden={true} /> | ||||
|           {showAttachFile && ( | ||||
|             <AttachmentBox | ||||
|               file={attachFile} | ||||
|  | @ -712,11 +655,7 @@ const PublishDialog = (props) => { | |||
|               /> | ||||
|             )} | ||||
|             {account && !account?.phone_numbers && ( | ||||
|               <Tooltip | ||||
|                 title={t( | ||||
|                   "publish_dialog_chip_call_no_verified_numbers_tooltip" | ||||
|                 )} | ||||
|               > | ||||
|               <Tooltip title={t("publish_dialog_chip_call_no_verified_numbers_tooltip")}> | ||||
|                 <span> | ||||
|                   <Chip | ||||
|                     clickable | ||||
|  | @ -733,23 +672,13 @@ const PublishDialog = (props) => { | |||
|             <Trans | ||||
|               i18nKey="publish_dialog_details_examples_description" | ||||
|               components={{ | ||||
|                 docsLink: ( | ||||
|                   <Link | ||||
|                     href="https://ntfy.sh/docs" | ||||
|                     target="_blank" | ||||
|                     rel="noopener" | ||||
|                   /> | ||||
|                 ), | ||||
|                 docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />, | ||||
|               }} | ||||
|             /> | ||||
|           </Typography> | ||||
|         </DialogContent> | ||||
|         <DialogFooter status={status}> | ||||
|           {activeRequest && ( | ||||
|             <Button onClick={() => activeRequest.abort()}> | ||||
|               {t("publish_dialog_button_cancel_sending")} | ||||
|             </Button> | ||||
|           )} | ||||
|           {activeRequest && <Button onClick={() => activeRequest.abort()}>{t("publish_dialog_button_cancel_sending")}</Button>} | ||||
|           {!activeRequest && ( | ||||
|             <> | ||||
|               <FormControlLabel | ||||
|  | @ -761,16 +690,12 @@ const PublishDialog = (props) => { | |||
|                     checked={publishAnother} | ||||
|                     onChange={(ev) => setPublishAnother(ev.target.checked)} | ||||
|                     inputProps={{ | ||||
|                       "aria-label": t( | ||||
|                         "publish_dialog_checkbox_publish_another" | ||||
|                       ), | ||||
|                       "aria-label": t("publish_dialog_checkbox_publish_another"), | ||||
|                     }} | ||||
|                   /> | ||||
|                 } | ||||
|               /> | ||||
|               <Button onClick={props.onClose}> | ||||
|                 {t("publish_dialog_button_cancel")} | ||||
|               </Button> | ||||
|               <Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button> | ||||
|               <Button onClick={handleSubmit} disabled={!sendButtonEnabled}> | ||||
|                 {t("publish_dialog_button_send")} | ||||
|               </Button> | ||||
|  | @ -796,12 +721,7 @@ const ClosableRow = (props) => { | |||
|     <Row> | ||||
|       {props.children} | ||||
|       {closable && ( | ||||
|         <DialogIconButton | ||||
|           disabled={props.disabled} | ||||
|           onClick={props.onClose} | ||||
|           sx={{ marginLeft: "6px" }} | ||||
|           aria-label={props.closeLabel} | ||||
|         > | ||||
|         <DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{ marginLeft: "6px" }} aria-label={props.closeLabel}> | ||||
|           <Close /> | ||||
|         </DialogIconButton> | ||||
|       )} | ||||
|  | @ -856,23 +776,14 @@ const AttachmentBox = (props) => { | |||
|           <Typography variant="body2" sx={{ color: "text.primary" }}> | ||||
|             {formatBytes(file.size)} | ||||
|             {props.error && ( | ||||
|               <Typography | ||||
|                 component="span" | ||||
|                 sx={{ color: "error.main" }} | ||||
|                 aria-live="polite" | ||||
|               > | ||||
|               <Typography component="span" sx={{ color: "error.main" }} aria-live="polite"> | ||||
|                 {" "} | ||||
|                 ({props.error}) | ||||
|               </Typography> | ||||
|             )} | ||||
|           </Typography> | ||||
|         </Box> | ||||
|         <DialogIconButton | ||||
|           disabled={props.disabled} | ||||
|           onClick={props.onClose} | ||||
|           sx={{ marginLeft: "6px" }} | ||||
|           aria-label={t("publish_dialog_attached_file_remove")} | ||||
|         > | ||||
|         <DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{ marginLeft: "6px" }} aria-label={t("publish_dialog_attached_file_remove")}> | ||||
|           <Close /> | ||||
|         </DialogIconButton> | ||||
|       </Box> | ||||
|  | @ -888,22 +799,14 @@ const ExpandingTextField = (props) => { | |||
|     if (!boundingRect) { | ||||
|       return props.minWidth; | ||||
|     } | ||||
|     return boundingRect.width >= props.minWidth | ||||
|       ? Math.round(boundingRect.width) | ||||
|       : props.minWidth; | ||||
|     return boundingRect.width >= props.minWidth ? Math.round(boundingRect.width) : props.minWidth; | ||||
|   }; | ||||
|   useEffect(() => { | ||||
|     setTextWidth(determineTextWidth() + 5); | ||||
|   }, [props.value]); | ||||
|   return ( | ||||
|     <> | ||||
|       <Typography | ||||
|         ref={invisibleFieldRef} | ||||
|         component="span" | ||||
|         variant={props.variant} | ||||
|         aria-hidden={true} | ||||
|         sx={{ position: "absolute", left: "-200%" }} | ||||
|       > | ||||
|       <Typography ref={invisibleFieldRef} component="span" variant={props.variant} aria-hidden={true} sx={{ position: "absolute", left: "-200%" }}> | ||||
|         {props.value} | ||||
|       </Typography> | ||||
|       <TextField | ||||
|  | @ -983,9 +886,7 @@ const DropBox = () => { | |||
|           alignItems: "center", | ||||
|         }} | ||||
|       > | ||||
|         <Typography variant="h5"> | ||||
|           {t("publish_dialog_drop_file_here")} | ||||
|         </Typography> | ||||
|         <Typography variant="h5">{t("publish_dialog_drop_file_here")}</Typography> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
|  |  | |||
|  | @ -28,16 +28,13 @@ export const ReserveAddDialog = (props) => { | |||
|   const [everyone, setEveryone] = useState(Permission.DENY_ALL); | ||||
|   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); | ||||
|   const allowTopicEdit = !props.topic; | ||||
|   const alreadyReserved = | ||||
|     props.reservations.filter((r) => r.topic === topic).length > 0; | ||||
|   const alreadyReserved = props.reservations.filter((r) => r.topic === topic).length > 0; | ||||
|   const submitButtonEnabled = validTopic(topic) && !alreadyReserved; | ||||
| 
 | ||||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       await accountApi.upsertReservation(topic, everyone); | ||||
|       console.debug( | ||||
|         `[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}` | ||||
|       ); | ||||
|       console.debug(`[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}`); | ||||
|     } catch (e) { | ||||
|       console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); | ||||
|       if (e instanceof UnauthorizedError) { | ||||
|  | @ -54,18 +51,10 @@ export const ReserveAddDialog = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onClose} | ||||
|       maxWidth="sm" | ||||
|       fullWidth | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|     <Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> | ||||
|       <DialogTitle>{t("prefs_reservations_dialog_title_add")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("prefs_reservations_dialog_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("prefs_reservations_dialog_description")}</DialogContentText> | ||||
|         {allowTopicEdit && ( | ||||
|           <TextField | ||||
|             autoFocus | ||||
|  | @ -80,11 +69,7 @@ export const ReserveAddDialog = (props) => { | |||
|             variant="standard" | ||||
|           /> | ||||
|         )} | ||||
|         <ReserveTopicSelect | ||||
|           value={everyone} | ||||
|           onChange={setEveryone} | ||||
|           sx={{ mt: 1 }} | ||||
|         /> | ||||
|         <ReserveTopicSelect value={everyone} onChange={setEveryone} sx={{ mt: 1 }} /> | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onClose}>{t("common_cancel")}</Button> | ||||
|  | @ -99,17 +84,13 @@ export const ReserveAddDialog = (props) => { | |||
| export const ReserveEditDialog = (props) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const [error, setError] = useState(""); | ||||
|   const [everyone, setEveryone] = useState( | ||||
|     props.reservation?.everyone || Permission.DENY_ALL | ||||
|   ); | ||||
|   const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL); | ||||
|   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); | ||||
| 
 | ||||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       await accountApi.upsertReservation(props.reservation.topic, everyone); | ||||
|       console.debug( | ||||
|         `[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}` | ||||
|       ); | ||||
|       console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`); | ||||
|     } catch (e) { | ||||
|       console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); | ||||
|       if (e instanceof UnauthorizedError) { | ||||
|  | @ -123,23 +104,11 @@ export const ReserveEditDialog = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onClose} | ||||
|       maxWidth="sm" | ||||
|       fullWidth | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|     <Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> | ||||
|       <DialogTitle>{t("prefs_reservations_dialog_title_edit")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("prefs_reservations_dialog_description")} | ||||
|         </DialogContentText> | ||||
|         <ReserveTopicSelect | ||||
|           value={everyone} | ||||
|           onChange={setEveryone} | ||||
|           sx={{ mt: 1 }} | ||||
|         /> | ||||
|         <DialogContentText>{t("prefs_reservations_dialog_description")}</DialogContentText> | ||||
|         <ReserveTopicSelect value={everyone} onChange={setEveryone} sx={{ mt: 1 }} /> | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onClose}>{t("common_cancel")}</Button> | ||||
|  | @ -158,9 +127,7 @@ export const ReserveDeleteDialog = (props) => { | |||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       await accountApi.deleteReservation(props.topic, deleteMessages); | ||||
|       console.debug( | ||||
|         `[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}` | ||||
|       ); | ||||
|       console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`); | ||||
|     } catch (e) { | ||||
|       console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); | ||||
|       if (e instanceof UnauthorizedError) { | ||||
|  | @ -174,18 +141,10 @@ export const ReserveDeleteDialog = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onClose} | ||||
|       maxWidth="sm" | ||||
|       fullWidth | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|     <Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> | ||||
|       <DialogTitle>{t("prefs_reservations_dialog_title_delete")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("reservation_delete_dialog_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("reservation_delete_dialog_description")}</DialogContentText> | ||||
|         <FormControl fullWidth variant="standard"> | ||||
|           <Select | ||||
|             value={deleteMessages} | ||||
|  | @ -203,17 +162,13 @@ export const ReserveDeleteDialog = (props) => { | |||
|               <ListItemIcon> | ||||
|                 <Check /> | ||||
|               </ListItemIcon> | ||||
|               <ListItemText | ||||
|                 primary={t("reservation_delete_dialog_action_keep_title")} | ||||
|               /> | ||||
|               <ListItemText primary={t("reservation_delete_dialog_action_keep_title")} /> | ||||
|             </MenuItem> | ||||
|             <MenuItem value={true}> | ||||
|               <ListItemIcon> | ||||
|                 <DeleteForever /> | ||||
|               </ListItemIcon> | ||||
|               <ListItemText | ||||
|                 primary={t("reservation_delete_dialog_action_delete_title")} | ||||
|               /> | ||||
|               <ListItemText primary={t("reservation_delete_dialog_action_delete_title")} /> | ||||
|             </MenuItem> | ||||
|           </Select> | ||||
|         </FormControl> | ||||
|  |  | |||
|  | @ -4,12 +4,7 @@ import { useTranslation } from "react-i18next"; | |||
| import MenuItem from "@mui/material/MenuItem"; | ||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | ||||
| import ListItemText from "@mui/material/ListItemText"; | ||||
| import { | ||||
|   PermissionDenyAll, | ||||
|   PermissionRead, | ||||
|   PermissionReadWrite, | ||||
|   PermissionWrite, | ||||
| } from "./ReserveIcons"; | ||||
| import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; | ||||
| import { Permission } from "../app/AccountApi"; | ||||
| 
 | ||||
| const ReserveTopicSelect = (props) => { | ||||
|  | @ -34,33 +29,25 @@ const ReserveTopicSelect = (props) => { | |||
|           <ListItemIcon> | ||||
|             <PermissionDenyAll /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText | ||||
|             primary={t("prefs_reservations_table_everyone_deny_all")} | ||||
|           /> | ||||
|           <ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} /> | ||||
|         </MenuItem> | ||||
|         <MenuItem value={Permission.READ_ONLY}> | ||||
|           <ListItemIcon> | ||||
|             <PermissionRead /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText | ||||
|             primary={t("prefs_reservations_table_everyone_read_only")} | ||||
|           /> | ||||
|           <ListItemText primary={t("prefs_reservations_table_everyone_read_only")} /> | ||||
|         </MenuItem> | ||||
|         <MenuItem value={Permission.WRITE_ONLY}> | ||||
|           <ListItemIcon> | ||||
|             <PermissionWrite /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText | ||||
|             primary={t("prefs_reservations_table_everyone_write_only")} | ||||
|           /> | ||||
|           <ListItemText primary={t("prefs_reservations_table_everyone_write_only")} /> | ||||
|         </MenuItem> | ||||
|         <MenuItem value={Permission.READ_WRITE}> | ||||
|           <ListItemIcon> | ||||
|             <PermissionReadWrite /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText | ||||
|             primary={t("prefs_reservations_table_everyone_read_write")} | ||||
|           /> | ||||
|           <ListItemText primary={t("prefs_reservations_table_everyone_read_write")} /> | ||||
|         </MenuItem> | ||||
|       </Select> | ||||
|     </FormControl> | ||||
|  |  | |||
|  | @ -31,9 +31,7 @@ const Signup = () => { | |||
|     try { | ||||
|       await accountApi.create(user.username, user.password); | ||||
|       const token = await accountApi.login(user); | ||||
|       console.log( | ||||
|         `[Signup] User signup for user ${user.username} successful, token is ${token}` | ||||
|       ); | ||||
|       console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`); | ||||
|       session.store(user.username, token); | ||||
|       window.location.href = routes.app; | ||||
|     } catch (e) { | ||||
|  | @ -51,9 +49,7 @@ const Signup = () => { | |||
|   if (!config.enable_signup) { | ||||
|     return ( | ||||
|       <AvatarBox> | ||||
|         <Typography sx={{ typography: "h6" }}> | ||||
|           {t("signup_disabled")} | ||||
|         </Typography> | ||||
|         <Typography sx={{ typography: "h6" }}>{t("signup_disabled")}</Typography> | ||||
|       </AvatarBox> | ||||
|     ); | ||||
|   } | ||||
|  | @ -61,12 +57,7 @@ const Signup = () => { | |||
|   return ( | ||||
|     <AvatarBox> | ||||
|       <Typography sx={{ typography: "h6" }}>{t("signup_title")}</Typography> | ||||
|       <Box | ||||
|         component="form" | ||||
|         onSubmit={handleSubmit} | ||||
|         noValidate | ||||
|         sx={{ mt: 1, maxWidth: 400 }} | ||||
|       > | ||||
|       <Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, maxWidth: 400 }}> | ||||
|         <TextField | ||||
|           margin="dense" | ||||
|           required | ||||
|  | @ -130,13 +121,7 @@ const Signup = () => { | |||
|             ), | ||||
|           }} | ||||
|         /> | ||||
|         <Button | ||||
|           type="submit" | ||||
|           fullWidth | ||||
|           variant="contained" | ||||
|           disabled={username === "" || password === "" || password !== confirm} | ||||
|           sx={{ mt: 2, mb: 2 }} | ||||
|         > | ||||
|         <Button type="submit" fullWidth variant="contained" disabled={username === "" || password === "" || password !== confirm} sx={{ mt: 2, mb: 2 }}> | ||||
|           {t("signup_form_button_submit")} | ||||
|         </Button> | ||||
|         {error && ( | ||||
|  |  | |||
|  | @ -6,21 +6,10 @@ import Dialog from "@mui/material/Dialog"; | |||
| import DialogContent from "@mui/material/DialogContent"; | ||||
| import DialogContentText from "@mui/material/DialogContentText"; | ||||
| import DialogTitle from "@mui/material/DialogTitle"; | ||||
| import { | ||||
|   Autocomplete, | ||||
|   Checkbox, | ||||
|   FormControlLabel, | ||||
|   FormGroup, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery } from "@mui/material"; | ||||
| import theme from "./theme"; | ||||
| import api from "../app/Api"; | ||||
| import { | ||||
|   randomAlphanumericString, | ||||
|   topicUrl, | ||||
|   validTopic, | ||||
|   validUrl, | ||||
| } from "../app/utils"; | ||||
| import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; | ||||
| import userManager from "../app/UserManager"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import poller from "../app/Poller"; | ||||
|  | @ -64,14 +53,7 @@ const SubscribeDialog = (props) => { | |||
|           onSuccess={handleSuccess} | ||||
|         /> | ||||
|       )} | ||||
|       {showLoginPage && ( | ||||
|         <LoginPage | ||||
|           baseUrl={baseUrl} | ||||
|           topic={topic} | ||||
|           onBack={() => setShowLoginPage(false)} | ||||
|           onSuccess={handleSuccess} | ||||
|         /> | ||||
|       )} | ||||
|       {showLoginPage && <LoginPage baseUrl={baseUrl} topic={topic} onBack={() => setShowLoginPage(false)} onSuccess={handleSuccess} />} | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
|  | @ -85,37 +67,20 @@ const SubscribePage = (props) => { | |||
|   const [everyone, setEveryone] = useState(Permission.DENY_ALL); | ||||
|   const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; | ||||
|   const topic = props.topic; | ||||
|   const existingTopicUrls = props.subscriptions.map((s) => | ||||
|     topicUrl(s.baseUrl, s.topic) | ||||
|   ); | ||||
|   const existingBaseUrls = Array.from( | ||||
|     new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)]) | ||||
|   ).filter((s) => s !== config.base_url); | ||||
|   const showReserveTopicCheckbox = | ||||
|     config.enable_reservations && | ||||
|     !anotherServerVisible && | ||||
|     (config.enable_payments || account); | ||||
|   const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); | ||||
|   const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter((s) => s !== config.base_url); | ||||
|   const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account); | ||||
|   const reserveTopicEnabled = | ||||
|     session.exists() && | ||||
|     (account?.role === Role.ADMIN || | ||||
|       (account?.role === Role.USER && | ||||
|         (account?.stats.reservations_remaining || 0) > 0)); | ||||
|     session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0)); | ||||
| 
 | ||||
|   const handleSubscribe = async () => { | ||||
|     const user = await userManager.get(baseUrl); // May be undefined
 | ||||
|     const username = user | ||||
|       ? user.username | ||||
|       : t("subscribe_dialog_error_user_anonymous"); | ||||
|     const username = user ? user.username : t("subscribe_dialog_error_user_anonymous"); | ||||
| 
 | ||||
|     // Check read access to topic
 | ||||
|     const success = await api.topicAuth(baseUrl, topic, user); | ||||
|     if (!success) { | ||||
|       console.log( | ||||
|         `[SubscribeDialog] Login to ${topicUrl( | ||||
|           baseUrl, | ||||
|           topic | ||||
|         )} failed for user ${username}` | ||||
|       ); | ||||
|       console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||
|       if (user) { | ||||
|         setError( | ||||
|           t("subscribe_dialog_error_user_not_authorized", { | ||||
|  | @ -130,14 +95,8 @@ const SubscribePage = (props) => { | |||
|     } | ||||
| 
 | ||||
|     // Reserve topic (if requested)
 | ||||
|     if ( | ||||
|       session.exists() && | ||||
|       baseUrl === config.base_url && | ||||
|       reserveTopicVisible | ||||
|     ) { | ||||
|       console.log( | ||||
|         `[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}` | ||||
|       ); | ||||
|     if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { | ||||
|       console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`); | ||||
|       try { | ||||
|         await accountApi.upsertReservation(topic, everyone); | ||||
|       } catch (e) { | ||||
|  | @ -151,12 +110,7 @@ const SubscribePage = (props) => { | |||
|       } | ||||
|     } | ||||
| 
 | ||||
|     console.log( | ||||
|       `[SubscribeDialog] Successful login to ${topicUrl( | ||||
|         baseUrl, | ||||
|         topic | ||||
|       )} for user ${username}` | ||||
|     ); | ||||
|     console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); | ||||
|     props.onSuccess(); | ||||
|   }; | ||||
| 
 | ||||
|  | @ -167,14 +121,10 @@ const SubscribePage = (props) => { | |||
| 
 | ||||
|   const subscribeButtonEnabled = (() => { | ||||
|     if (anotherServerVisible) { | ||||
|       const isExistingTopicUrl = existingTopicUrls.includes( | ||||
|         topicUrl(baseUrl, topic) | ||||
|       ); | ||||
|       const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); | ||||
|       return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; | ||||
|     } else { | ||||
|       const isExistingTopicUrl = existingTopicUrls.includes( | ||||
|         topicUrl(config.base_url, topic) | ||||
|       ); | ||||
|       const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); | ||||
|       return validTopic(topic) && !isExistingTopicUrl; | ||||
|     } | ||||
|   })(); | ||||
|  | @ -191,9 +141,7 @@ const SubscribePage = (props) => { | |||
|     <> | ||||
|       <DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("subscribe_dialog_subscribe_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("subscribe_dialog_subscribe_description")}</DialogContentText> | ||||
|         <div style={{ display: "flex", paddingBottom: "8px" }} role="row"> | ||||
|           <TextField | ||||
|             autoFocus | ||||
|  | @ -241,9 +189,7 @@ const SubscribePage = (props) => { | |||
|                 </> | ||||
|               } | ||||
|             /> | ||||
|             {reserveTopicVisible && ( | ||||
|               <ReserveTopicSelect value={everyone} onChange={setEveryone} /> | ||||
|             )} | ||||
|             {reserveTopicVisible && <ReserveTopicSelect value={everyone} onChange={setEveryone} />} | ||||
|           </FormGroup> | ||||
|         )} | ||||
|         {!reserveTopicVisible && ( | ||||
|  | @ -253,9 +199,7 @@ const SubscribePage = (props) => { | |||
|                 <Checkbox | ||||
|                   onChange={handleUseAnotherChanged} | ||||
|                   inputProps={{ | ||||
|                     "aria-label": t( | ||||
|                       "subscribe_dialog_subscribe_use_another_label" | ||||
|                     ), | ||||
|                     "aria-label": t("subscribe_dialog_subscribe_use_another_label"), | ||||
|                   }} | ||||
|                 /> | ||||
|               } | ||||
|  | @ -268,12 +212,7 @@ const SubscribePage = (props) => { | |||
|                 inputValue={props.baseUrl} | ||||
|                 onInputChange={updateBaseUrl} | ||||
|                 renderInput={(params) => ( | ||||
|                   <TextField | ||||
|                     {...params} | ||||
|                     placeholder={config.base_url} | ||||
|                     variant="standard" | ||||
|                     aria-label={t("subscribe_dialog_subscribe_base_url_label")} | ||||
|                   /> | ||||
|                   <TextField {...params} placeholder={config.base_url} variant="standard" aria-label={t("subscribe_dialog_subscribe_base_url_label")} /> | ||||
|                 )} | ||||
|               /> | ||||
|             )} | ||||
|  | @ -281,9 +220,7 @@ const SubscribePage = (props) => { | |||
|         )} | ||||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onCancel}> | ||||
|           {t("subscribe_dialog_subscribe_button_cancel")} | ||||
|         </Button> | ||||
|         <Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button> | ||||
|         <Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}> | ||||
|           {t("subscribe_dialog_subscribe_button_subscribe")} | ||||
|         </Button> | ||||
|  | @ -304,23 +241,11 @@ const LoginPage = (props) => { | |||
|     const user = { baseUrl, username, password }; | ||||
|     const success = await api.topicAuth(baseUrl, topic, user); | ||||
|     if (!success) { | ||||
|       console.log( | ||||
|         `[SubscribeDialog] Login to ${topicUrl( | ||||
|           baseUrl, | ||||
|           topic | ||||
|         )} failed for user ${username}` | ||||
|       ); | ||||
|       setError( | ||||
|         t("subscribe_dialog_error_user_not_authorized", { username: username }) | ||||
|       ); | ||||
|       console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||
|       setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); | ||||
|       return; | ||||
|     } | ||||
|     console.log( | ||||
|       `[SubscribeDialog] Successful login to ${topicUrl( | ||||
|         baseUrl, | ||||
|         topic | ||||
|       )} for user ${username}` | ||||
|     ); | ||||
|     console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); | ||||
|     await userManager.save(user); | ||||
|     props.onSuccess(); | ||||
|   }; | ||||
|  | @ -329,9 +254,7 @@ const LoginPage = (props) => { | |||
|     <> | ||||
|       <DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("subscribe_dialog_login_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("subscribe_dialog_login_description")}</DialogContentText> | ||||
|         <TextField | ||||
|           autoFocus | ||||
|           margin="dense" | ||||
|  | @ -362,9 +285,7 @@ const LoginPage = (props) => { | |||
|       </DialogContent> | ||||
|       <DialogFooter status={error}> | ||||
|         <Button onClick={props.onBack}>{t("common_back")}</Button> | ||||
|         <Button onClick={handleLogin}> | ||||
|           {t("subscribe_dialog_login_button_login")} | ||||
|         </Button> | ||||
|         <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button> | ||||
|       </DialogFooter> | ||||
|     </> | ||||
|   ); | ||||
|  |  | |||
|  | @ -6,13 +6,7 @@ import Dialog from "@mui/material/Dialog"; | |||
| import DialogContent from "@mui/material/DialogContent"; | ||||
| import DialogContentText from "@mui/material/DialogContentText"; | ||||
| import DialogTitle from "@mui/material/DialogTitle"; | ||||
| import { | ||||
|   Chip, | ||||
|   InputAdornment, | ||||
|   Portal, | ||||
|   Snackbar, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { Chip, InputAdornment, Portal, Snackbar, useMediaQuery } from "@mui/material"; | ||||
| import theme from "./theme"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import DialogFooter from "./DialogFooter"; | ||||
|  | @ -28,11 +22,7 @@ import { useNavigate } from "react-router-dom"; | |||
| import IconButton from "@mui/material/IconButton"; | ||||
| import { Clear } from "@mui/icons-material"; | ||||
| import { AccountContext } from "./App"; | ||||
| import { | ||||
|   ReserveAddDialog, | ||||
|   ReserveDeleteDialog, | ||||
|   ReserveEditDialog, | ||||
| } from "./ReserveDialogs"; | ||||
| import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; | ||||
| import { UnauthorizedError } from "../app/errors"; | ||||
| 
 | ||||
| export const SubscriptionPopup = (props) => { | ||||
|  | @ -48,19 +38,11 @@ export const SubscriptionPopup = (props) => { | |||
|   const placement = props.placement ?? "left"; | ||||
|   const reservations = account?.reservations || []; | ||||
| 
 | ||||
|   const showReservationAdd = | ||||
|     config.enable_reservations && | ||||
|     !subscription?.reservation && | ||||
|     account?.stats.reservations_remaining > 0; | ||||
|   const showReservationAdd = config.enable_reservations && !subscription?.reservation && account?.stats.reservations_remaining > 0; | ||||
|   const showReservationAddDisabled = | ||||
|     !showReservationAdd && | ||||
|     config.enable_reservations && | ||||
|     !subscription?.reservation && | ||||
|     (config.enable_payments || account?.stats.reservations_remaining === 0); | ||||
|   const showReservationEdit = | ||||
|     config.enable_reservations && !!subscription?.reservation; | ||||
|   const showReservationDelete = | ||||
|     config.enable_reservations && !!subscription?.reservation; | ||||
|     !showReservationAdd && config.enable_reservations && !subscription?.reservation && (config.enable_payments || account?.stats.reservations_remaining === 0); | ||||
|   const showReservationEdit = config.enable_reservations && !!subscription?.reservation; | ||||
|   const showReservationDelete = config.enable_reservations && !!subscription?.reservation; | ||||
| 
 | ||||
|   const handleChangeDisplayName = async () => { | ||||
|     setDisplayNameDialogOpen(true); | ||||
|  | @ -115,14 +97,10 @@ export const SubscriptionPopup = (props) => { | |||
|     ])[0]; | ||||
|     const nowSeconds = Math.round(Date.now() / 1000); | ||||
|     const message = shuffle([ | ||||
|       `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime( | ||||
|         nowSeconds | ||||
|       )} right now. Is that early or late?`,
 | ||||
|       `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`, | ||||
|       `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, | ||||
|       `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, | ||||
|       `Alright then, it's ${formatShortDateTime( | ||||
|         nowSeconds | ||||
|       )} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
 | ||||
|       `Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, | ||||
|       `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, | ||||
|       `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, | ||||
|       `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`, | ||||
|  | @ -140,24 +118,16 @@ export const SubscriptionPopup = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   const handleClearAll = async () => { | ||||
|     console.log( | ||||
|       `[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}` | ||||
|     ); | ||||
|     console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`); | ||||
|     await subscriptionManager.deleteNotifications(props.subscription.id); | ||||
|   }; | ||||
| 
 | ||||
|   const handleUnsubscribe = async () => { | ||||
|     console.log( | ||||
|       `[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, | ||||
|       props.subscription | ||||
|     ); | ||||
|     console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription); | ||||
|     await subscriptionManager.remove(props.subscription.id); | ||||
|     if (session.exists() && !subscription.internal) { | ||||
|       try { | ||||
|         await accountApi.deleteSubscription( | ||||
|           props.subscription.baseUrl, | ||||
|           props.subscription.topic | ||||
|         ); | ||||
|         await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic); | ||||
|       } catch (e) { | ||||
|         console.log(`[SubscriptionPopup] Error unsubscribing`, e); | ||||
|         if (e instanceof UnauthorizedError) { | ||||
|  | @ -175,67 +145,26 @@ export const SubscriptionPopup = (props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <PopupMenu | ||||
|         horizontal={placement} | ||||
|         anchorEl={props.anchor} | ||||
|         open={!!props.anchor} | ||||
|         onClose={props.onClose} | ||||
|       > | ||||
|         <MenuItem onClick={handleChangeDisplayName}> | ||||
|           {t("action_bar_change_display_name")} | ||||
|         </MenuItem> | ||||
|         {showReservationAdd && ( | ||||
|           <MenuItem onClick={handleReserveAdd}> | ||||
|             {t("action_bar_reservation_add")} | ||||
|           </MenuItem> | ||||
|         )} | ||||
|       <PopupMenu horizontal={placement} anchorEl={props.anchor} open={!!props.anchor} onClose={props.onClose}> | ||||
|         <MenuItem onClick={handleChangeDisplayName}>{t("action_bar_change_display_name")}</MenuItem> | ||||
|         {showReservationAdd && <MenuItem onClick={handleReserveAdd}>{t("action_bar_reservation_add")}</MenuItem>} | ||||
|         {showReservationAddDisabled && ( | ||||
|           <MenuItem sx={{ cursor: "default" }}> | ||||
|             <span style={{ opacity: 0.3 }}> | ||||
|               {t("action_bar_reservation_add")} | ||||
|             </span> | ||||
|             <span style={{ opacity: 0.3 }}>{t("action_bar_reservation_add")}</span> | ||||
|             <ReserveLimitChip /> | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {showReservationEdit && ( | ||||
|           <MenuItem onClick={handleReserveEdit}> | ||||
|             {t("action_bar_reservation_edit")} | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         {showReservationDelete && ( | ||||
|           <MenuItem onClick={handleReserveDelete}> | ||||
|             {t("action_bar_reservation_delete")} | ||||
|           </MenuItem> | ||||
|         )} | ||||
|         <MenuItem onClick={handleSendTestMessage}> | ||||
|           {t("action_bar_send_test_notification")} | ||||
|         </MenuItem> | ||||
|         <MenuItem onClick={handleClearAll}> | ||||
|           {t("action_bar_clear_notifications")} | ||||
|         </MenuItem> | ||||
|         <MenuItem onClick={handleUnsubscribe}> | ||||
|           {t("action_bar_unsubscribe")} | ||||
|         </MenuItem> | ||||
|         {showReservationEdit && <MenuItem onClick={handleReserveEdit}>{t("action_bar_reservation_edit")}</MenuItem>} | ||||
|         {showReservationDelete && <MenuItem onClick={handleReserveDelete}>{t("action_bar_reservation_delete")}</MenuItem>} | ||||
|         <MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem> | ||||
|         <MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem> | ||||
|         <MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem> | ||||
|       </PopupMenu> | ||||
|       <Portal> | ||||
|         <Snackbar | ||||
|           open={showPublishError} | ||||
|           autoHideDuration={3000} | ||||
|           onClose={() => setShowPublishError(false)} | ||||
|           message={t("message_bar_error_publishing")} | ||||
|         /> | ||||
|         <DisplayNameDialog | ||||
|           open={displayNameDialogOpen} | ||||
|           subscription={subscription} | ||||
|           onClose={() => setDisplayNameDialogOpen(false)} | ||||
|         /> | ||||
|         <Snackbar open={showPublishError} autoHideDuration={3000} onClose={() => setShowPublishError(false)} message={t("message_bar_error_publishing")} /> | ||||
|         <DisplayNameDialog open={displayNameDialogOpen} subscription={subscription} onClose={() => setDisplayNameDialogOpen(false)} /> | ||||
|         {showReservationAdd && ( | ||||
|           <ReserveAddDialog | ||||
|             open={reserveAddDialogOpen} | ||||
|             topic={subscription.topic} | ||||
|             reservations={reservations} | ||||
|             onClose={() => setReserveAddDialogOpen(false)} | ||||
|           /> | ||||
|           <ReserveAddDialog open={reserveAddDialogOpen} topic={subscription.topic} reservations={reservations} onClose={() => setReserveAddDialogOpen(false)} /> | ||||
|         )} | ||||
|         {showReservationEdit && ( | ||||
|           <ReserveEditDialog | ||||
|  | @ -246,11 +175,7 @@ export const SubscriptionPopup = (props) => { | |||
|           /> | ||||
|         )} | ||||
|         {showReservationDelete && ( | ||||
|           <ReserveDeleteDialog | ||||
|             open={reserveDeleteDialogOpen} | ||||
|             topic={subscription.topic} | ||||
|             onClose={() => setReserveDeleteDialogOpen(false)} | ||||
|           /> | ||||
|           <ReserveDeleteDialog open={reserveDeleteDialogOpen} topic={subscription.topic} onClose={() => setReserveDeleteDialogOpen(false)} /> | ||||
|         )} | ||||
|       </Portal> | ||||
|     </> | ||||
|  | @ -261,28 +186,17 @@ const DisplayNameDialog = (props) => { | |||
|   const { t } = useTranslation(); | ||||
|   const subscription = props.subscription; | ||||
|   const [error, setError] = useState(""); | ||||
|   const [displayName, setDisplayName] = useState( | ||||
|     subscription.displayName ?? "" | ||||
|   ); | ||||
|   const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); | ||||
|   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); | ||||
| 
 | ||||
|   const handleSave = async () => { | ||||
|     await subscriptionManager.setDisplayName(subscription.id, displayName); | ||||
|     if (session.exists() && !subscription.internal) { | ||||
|       try { | ||||
|         console.log( | ||||
|           `[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}` | ||||
|         ); | ||||
|         await accountApi.updateSubscription( | ||||
|           subscription.baseUrl, | ||||
|           subscription.topic, | ||||
|           { display_name: displayName } | ||||
|         ); | ||||
|         console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`); | ||||
|         await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName }); | ||||
|       } catch (e) { | ||||
|         console.log( | ||||
|           `[SubscriptionSettingsDialog] Error updating subscription`, | ||||
|           e | ||||
|         ); | ||||
|         console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e); | ||||
|         if (e instanceof UnauthorizedError) { | ||||
|           session.resetAndRedirect(routes.login); | ||||
|         } else { | ||||
|  | @ -295,18 +209,10 @@ const DisplayNameDialog = (props) => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onClose} | ||||
|       maxWidth="sm" | ||||
|       fullWidth | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|     <Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> | ||||
|       <DialogTitle>{t("display_name_dialog_title")}</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText> | ||||
|           {t("display_name_dialog_description")} | ||||
|         </DialogContentText> | ||||
|         <DialogContentText>{t("display_name_dialog_description")}</DialogContentText> | ||||
|         <TextField | ||||
|           autoFocus | ||||
|           placeholder={t("display_name_dialog_placeholder")} | ||||
|  | @ -340,17 +246,10 @@ const DisplayNameDialog = (props) => { | |||
| 
 | ||||
| export const ReserveLimitChip = () => { | ||||
|   const { account } = useContext(AccountContext); | ||||
|   if ( | ||||
|     account?.role === Role.ADMIN || | ||||
|     account?.stats.reservations_remaining > 0 | ||||
|   ) { | ||||
|   if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { | ||||
|     return <></>; | ||||
|   } else if (config.enable_payments) { | ||||
|     return account?.limits.reservations > 0 ? ( | ||||
|       <LimitReachedChip /> | ||||
|     ) : ( | ||||
|       <ProChip /> | ||||
|     ); | ||||
|     return account?.limits.reservations > 0 ? <LimitReachedChip /> : <ProChip />; | ||||
|   } else if (account) { | ||||
|     return <LimitReachedChip />; | ||||
|   } | ||||
|  |  | |||
|  | @ -3,16 +3,7 @@ import { useContext, useEffect, useState } from "react"; | |||
| import Dialog from "@mui/material/Dialog"; | ||||
| import DialogContent from "@mui/material/DialogContent"; | ||||
| import DialogTitle from "@mui/material/DialogTitle"; | ||||
| import { | ||||
|   Alert, | ||||
|   CardActionArea, | ||||
|   CardContent, | ||||
|   Chip, | ||||
|   Link, | ||||
|   ListItem, | ||||
|   Switch, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery } from "@mui/material"; | ||||
| import theme from "./theme"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import accountApi, { SubscriptionInterval } from "../app/AccountApi"; | ||||
|  | @ -21,12 +12,7 @@ import routes from "./routes"; | |||
| import Card from "@mui/material/Card"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import { AccountContext } from "./App"; | ||||
| import { | ||||
|   formatBytes, | ||||
|   formatNumber, | ||||
|   formatPrice, | ||||
|   formatShortDate, | ||||
| } from "../app/utils"; | ||||
| import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils"; | ||||
| import { Trans, useTranslation } from "react-i18next"; | ||||
| import List from "@mui/material/List"; | ||||
| import { Check, Close } from "@mui/icons-material"; | ||||
|  | @ -43,9 +29,7 @@ const UpgradeDialog = (props) => { | |||
|   const { account } = useContext(AccountContext); // May be undefined!
 | ||||
|   const [error, setError] = useState(""); | ||||
|   const [tiers, setTiers] = useState(null); | ||||
|   const [interval, setInterval] = useState( | ||||
|     account?.billing?.interval || SubscriptionInterval.YEAR | ||||
|   ); | ||||
|   const [interval, setInterval] = useState(account?.billing?.interval || SubscriptionInterval.YEAR); | ||||
|   const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined
 | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); | ||||
|  | @ -61,9 +45,7 @@ const UpgradeDialog = (props) => { | |||
|     return <></>; | ||||
|   } | ||||
| 
 | ||||
|   const tiersMap = Object.assign( | ||||
|     ...tiers.map((tier) => ({ [tier.code]: tier })) | ||||
|   ); | ||||
|   const tiersMap = Object.assign(...tiers.map((tier) => ({ [tier.code]: tier }))); | ||||
|   const newTier = tiersMap[newTierCode]; // May be undefined
 | ||||
|   const currentTier = account?.tier; // May be undefined
 | ||||
|   const currentInterval = account?.billing?.interval; // May be undefined
 | ||||
|  | @ -75,10 +57,7 @@ const UpgradeDialog = (props) => { | |||
|     submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); | ||||
|     submitAction = Action.REDIRECT_SIGNUP; | ||||
|     banner = null; | ||||
|   } else if ( | ||||
|     currentTierCode === newTierCode && | ||||
|     (currentInterval === undefined || currentInterval === interval) | ||||
|   ) { | ||||
|   } else if (currentTierCode === newTierCode && (currentInterval === undefined || currentInterval === interval)) { | ||||
|     submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); | ||||
|     submitAction = null; | ||||
|     banner = currentTierCode ? Banner.PRORATION_INFO : null; | ||||
|  | @ -99,10 +78,7 @@ const UpgradeDialog = (props) => { | |||
|   // Exceptional conditions
 | ||||
|   if (loading) { | ||||
|     submitAction = null; | ||||
|   } else if ( | ||||
|     newTier?.code && | ||||
|     account?.reservations?.length > newTier?.limits?.reservations | ||||
|   ) { | ||||
|   } else if (newTier?.code && account?.reservations?.length > newTier?.limits?.reservations) { | ||||
|     submitAction = null; | ||||
|     banner = Banner.RESERVATIONS_WARNING; | ||||
|   } | ||||
|  | @ -115,10 +91,7 @@ const UpgradeDialog = (props) => { | |||
|     try { | ||||
|       setLoading(true); | ||||
|       if (submitAction === Action.CREATE_SUBSCRIPTION) { | ||||
|         const response = await accountApi.createBillingSubscription( | ||||
|           newTierCode, | ||||
|           interval | ||||
|         ); | ||||
|         const response = await accountApi.createBillingSubscription(newTierCode, interval); | ||||
|         window.location.href = response.redirect_url; | ||||
|       } else if (submitAction === Action.UPDATE_SUBSCRIPTION) { | ||||
|         await accountApi.updateBillingSubscription(newTierCode, interval); | ||||
|  | @ -142,16 +115,12 @@ const UpgradeDialog = (props) => { | |||
|   let discount = 0, | ||||
|     upto = false; | ||||
|   if (newTier?.prices) { | ||||
|     discount = Math.round( | ||||
|       ((newTier.prices.month * 12) / newTier.prices.year - 1) * 100 | ||||
|     ); | ||||
|     discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100); | ||||
|   } else { | ||||
|     let n = 0; | ||||
|     for (const t of tiers) { | ||||
|       if (t.prices) { | ||||
|         const tierDiscount = Math.round( | ||||
|           ((t.prices.month * 12) / t.prices.year - 1) * 100 | ||||
|         ); | ||||
|         const tierDiscount = Math.round(((t.prices.month * 12) / t.prices.year - 1) * 100); | ||||
|         if (tierDiscount > discount) { | ||||
|           discount = tierDiscount; | ||||
|           n++; | ||||
|  | @ -162,12 +131,7 @@ const UpgradeDialog = (props) => { | |||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog | ||||
|       open={props.open} | ||||
|       onClose={props.onCancel} | ||||
|       maxWidth="lg" | ||||
|       fullScreen={fullScreen} | ||||
|     > | ||||
|     <Dialog open={props.open} onClose={props.onCancel} maxWidth="lg" fullScreen={fullScreen}> | ||||
|       <DialogTitle> | ||||
|         <div style={{ display: "flex", flexDirection: "row" }}> | ||||
|           <div style={{ flexGrow: 1 }}>{t("account_upgrade_dialog_title")}</div> | ||||
|  | @ -184,13 +148,7 @@ const UpgradeDialog = (props) => { | |||
|             </Typography> | ||||
|             <Switch | ||||
|               checked={interval === SubscriptionInterval.YEAR} | ||||
|               onChange={(ev) => | ||||
|                 setInterval( | ||||
|                   ev.target.checked | ||||
|                     ? SubscriptionInterval.YEAR | ||||
|                     : SubscriptionInterval.MONTH | ||||
|                 ) | ||||
|               } | ||||
|               onChange={(ev) => setInterval(ev.target.checked ? SubscriptionInterval.YEAR : SubscriptionInterval.MONTH)} | ||||
|             /> | ||||
|             <Typography component="span" variant="subtitle1"> | ||||
|               {t("account_upgrade_dialog_interval_yearly")} | ||||
|  | @ -199,20 +157,12 @@ const UpgradeDialog = (props) => { | |||
|               <Chip | ||||
|                 label={ | ||||
|                   upto | ||||
|                     ? t( | ||||
|                         "account_upgrade_dialog_interval_yearly_discount_save_up_to", | ||||
|                         { discount: discount } | ||||
|                       ) | ||||
|                     : t( | ||||
|                         "account_upgrade_dialog_interval_yearly_discount_save", | ||||
|                         { discount: discount } | ||||
|                       ) | ||||
|                     ? t("account_upgrade_dialog_interval_yearly_discount_save_up_to", { discount: discount }) | ||||
|                     : t("account_upgrade_dialog_interval_yearly_discount_save", { discount: discount }) | ||||
|                 } | ||||
|                 color="primary" | ||||
|                 size="small" | ||||
|                 variant={ | ||||
|                   interval === SubscriptionInterval.YEAR ? "filled" : "outlined" | ||||
|                 } | ||||
|                 variant={interval === SubscriptionInterval.YEAR ? "filled" : "outlined"} | ||||
|                 sx={{ marginLeft: "5px" }} | ||||
|               /> | ||||
|             )} | ||||
|  | @ -258,9 +208,7 @@ const UpgradeDialog = (props) => { | |||
|           <Alert severity="warning" sx={{ fontSize: "1rem" }}> | ||||
|             <Trans | ||||
|               i18nKey="account_upgrade_dialog_reservations_warning" | ||||
|               count={ | ||||
|                 account?.reservations.length - newTier?.limits.reservations | ||||
|               } | ||||
|               count={account?.reservations.length - newTier?.limits.reservations} | ||||
|               components={{ | ||||
|                 Link: <NavLink to={routes.settings} />, | ||||
|               }} | ||||
|  | @ -309,9 +257,7 @@ const UpgradeDialog = (props) => { | |||
|           {error} | ||||
|         </DialogContentText> | ||||
|         <DialogActions sx={{ paddingRight: 2 }}> | ||||
|           <Button onClick={props.onCancel}> | ||||
|             {t("account_upgrade_dialog_button_cancel")} | ||||
|           </Button> | ||||
|           <Button onClick={props.onCancel}>{t("account_upgrade_dialog_button_cancel")}</Button> | ||||
|           <Button onClick={handleSubmit} disabled={!submitAction}> | ||||
|             {submitButtonLabel} | ||||
|           </Button> | ||||
|  | @ -382,16 +328,10 @@ const TierCard = (props) => { | |||
|               {tier.name || t("account_basics_tier_free")} | ||||
|             </Typography> | ||||
|             <div> | ||||
|               <Typography | ||||
|                 component="span" | ||||
|                 variant="h4" | ||||
|                 sx={{ fontWeight: 500, marginRight: "3px" }} | ||||
|               > | ||||
|               <Typography component="span" variant="h4" sx={{ fontWeight: 500, marginRight: "3px" }}> | ||||
|                 {formatPrice(monthlyPrice)} | ||||
|               </Typography> | ||||
|               {monthlyPrice > 0 && ( | ||||
|                 <>/ {t("account_upgrade_dialog_tier_price_per_month")}</> | ||||
|               )} | ||||
|               {monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}</>} | ||||
|             </div> | ||||
|             <List dense> | ||||
|               {tier.limits.reservations > 0 && ( | ||||
|  | @ -423,21 +363,10 @@ const TierCard = (props) => { | |||
|                 </Feature> | ||||
|               )} | ||||
|               <Feature> | ||||
|                 {t( | ||||
|                   "account_upgrade_dialog_tier_features_attachment_file_size", | ||||
|                   { filesize: formatBytes(tier.limits.attachment_file_size, 0) } | ||||
|                 )} | ||||
|                 {t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })} | ||||
|               </Feature> | ||||
|               {tier.limits.reservations === 0 && ( | ||||
|                 <NoFeature> | ||||
|                   {t("account_upgrade_dialog_tier_features_no_reservations")} | ||||
|                 </NoFeature> | ||||
|               )} | ||||
|               {tier.limits.calls === 0 && ( | ||||
|                 <NoFeature> | ||||
|                   {t("account_upgrade_dialog_tier_features_no_calls")} | ||||
|                 </NoFeature> | ||||
|               )} | ||||
|               {tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>} | ||||
|               {tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>} | ||||
|             </List> | ||||
|             {tier.prices && props.interval === SubscriptionInterval.MONTH && ( | ||||
|               <Typography variant="body2" color="gray"> | ||||
|  | @ -476,10 +405,7 @@ const FeatureItem = (props) => { | |||
|         {props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />} | ||||
|         {!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />} | ||||
|       </ListItemIcon> | ||||
|       <ListItemText | ||||
|         sx={{ mt: "2px", mb: "2px" }} | ||||
|         primary={<Typography variant="body1">{props.children}</Typography>} | ||||
|       /> | ||||
|       <ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} /> | ||||
|     </ListItem> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -32,41 +32,25 @@ export const useConnectionListeners = (account, subscriptions, users) => { | |||
|       }; | ||||
| 
 | ||||
|       const handleInternalMessage = async (message) => { | ||||
|         console.log( | ||||
|           `[ConnectionListener] Received message on sync topic`, | ||||
|           message.message | ||||
|         ); | ||||
|         console.log(`[ConnectionListener] Received message on sync topic`, message.message); | ||||
|         try { | ||||
|           const data = JSON.parse(message.message); | ||||
|           if (data.event === "sync") { | ||||
|             console.log(`[ConnectionListener] Triggering account sync`); | ||||
|             await accountApi.sync(); | ||||
|           } else { | ||||
|             console.log( | ||||
|               `[ConnectionListener] Unknown message type. Doing nothing.` | ||||
|             ); | ||||
|             console.log(`[ConnectionListener] Unknown message type. Doing nothing.`); | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.log( | ||||
|             `[ConnectionListener] Error parsing sync topic message`, | ||||
|             e | ||||
|           ); | ||||
|           console.log(`[ConnectionListener] Error parsing sync topic message`, e); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       const handleNotification = async (subscriptionId, notification) => { | ||||
|         const added = await subscriptionManager.addNotification( | ||||
|           subscriptionId, | ||||
|           notification | ||||
|         ); | ||||
|         const added = await subscriptionManager.addNotification(subscriptionId, notification); | ||||
|         if (added) { | ||||
|           const defaultClickAction = (subscription) => | ||||
|             navigate(routes.forSubscription(subscription)); | ||||
|           await notifier.notify( | ||||
|             subscriptionId, | ||||
|             notification, | ||||
|             defaultClickAction | ||||
|           ); | ||||
|           const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription)); | ||||
|           await notifier.notify(subscriptionId, notification, defaultClickAction); | ||||
|         } | ||||
|       }; | ||||
|       connectionManager.registerStateListener(subscriptionManager.updateState); | ||||
|  | @ -109,20 +93,12 @@ export const useAutoSubscribe = (subscriptions, selected) => { | |||
|       return; | ||||
|     } | ||||
|     setHasRun(true); | ||||
|     const eligible = | ||||
|       params.topic && !selected && !disallowedTopic(params.topic); | ||||
|     const eligible = params.topic && !selected && !disallowedTopic(params.topic); | ||||
|     if (eligible) { | ||||
|       const baseUrl = params.baseUrl | ||||
|         ? expandSecureUrl(params.baseUrl) | ||||
|         : config.base_url; | ||||
|       console.log( | ||||
|         `[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}` | ||||
|       ); | ||||
|       const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url; | ||||
|       console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); | ||||
|       (async () => { | ||||
|         const subscription = await subscriptionManager.add( | ||||
|           baseUrl, | ||||
|           params.topic | ||||
|         ); | ||||
|         const subscription = await subscriptionManager.add(baseUrl, params.topic); | ||||
|         if (session.exists()) { | ||||
|           try { | ||||
|             await accountApi.addSubscription(baseUrl, params.topic); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue