diff --git a/server/server.go b/server/server.go index e4187c1f..26bb8cf4 100644 --- a/server/server.go +++ b/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 diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 55628561..523aea30 100644 --- a/web/public/static/langs/en.json +++ b/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", diff --git a/web/src/components/Account.js b/web/src/components/Account.js index 890ee4f9..304272ad 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -41,9 +41,9 @@ const Account = () => { const Basics = () => { const { t } = useTranslation(); return ( - + - Account + {t("account_basics_title")} @@ -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 ( - - - {t("Usage")} - - - -
- {account?.role === "admin" - ? <>Unlimited 👑 - : t(`account_type_${accountType}`)} -
-
- -
- {account.stats.messages} - {account.limits.messages > 0 ? t("of {{limit}}", { limit: account.limits.messages }) : t("Unlimited")} -
- 0 ? normalize(account.stats.messages, account.limits.messages) : 100} /> -
- -
- {account.stats.emails} - {account.limits.emails > 0 ? t("of {{limit}}", { limit: account.limits.emails }) : t("Unlimited")} -
- 0 ? normalize(account.stats.emails, account.limits.emails) : 100} /> -
- -
- {formatBytes(account.stats.attachment_total_size)} - {account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")} -
- 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} /> -
-
- {account.limits.basis === "ip" && - Usage stats and limits for this account are based on your IP address, so they may be shared - with other users. - } -
- ); -}; - -const Delete = () => { - const { t } = useTranslation(); - return ( - - - {t("Delete account")} - - - - - - ); -}; - const Username = () => { const { t } = useTranslation(); const { account } = useOutletContext(); return ( - +
{session.username()} {account?.role === "admin" - ? <>{" "}👑 + ? <>{" "}👑 : ""}
@@ -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 ( - +
⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ - +
@@ -186,13 +124,13 @@ const ChangePasswordDialog = (props) => { })(); return ( - Change password + {t("account_basics_password_dialog_title")} setNewPassword(ev.target.value)} @@ -202,8 +140,8 @@ const ChangePasswordDialog = (props) => { setConfirmPassword(ev.target.value)} @@ -212,26 +150,94 @@ const ChangePasswordDialog = (props) => { /> - - + + ); }; +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 ( + + + {t("account_usage_title")} + + + +
+ {account?.role === "admin" + ? <>{t("account_usage_unlimited")} 👑 + : t(`account_usage_plan_code_${planCode}`)} +
+
+ +
+ {account.stats.messages} + {account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")} +
+ 0 ? normalize(account.stats.messages, account.limits.messages) : 100} /> +
+ +
+ {account.stats.emails} + {account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")} +
+ 0 ? normalize(account.stats.emails, account.limits.emails) : 100} /> +
+ +
+ {formatBytes(account.stats.attachment_total_size)} + {account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")} +
+ 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} /> +
+
+ {account.limits.basis === "ip" && + + {t("account_usage_basis_ip_description")} + + } +
+ ); +}; + +const Delete = () => { + const { t } = useTranslation(); + return ( + + + {t("account_delete_title")} + + + + + + ); +}; + 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 ( - +
{ const buttonEnabled = username === session.username(); return ( - {t("Delete account")} + {t("account_delete_title")} - {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()})} setUsername(ev.target.value)} @@ -288,8 +295,8 @@ const DeleteAccountDialog = (props) => { /> - - + + ); @@ -319,7 +326,6 @@ const Pref = (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 ( <> {subscription.mutedUntil ? : } - + - - {({TransitionProps, placement}) => ( - - - - - {t("action_bar_subscription_settings")} - {t("action_bar_send_test_notification")} - {t("action_bar_clear_notifications")} - {t("action_bar_unsubscribe")} - - - - - )} - + {t("action_bar_subscription_settings")} + {t("action_bar_send_test_notification")} + {t("action_bar_clear_notifications")} + {t("action_bar_unsubscribe")} + { ); }; -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() && - + } {!session.exists() && config.enableLogin && - + } {!session.exists() && config.enableSignup && - + } - navigate(routes.account)}> @@ -335,18 +271,58 @@ const ProfileIcon = (props) => { - Settings + {t("action_bar_profile_settings")} - Logout + {t("action_bar_profile_logout")} - + ); }; +const PopupMenu = (props) => { + return ( + + {props.children} + + ); +}; export default ActionBar; diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index 5cef785c..988893c8 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -124,10 +124,11 @@ const NavList = (props) => { } {session.exists() && - navigate(routes.account)} selected={location.pathname === routes.account}> + navigate(routes.account)} selected={location.pathname === routes.account}> - } + + } navigate(routes.settings)} selected={location.pathname === routes.settings}> diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index e7b0586b..b1dcc26e 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -45,7 +45,6 @@ const Preferences = () => { - ); @@ -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 ( - - - - Access control - - - Define read/write access to topics for this server. - - {entries?.length > 0 && } - - - - - - - ); -}; - -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 ( - - - - Topic - User - Access - - - - - {props.entries?.map(user => ( - - {user.username} - {user.baseUrl} - - handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> - - - handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> - - - - - ))} - - -
- ); -}; - -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 ( - - Add entry - - setTopic(ev.target.value)} - type="text" - fullWidth - variant="standard" - /> - - - - - - - - - - ); -}; -*/ - const maybeUpdateAccountSettings = async (payload) => { if (!session.exists()) { return;