1
0
binwiederhier 3 жил өмнө
parent
commit
9be8be49ef

+ 0 - 1
server/server.go

@@ -49,7 +49,6 @@ import (
 			- "mute" setting
 			- figure out what settings are "web" or "phone"
 		UI:
-		- Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown
 		- Translations
 		- aria-labels
 		- Home UI sign-in/login to top right

+ 37 - 6
web/public/static/langs/en.json

@@ -8,6 +8,11 @@
   "action_bar_unsubscribe": "Unsubscribe",
   "action_bar_toggle_mute": "Mute/unmute notifications",
   "action_bar_toggle_action_menu": "Open/close action menu",
+  "action_bar_profile_title": "Profile",
+  "action_bar_profile_settings": "Settings",
+  "action_bar_profile_logout": "Logout",
+  "action_bar_sign_in": "Sign in",
+  "action_bar_sign_up": "Sign up",
   "message_bar_type_message": "Type a message here",
   "message_bar_error_publishing": "Error publishing notification",
   "message_bar_show_dialog": "Show publish dialog",
@@ -141,12 +146,38 @@
   "subscribe_dialog_login_button_login": "Login",
   "subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
   "subscribe_dialog_error_user_anonymous": "anonymous",
-  "account_type_default": "Default",
-  "account_type_unlimited": "Unlimited",
-  "account_type_none": "None",
-  "account_type_pro": "Pro",
-  "account_type_business": "Business",
-  "account_type_business_plus": "Business Plus",
+  "account_basics_title": "Account",
+  "account_basics_username_title": "Username",
+  "account_basics_username_description": "Hey, that's you ❤",
+  "account_basics_username_admin_tooltip": "You are Admin",
+  "account_basics_password_title": "Password",
+  "account_basics_password_description": "Change your account password",
+  "account_basics_password_dialog_title": "Change password",
+  "account_basics_password_dialog_new_password_label": "New password",
+  "account_basics_password_dialog_confirm_password_label": "Confirm password",
+  "account_basics_password_dialog_button_cancel": "Cancel",
+  "account_basics_password_dialog_button_submit": "Change password",
+  "account_usage_title": "Usage",
+  "account_usage_of_limit": "of {{limit}}",
+  "account_usage_unlimited": "Unlimited",
+  "account_usage_plan_title": "Account type",
+  "account_usage_plan_code_default": "Default",
+  "account_usage_plan_code_unlimited": "Unlimited",
+  "account_usage_plan_code_none": "None",
+  "account_usage_plan_code_pro": "Pro",
+  "account_usage_plan_code_business": "Business",
+  "account_usage_plan_code_business_plus": "Business Plus",
+  "account_usage_messages_title": "Published messages",
+  "account_usage_emails_title": "Emails sent",
+  "account_usage_attachment_storage_title": "Attachment storage",
+  "account_usage_attachment_storage_subtitle": "{{filesize}} per file",
+  "account_usage_basis_ip_description": "Usage stats and limits for this account are based on your IP address, so they may be shared with other users.",
+  "account_delete_title": "Delete account",
+  "account_delete_description": "Permanently delete your account",
+  "account_delete_dialog_description": "This will permanently delete your account, including all data that is stored on the server. If you really want to proceed, please type '{{username}}' in the text box below.",
+  "account_delete_dialog_label": "Type '{{username}}' to delete account",
+  "account_delete_dialog_button_cancel": "Cancel",
+  "account_delete_dialog_button_submit": "Permanently delete account",
   "prefs_notifications_title": "Notifications",
   "prefs_notifications_sound_title": "Notification sound",
   "prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",

+ 96 - 90
web/src/components/Account.js

@@ -41,9 +41,9 @@ const Account = () => {
 const Basics = () => {
     const { t } = useTranslation();
     return (
-        <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}>
+        <Card sx={{p: 3}} aria-label={t("account_basics_title")}>
             <Typography variant="h5" sx={{marginBottom: 2}}>
-                Account
+                {t("account_basics_title")}
             </Typography>
             <PrefGroup>
                 <Username/>
@@ -53,80 +53,15 @@ const Basics = () => {
     );
 };
 
-const Stats = () => {
-    const { t } = useTranslation();
-    const { account } = useOutletContext();
-    if (!account) {
-        return <></>;
-    }
-    const accountType = account.plan.code ?? "none";
-    const normalize = (value, max) => (value / max * 100);
-    return (
-        <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}>
-            <Typography variant="h5" sx={{marginBottom: 2}}>
-                {t("Usage")}
-            </Typography>
-            <PrefGroup>
-                <Pref labelId={"accountType"} title={t("Account type")}>
-                    <div>
-                        {account?.role === "admin"
-                            ? <>Unlimited <Tooltip title={"You are Admin"}><span style={{cursor: "default"}}>👑</span></Tooltip></>
-                            : t(`account_type_${accountType}`)}
-                    </div>
-                </Pref>
-                <Pref labelId={"messages"} title={t("Published messages")}>
-                    <div>
-                        <Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
-                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("of {{limit}}", { limit: account.limits.messages }) : t("Unlimited")}</Typography>
-                    </div>
-                    <LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
-                </Pref>
-                <Pref labelId={"emails"} title={t("Emails sent")}>
-                    <div>
-                        <Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
-                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("of {{limit}}", { limit: account.limits.emails }) : t("Unlimited")}</Typography>
-                    </div>
-                    <LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
-                </Pref>
-                <Pref labelId={"attachments"} title={t("Attachment storage")} subtitle={t("{{filesize}} per file", { filesize: formatBytes(account.limits.attachment_file_size) })}>
-                    <div>
-                        <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
-                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")}</Typography>
-                    </div>
-                    <LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
-                </Pref>
-            </PrefGroup>
-            {account.limits.basis === "ip" && <Typography variant="body1">
-                <em>Usage stats and limits for this account are based on your IP address, so they may be shared
-                    with other users.</em>
-            </Typography>}
-        </Card>
-    );
-};
-
-const Delete = () => {
-    const { t } = useTranslation();
-    return (
-        <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}>
-            <Typography variant="h5" sx={{marginBottom: 2}}>
-                {t("Delete account")}
-            </Typography>
-            <PrefGroup>
-                <DeleteAccount/>
-            </PrefGroup>
-        </Card>
-    );
-};
-
 const Username = () => {
     const { t } = useTranslation();
     const { account } = useOutletContext();
     return (
-        <Pref labelId={"username"} title={t("Username")} description={t("Hey, that's you ❤")}>
+        <Pref title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
             <div>
                 {session.username()}
                 {account?.role === "admin"
-                    ? <>{" "}<Tooltip title={"You are Admin"}><span style={{cursor: "default"}}>👑</span></Tooltip></>
+                    ? <>{" "}<Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
                     : ""}
             </div>
         </Pref>
@@ -137,14 +72,16 @@ const ChangePassword = () => {
     const { t } = useTranslation();
     const [dialogKey, setDialogKey] = useState(0);
     const [dialogOpen, setDialogOpen] = useState(false);
-    const labelId = "prefChangePassword";
+
     const handleDialogOpen = () => {
         setDialogKey(prev => prev+1);
         setDialogOpen(true);
     };
+
     const handleDialogCancel = () => {
         setDialogOpen(false);
     };
+
     const handleDialogSubmit = async (newPassword) => {
         try {
             await accountApi.changePassword(newPassword);
@@ -158,11 +95,12 @@ const ChangePassword = () => {
             // TODO show error
         }
     };
+
     return (
-        <Pref labelId={labelId} title={t("Password")} description={t("Change your account password")}>
+        <Pref title={t("account_basics_password_title")} description={t("account_basics_password_description")}>
             <div>
                 <Typography color="gray" sx={{float: "left", fontSize: "0.7rem", lineHeight: "3.5"}}>⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤</Typography>
-                <IconButton onClick={handleDialogOpen} aria-label={t("xxxxxxxx")}>
+                <IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
                     <EditIcon/>
                 </IconButton>
             </div>
@@ -186,13 +124,13 @@ const ChangePasswordDialog = (props) => {
     })();
     return (
         <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
-            <DialogTitle>Change password</DialogTitle>
+            <DialogTitle>{t("account_basics_password_dialog_title")}</DialogTitle>
             <DialogContent>
                 <TextField
                     margin="dense"
                     id="new-password"
-                    label={t("New password")}
-                    aria-label={t("xxxx")}
+                    label={t("account_basics_password_dialog_new_password_label")}
+                    aria-label={t("account_basics_password_dialog_new_password_label")}
                     type="password"
                     value={newPassword}
                     onChange={ev => setNewPassword(ev.target.value)}
@@ -202,8 +140,8 @@ const ChangePasswordDialog = (props) => {
                 <TextField
                     margin="dense"
                     id="confirm"
-                    label={t("Confirm password")}
-                    aria-label={t("xxx")}
+                    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)}
@@ -212,26 +150,94 @@ const ChangePasswordDialog = (props) => {
                 />
             </DialogContent>
             <DialogActions>
-                <Button onClick={props.onCancel}>{t("Cancel")}</Button>
-                <Button onClick={() => props.onSubmit(newPassword)} disabled={!changeButtonEnabled}>{t("Change password")}</Button>
+                <Button onClick={props.onCancel}>{t("account_basics_password_dialog_button_cancel")}</Button>
+                <Button onClick={() => props.onSubmit(newPassword)} disabled={!changeButtonEnabled}>{t("account_basics_password_dialog_button_submit")}</Button>
             </DialogActions>
         </Dialog>
     );
 };
 
+const Stats = () => {
+    const { t } = useTranslation();
+    const { account } = useOutletContext();
+    if (!account) {
+        return <></>;
+    }
+    const planCode = account.plan.code ?? "none";
+    const normalize = (value, max) => (value / max * 100);
+    return (
+        <Card sx={{p: 3}} aria-label={t("account_usage_title")}>
+            <Typography variant="h5" sx={{marginBottom: 2}}>
+                {t("account_usage_title")}
+            </Typography>
+            <PrefGroup>
+                <Pref title={t("account_usage_plan_title")}>
+                    <div>
+                        {account?.role === "admin"
+                            ? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
+                            : t(`account_usage_plan_code_${planCode}`)}
+                    </div>
+                </Pref>
+                <Pref title={t("account_usage_messages_title")}>
+                    <div>
+                        <Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
+                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
+                    </div>
+                    <LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
+                </Pref>
+                <Pref title={t("account_usage_emails_title")}>
+                    <div>
+                        <Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
+                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
+                    </div>
+                    <LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
+                </Pref>
+                <Pref title={t("account_usage_attachment_storage_title")} subtitle={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}>
+                    <div>
+                        <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
+                        <Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
+                    </div>
+                    <LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
+                </Pref>
+            </PrefGroup>
+            {account.limits.basis === "ip" &&
+                <Typography variant="body1">
+                    <em>{t("account_usage_basis_ip_description")}</em>
+                </Typography>
+            }
+        </Card>
+    );
+};
+
+const Delete = () => {
+    const { t } = useTranslation();
+    return (
+        <Card sx={{p: 3}} aria-label={t("account_delete_title")}>
+            <Typography variant="h5" sx={{marginBottom: 2}}>
+                {t("account_delete_title")}
+            </Typography>
+            <PrefGroup>
+                <DeleteAccount/>
+            </PrefGroup>
+        </Card>
+    );
+};
+
 const DeleteAccount = () => {
     const { t } = useTranslation();
     const [dialogKey, setDialogKey] = useState(0);
     const [dialogOpen, setDialogOpen] = useState(false);
-    const labelId = "prefDeleteAccount";
+
     const handleDialogOpen = () => {
         setDialogKey(prev => prev+1);
         setDialogOpen(true);
     };
+
     const handleDialogCancel = () => {
         setDialogOpen(false);
     };
-    const handleDialogSubmit = async (newPassword) => {
+
+    const handleDialogSubmit = async () => {
         try {
             await accountApi.delete();
             await db.delete();
@@ -246,11 +252,12 @@ const DeleteAccount = () => {
             // TODO show error
         }
     };
+
     return (
-        <Pref labelId={labelId} title={t("Delete account")} description={t("Permanently delete your account")}>
+        <Pref title={t("account_delete_title")} description={t("account_delete_description")}>
             <div>
                 <Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
-                    Delete account
+                    {t("account_delete_title")}
                 </Button>
             </div>
             <DeleteAccountDialog
@@ -270,16 +277,16 @@ const DeleteAccountDialog = (props) => {
     const buttonEnabled = username === session.username();
     return (
         <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
-            <DialogTitle>{t("Delete account")}</DialogTitle>
+            <DialogTitle>{t("account_delete_title")}</DialogTitle>
             <DialogContent>
                 <Typography variant="body1">
-                    {t("This will permanently delete your account, including all data that is stored on the server. If you really want to proceed, please type '{{username}}' in the text box below.", { username: session.username()})}
+                    {t("account_delete_dialog_description", { username: session.username()})}
                 </Typography>
                 <TextField
                     margin="dense"
                     id="account-delete-confirm"
-                    label={t("Type '{{username}}' to delete account", { username: session.username()})}
-                    aria-label={t("xxxx")}
+                    label={t("account_delete_dialog_label", { username: session.username()})}
+                    aria-label={t("account_delete_dialog_label", { username: session.username()})}
                     type="text"
                     value={username}
                     onChange={ev => setUsername(ev.target.value)}
@@ -288,8 +295,8 @@ const DeleteAccountDialog = (props) => {
                 />
             </DialogContent>
             <DialogActions>
-                <Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
-                <Button onClick={props.onSubmit} color="error" disabled={!buttonEnabled}>{t("Permanently delete account")}</Button>
+                <Button onClick={props.onCancel}>{t("account_delete_dialog_button_cancel")}</Button>
+                <Button onClick={props.onSubmit} color="error" disabled={!buttonEnabled}>{t("account_delete_dialog_button_submit")}</Button>
             </DialogActions>
         </Dialog>
     );
@@ -319,7 +326,6 @@ const Pref = (props) => {
         >
             <div
                 role="cell"
-                id={props.labelId}
                 aria-label={props.title}
                 style={{
                     flex: '1 0 40%',

+ 70 - 94
web/src/components/ActionBar.js

@@ -83,18 +83,17 @@ const ActionBar = (props) => {
     );
 };
 
-// Originally from https://mui.com/components/menus/#MenuListComposition.js
 const SettingsIcons = (props) => {
     const { t } = useTranslation();
     const navigate = useNavigate();
-    const [open, setOpen] = useState(false);
+    const [anchorEl, setAnchorEl] = useState(null);
     const [snackOpen, setSnackOpen] = useState(false);
     const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false);
-    const anchorRef = useRef(null);
     const subscription = props.subscription;
+    const open = Boolean(anchorEl);
 
-    const handleToggleOpen = () => {
-        setOpen((prevOpen) => !prevOpen);
+    const handleToggleOpen = (event) => {
+        setAnchorEl(event.currentTarget);
     };
 
     const handleToggleMute = async () => {
@@ -102,22 +101,17 @@ const SettingsIcons = (props) => {
         await subscriptionManager.setMutedUntil(subscription.id, mutedUntil);
     }
 
-    const handleClose = (event) => {
-        if (anchorRef.current && anchorRef.current.contains(event.target)) {
-            return;
-        }
-        setOpen(false);
+    const handleClose = () => {
+        setAnchorEl(null);
     };
 
     const handleClearAll = async (event) => {
-        handleClose(event);
         console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
         await subscriptionManager.deleteNotifications(props.subscription.id);
     };
 
     const handleUnsubscribe = async (event) => {
         console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
-        handleClose(event);
         await subscriptionManager.remove(props.subscription.id);
         if (session.exists() && props.subscription.remoteId) {
             try {
@@ -181,61 +175,26 @@ const SettingsIcons = (props) => {
             console.log(`[ActionBar] Error publishing message`, e);
             setSnackOpen(true);
         }
-        setOpen(false);
     }
 
-    const handleListKeyDown = (event) => {
-        if (event.key === 'Tab') {
-            event.preventDefault();
-            setOpen(false);
-        } else if (event.key === 'Escape') {
-            setOpen(false);
-        }
-    }
-
-    // return focus to the button when we transitioned from !open -> open
-    const prevOpen = useRef(open);
-    useEffect(() => {
-        if (prevOpen.current === true && open === false) {
-            anchorRef.current.focus();
-        }
-        prevOpen.current = open;
-    }, [open]);
-
     return (
         <>
             <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" ref={anchorRef} onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
+            <IconButton color="inherit" size="large" edge="end" onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
                 <MoreVertIcon/>
             </IconButton>
-            <Popper
+            <PopupMenu
+                anchorEl={anchorEl}
                 open={open}
-                anchorEl={anchorRef.current}
-                role={undefined}
-                placement="bottom-start"
-                transition
-                disablePortal
+                onClose={handleClose}
             >
-                {({TransitionProps, placement}) => (
-                    <Grow
-                        {...TransitionProps}
-                        style={{transformOrigin: placement === 'bottom-start' ? 'left top' : 'left bottom'}}
-                    >
-                        <Paper>
-                            <ClickAwayListener onClickAway={handleClose}>
-                                <MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}>
-                                    <MenuItem onClick={handleSubscriptionSettings}>{t("action_bar_subscription_settings")}</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>
-                                </MenuList>
-                            </ClickAwayListener>
-                        </Paper>
-                    </Grow>
-                )}
-            </Popper>
+                <MenuItem onClick={handleSubscriptionSettings}>{t("action_bar_subscription_settings")}</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={snackOpen}
@@ -256,7 +215,7 @@ const SettingsIcons = (props) => {
     );
 };
 
-const ProfileIcon = (props) => {
+const ProfileIcon = () => {
     const { t } = useTranslation();
     const [anchorEl, setAnchorEl] = useState(null);
     const open = Boolean(anchorEl);
@@ -265,9 +224,11 @@ const ProfileIcon = (props) => {
     const handleClick = (event) => {
         setAnchorEl(event.currentTarget);
     };
+
     const handleClose = () => {
         setAnchorEl(null);
     };
+
     const handleLogout = async () => {
         try {
             await accountApi.logout();
@@ -276,53 +237,28 @@ const ProfileIcon = (props) => {
             session.resetAndRedirect(routes.app);
         }
     };
+
     return (
         <>
             {session.exists() &&
-                <IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("xxxxxxx")}>
+                <IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("action_bar_profile_title")}>
                     <AccountCircleIcon/>
                 </IconButton>
             }
             {!session.exists() && config.enableLogin &&
-                <Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{m: 1}}>Sign in</Button>
+                <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.enableSignup &&
-                <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
+                <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)} aria-label={t("action_bar_sign_up")}>
+                    {t("action_bar_sign_up")}
+                </Button>
             }
-            <Menu
+            <PopupMenu
                 anchorEl={anchorEl}
-                id="account-menu"
                 open={open}
                 onClose={handleClose}
-                onClick={handleClose}
-                PaperProps={{
-                    elevation: 0,
-                    sx: {
-                        overflow: 'visible',
-                        filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
-                        mt: 1.5,
-                        '& .MuiAvatar-root': {
-                            width: 32,
-                            height: 32,
-                            ml: -0.5,
-                            mr: 1,
-                        },
-                        '&:before': {
-                            content: '""',
-                            display: 'block',
-                            position: 'absolute',
-                            top: 0,
-                            right: 19,
-                            width: 10,
-                            height: 10,
-                            bgcolor: 'background.paper',
-                            transform: 'translateY(-50%) rotate(45deg)',
-                            zIndex: 0,
-                        },
-                    },
-                }}
-                transformOrigin={{ horizontal: 'right', vertical: 'top' }}
-                anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
             >
                 <MenuItem onClick={() => navigate(routes.account)}>
                     <ListItemIcon>
@@ -335,18 +271,58 @@ const ProfileIcon = (props) => {
                     <ListItemIcon>
                         <Settings fontSize="small" />
                     </ListItemIcon>
-                    Settings
+                    {t("action_bar_profile_settings")}
                 </MenuItem>
                 <MenuItem onClick={handleLogout}>
                     <ListItemIcon>
                         <Logout fontSize="small" />
                     </ListItemIcon>
-                    Logout
+                    {t("action_bar_profile_logout")}
                 </MenuItem>
-            </Menu>
+            </PopupMenu>
         </>
     );
 };
 
+const PopupMenu = (props) => {
+    return (
+        <Menu
+            anchorEl={props.anchorEl}
+            open={props.open}
+            onClose={props.onClose}
+            onClick={props.onClose}
+            PaperProps={{
+                elevation: 0,
+                sx: {
+                    overflow: 'visible',
+                    filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
+                    mt: 1.5,
+                    '& .MuiAvatar-root': {
+                        width: 32,
+                        height: 32,
+                        ml: -0.5,
+                        mr: 1,
+                    },
+                    '&:before': {
+                        content: '""',
+                        display: 'block',
+                        position: 'absolute',
+                        top: 0,
+                        right: 19,
+                        width: 10,
+                        height: 10,
+                        bgcolor: 'background.paper',
+                        transform: 'translateY(-50%) rotate(45deg)',
+                        zIndex: 0,
+                    },
+                },
+            }}
+            transformOrigin={{ horizontal: 'right', vertical: 'top' }}
+            anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
+        >
+            {props.children}
+        </Menu>
+    );
+};
 
 export default ActionBar;

+ 3 - 2
web/src/components/Navigation.js

@@ -124,10 +124,11 @@ const NavList = (props) => {
                         <Divider sx={{my: 1}}/>
                     </>}
                 {session.exists() &&
-                        <ListItemButton onClick={() => navigate(routes.account)} selected={location.pathname === routes.account}>
+                    <ListItemButton onClick={() => navigate(routes.account)} selected={location.pathname === routes.account}>
                         <ListItemIcon><Person/></ListItemIcon>
                         <ListItemText primary={t("nav_button_account")}/>
-                    </ListItemButton>}
+                    </ListItemButton>
+                }
                 <ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}>
                     <ListItemIcon><SettingsIcon/></ListItemIcon>
                     <ListItemText primary={t("nav_button_settings")}/>

+ 0 - 165
web/src/components/Preferences.js

@@ -45,7 +45,6 @@ const Preferences = () => {
                 <Notifications/>
                 <Appearance/>
                 <Users/>
-                <AccessControl/>
             </Stack>
         </Container>
     );
@@ -507,170 +506,6 @@ const Language = () => {
     )
 };
 
-const AccessControl = () => {
-    return <></>;
-}
-/*
-const AccessControl = () => {
-    const { t } = useTranslation();
-    const [dialogKey, setDialogKey] = useState(0);
-    const [dialogOpen, setDialogOpen] = useState(false);
-    const entries = useLiveQuery(() => userManager.all());
-    const handleAddClick = () => {
-        setDialogKey(prev => prev+1);
-        setDialogOpen(true);
-    };
-    const handleDialogCancel = () => {
-        setDialogOpen(false);
-    };
-    const handleDialogSubmit = async (user) => {
-        setDialogOpen(false);
-        try {
-            await userManager.save(user);
-            console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`);
-        } catch (e) {
-            console.log(`[Preferences] Error adding user.`, e);
-        }
-    };
-    return (
-        <Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
-            <CardContent sx={{ paddingBottom: 1 }}>
-                <Typography variant="h5" sx={{marginBottom: 2}}>
-                    Access control
-                </Typography>
-                <Paragraph>
-                    Define read/write access to topics for this server.
-                </Paragraph>
-                {entries?.length > 0 && <AccessControlTable entries={entries}/>}
-            </CardContent>
-            <CardActions>
-                <Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
-                <AccessControlDialog
-                    key={`aclDialog${dialogKey}`}
-                    open={dialogOpen}
-                    user={null}
-                    users={entries}
-                    onCancel={handleDialogCancel}
-                    onSubmit={handleDialogSubmit}
-                />
-            </CardActions>
-        </Card>
-    );
-};
-
-const AccessControlTable = (props) => {
-    const { t } = useTranslation();
-    const [dialogKey, setDialogKey] = useState(0);
-    const [dialogOpen, setDialogOpen] = useState(false);
-    const [dialogUser, setDialogUser] = useState(null);
-    const handleEditClick = (user) => {
-        setDialogKey(prev => prev+1);
-        setDialogUser(user);
-        setDialogOpen(true);
-    };
-    const handleDialogCancel = () => {
-        setDialogOpen(false);
-    };
-    const handleDialogSubmit = async (user) => {
-        setDialogOpen(false);
-        try {
-            await userManager.save(user);
-            console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`);
-        } catch (e) {
-            console.log(`[Preferences] Error updating user.`, e);
-        }
-    };
-    const handleDeleteClick = async (user) => {
-        try {
-            await userManager.delete(user.baseUrl);
-            console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`);
-        } catch (e) {
-            console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e);
-        }
-    };
-    return (
-        <Table size="small" aria-label={t("prefs_users_table")}>
-            <TableHead>
-                <TableRow>
-                    <TableCell sx={{paddingLeft: 0}}>Topic</TableCell>
-                    <TableCell>User</TableCell>
-                    <TableCell>Access</TableCell>
-                    <TableCell/>
-                </TableRow>
-            </TableHead>
-            <TableBody>
-                {props.entries?.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")}>{user.username}</TableCell>
-                        <TableCell aria-label={t("xxxxxxxxxx")}>{user.baseUrl}</TableCell>
-                        <TableCell align="right">
-                            <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
-                                <EditIcon/>
-                            </IconButton>
-                            <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
-                                <CloseIcon />
-                            </IconButton>
-                        </TableCell>
-                    </TableRow>
-                ))}
-            </TableBody>
-            <AccessControlDialog
-                key={`userEditDialog${dialogKey}`}
-                open={dialogOpen}
-                user={dialogUser}
-                users={props.entries}
-                onCancel={handleDialogCancel}
-                onSubmit={handleDialogSubmit}
-            />
-        </Table>
-    );
-};
-
-const AccessControlDialog = (props) => {
-    const { t } = useTranslation();
-    const [topic, setTopic] = useState("");
-    const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
-    const addButtonEnabled = (() => {
-        return validTopic(topic);
-    })();
-    const handleSubmit = async () => {
-        // TODO
-    };
-    return (
-        <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
-            <DialogTitle>Add entry</DialogTitle>
-            <DialogContent>
-                <TextField
-                    autoFocus={editMode}
-                    margin="dense"
-                    id="topic"
-                    label={"Topic"}
-                    aria-label={"Topic xx"}
-                    value={topic}
-                    onChange={ev => setTopic(ev.target.value)}
-                    type="text"
-                    fullWidth
-                    variant="standard"
-                />
-                <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
-                    <Select value={"read-write"} onChange={() => {}}>
-                        <MenuItem value={"private"}>Read/write access only by me</MenuItem>
-                        <MenuItem value={"read-only"}>Read/write access by user, anonymous read</MenuItem>
-                    </Select>
-                </FormControl>
-            </DialogContent>
-            <DialogActions>
-                <Button onClick={props.onCancel}>Cancel</Button>
-                <Button onClick={handleSubmit} disabled={!addButtonEnabled}>Add entry</Button>
-            </DialogActions>
-        </Dialog>
-    );
-};
-*/
-
 const maybeUpdateAccountSettings = async (payload) => {
     if (!session.exists()) {
         return;