WIP: DIsplay name for the web app
This commit is contained in:
		
							parent
							
								
									2d26a990a9
								
							
						
					
					
						commit
						4d6c147f24
					
				
					 9 changed files with 108 additions and 15 deletions
				
			
		|  | @ -14,7 +14,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | ||||||
| 
 | 
 | ||||||
| **Bugs:** | **Bugs:** | ||||||
| 
 | 
 | ||||||
| * Long-click selecting of notifications doesn't scoll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8)) | * Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| * Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8)) | * Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| * Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting) | * Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|   "action_bar_show_menu": "Show menu", |   "action_bar_show_menu": "Show menu", | ||||||
|   "action_bar_logo_alt": "ntfy logo", |   "action_bar_logo_alt": "ntfy logo", | ||||||
|   "action_bar_settings": "Settings", |   "action_bar_settings": "Settings", | ||||||
|  |   "action_bar_subscription_settings": "Subscription settings", | ||||||
|   "action_bar_send_test_notification": "Send test notification", |   "action_bar_send_test_notification": "Send test notification", | ||||||
|   "action_bar_clear_notifications": "Clear all notifications", |   "action_bar_clear_notifications": "Clear all notifications", | ||||||
|   "action_bar_unsubscribe": "Unsubscribe", |   "action_bar_unsubscribe": "Unsubscribe", | ||||||
|  | @ -59,6 +60,11 @@ | ||||||
|   "notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", |   "notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", | ||||||
|   "notifications_example": "Example", |   "notifications_example": "Example", | ||||||
|   "notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.", |   "notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.", | ||||||
|  |   "subscription_settings_dialog_title": "Subscription settings", | ||||||
|  |   "subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.", | ||||||
|  |   "subscription_settings_dialog_display_name_placeholder": "Display name", | ||||||
|  |   "subscription_settings_button_cancel": "Cancel", | ||||||
|  |   "subscription_settings_button_save": "Save", | ||||||
|   "notifications_loading": "Loading notifications …", |   "notifications_loading": "Loading notifications …", | ||||||
|   "publish_dialog_title_topic": "Publish to {{topic}}", |   "publish_dialog_title_topic": "Publish to {{topic}}", | ||||||
|   "publish_dialog_title_no_topic": "Publish notification", |   "publish_dialog_title_no_topic": "Publish notification", | ||||||
|  |  | ||||||
|  | @ -1,13 +1,12 @@ | ||||||
| import { | import { | ||||||
|     basicAuth, |  | ||||||
|     encodeBase64, |  | ||||||
|     fetchLinesIterator, |     fetchLinesIterator, | ||||||
|     maybeWithBasicAuth, |     maybeWithBasicAuth, | ||||||
|     topicShortUrl, |     topicShortUrl, | ||||||
|     topicUrl, |     topicUrl, | ||||||
|     topicUrlAuth, |     topicUrlAuth, | ||||||
|     topicUrlJsonPoll, |     topicUrlJsonPoll, | ||||||
|     topicUrlJsonPollWithSince, userStatsUrl |     topicUrlJsonPollWithSince, | ||||||
|  |     userStatsUrl | ||||||
| } from "./utils"; | } from "./utils"; | ||||||
| import userManager from "./UserManager"; | import userManager from "./UserManager"; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicShortUrl} from "./utils"; | import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl} from "./utils"; | ||||||
| import prefs from "./Prefs"; | import prefs from "./Prefs"; | ||||||
| import subscriptionManager from "./SubscriptionManager"; | import subscriptionManager from "./SubscriptionManager"; | ||||||
| import logo from "../img/ntfy.png"; | import logo from "../img/ntfy.png"; | ||||||
|  | @ -18,8 +18,9 @@ class Notifier { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); |         const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); | ||||||
|  |         const displayName = topicDisplayName(subscription); | ||||||
|         const message = formatMessage(notification); |         const message = formatMessage(notification); | ||||||
|         const title = formatTitleWithDefault(notification, shortUrl); |         const title = formatTitleWithDefault(notification, displayName); | ||||||
| 
 | 
 | ||||||
|         // Show notification
 |         // Show notification
 | ||||||
|         console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); |         console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); | ||||||
|  |  | ||||||
|  | @ -133,6 +133,12 @@ class SubscriptionManager { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async setDisplayName(subscriptionId, displayName) { | ||||||
|  |         await db.subscriptions.update(subscriptionId, { | ||||||
|  |             displayName: displayName | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async pruneNotifications(thresholdTimestamp) { |     async pruneNotifications(thresholdTimestamp) { | ||||||
|         await db.notifications |         await db.notifications | ||||||
|             .where("time").below(thresholdTimestamp) |             .where("time").below(thresholdTimestamp) | ||||||
|  |  | ||||||
|  | @ -38,6 +38,15 @@ export const disallowedTopic = (topic) => { | ||||||
|     return config.disallowedTopics.includes(topic); |     return config.disallowedTopics.includes(topic); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export const topicDisplayName = (subscription) => { | ||||||
|  |     if (subscription.displayName) { | ||||||
|  |         return subscription.displayName; | ||||||
|  |     } else if (subscription.baseUrl === window.location.origin) { | ||||||
|  |         return subscription.topic; | ||||||
|  |     } | ||||||
|  |     return topicShortUrl(subscription.baseUrl, subscription.topic); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| // Format emojis (see emoji.js)
 | // Format emojis (see emoji.js)
 | ||||||
| const emojis = {}; | const emojis = {}; | ||||||
| rawEmojis.forEach(emoji => { | rawEmojis.forEach(emoji => { | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import Typography from "@mui/material/Typography"; | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import {useEffect, useRef, useState} from "react"; | import {useEffect, useRef, useState} from "react"; | ||||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||||
| import {formatShortDateTime, shuffle, topicShortUrl} from "../app/utils"; | import {formatShortDateTime, shuffle, topicDisplayName, topicShortUrl} from "../app/utils"; | ||||||
| import {useLocation, useNavigate} from "react-router-dom"; | import {useLocation, useNavigate} from "react-router-dom"; | ||||||
| import ClickAwayListener from '@mui/material/ClickAwayListener'; | import ClickAwayListener from '@mui/material/ClickAwayListener'; | ||||||
| import Grow from '@mui/material/Grow'; | import Grow from '@mui/material/Grow'; | ||||||
|  | @ -24,13 +24,14 @@ import subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import logo from "../img/ntfy.svg"; | import logo from "../img/ntfy.svg"; | ||||||
| import {useTranslation} from "react-i18next"; | import {useTranslation} from "react-i18next"; | ||||||
| import {Portal, Snackbar} from "@mui/material"; | import {Portal, Snackbar} from "@mui/material"; | ||||||
|  | import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog"; | ||||||
| 
 | 
 | ||||||
| const ActionBar = (props) => { | const ActionBar = (props) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const location = useLocation(); |     const location = useLocation(); | ||||||
|     let title = "ntfy"; |     let title = "ntfy"; | ||||||
|     if (props.selected) { |     if (props.selected) { | ||||||
|         title = topicShortUrl(props.selected.baseUrl, props.selected.topic); |         title = topicDisplayName(props.selected); | ||||||
|     } else if (location.pathname === "/settings") { |     } else if (location.pathname === "/settings") { | ||||||
|         title = t("action_bar_settings"); |         title = t("action_bar_settings"); | ||||||
|     } |     } | ||||||
|  | @ -79,6 +80,7 @@ const SettingsIcons = (props) => { | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const [open, setOpen] = useState(false); |     const [open, setOpen] = useState(false); | ||||||
|     const [snackOpen, setSnackOpen] = useState(false); |     const [snackOpen, setSnackOpen] = useState(false); | ||||||
|  |     const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false); | ||||||
|     const anchorRef = useRef(null); |     const anchorRef = useRef(null); | ||||||
|     const subscription = props.subscription; |     const subscription = props.subscription; | ||||||
| 
 | 
 | ||||||
|  | @ -116,6 +118,10 @@ const SettingsIcons = (props) => { | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const handleSubscriptionSettings = async () => { | ||||||
|  |         setSubscriptionSettingsOpen(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const handleSendTestMessage = async () => { |     const handleSendTestMessage = async () => { | ||||||
|         const baseUrl = props.subscription.baseUrl; |         const baseUrl = props.subscription.baseUrl; | ||||||
|         const topic = props.subscription.topic; |         const topic = props.subscription.topic; | ||||||
|  | @ -201,6 +207,7 @@ const SettingsIcons = (props) => { | ||||||
|                         <Paper> |                         <Paper> | ||||||
|                             <ClickAwayListener onClickAway={handleClose}> |                             <ClickAwayListener onClickAway={handleClose}> | ||||||
|                                 <MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}> |                                 <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={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem> | ||||||
|                                     <MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem> |                                     <MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem> | ||||||
|                                     <MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem> |                                     <MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem> | ||||||
|  | @ -218,6 +225,14 @@ const SettingsIcons = (props) => { | ||||||
|                     message={t("message_bar_error_publishing")} |                     message={t("message_bar_error_publishing")} | ||||||
|                 /> |                 /> | ||||||
|             </Portal> |             </Portal> | ||||||
|  |             <Portal> | ||||||
|  |                 <SubscriptionSettingsDialog | ||||||
|  |                     key={`subscriptionSettingsDialog${subscription.id}`} | ||||||
|  |                     open={subscriptionSettingsOpen} | ||||||
|  |                     subscription={subscription} | ||||||
|  |                     onClose={() => setSubscriptionSettingsOpen(false)} | ||||||
|  |                 /> | ||||||
|  |             </Portal> | ||||||
|         </> |         </> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import SubscribeDialog from "./SubscribeDialog"; | ||||||
| import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader} from "@mui/material"; | import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader} from "@mui/material"; | ||||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import {openUrl, topicShortUrl, topicUrl} from "../app/utils"; | import {openUrl, topicDisplayName, topicUrl} from "../app/utils"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import {ConnectionState} from "../app/Connection"; | import {ConnectionState} from "../app/Connection"; | ||||||
| import {useLocation, useNavigate} from "react-router-dom"; | import {useLocation, useNavigate} from "react-router-dom"; | ||||||
|  | @ -173,12 +173,10 @@ const SubscriptionItem = (props) => { | ||||||
|     const icon = (subscription.state === ConnectionState.Connecting) |     const icon = (subscription.state === ConnectionState.Connecting) | ||||||
|         ? <CircularProgress size="24px"/> |         ? <CircularProgress size="24px"/> | ||||||
|         : <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>; |         : <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>; | ||||||
|     const label = (subscription.baseUrl === window.location.origin) |     const displayName = topicDisplayName(subscription); | ||||||
|         ? subscription.topic |  | ||||||
|         : topicShortUrl(subscription.baseUrl, subscription.topic); |  | ||||||
|     const ariaLabel = (subscription.state === ConnectionState.Connecting) |     const ariaLabel = (subscription.state === ConnectionState.Connecting) | ||||||
|         ? `${label} (${t("nav_button_connecting")})` |         ? `${displayName} (${t("nav_button_connecting")})` | ||||||
|         : label; |         : displayName; | ||||||
|     const handleClick = async () => { |     const handleClick = async () => { | ||||||
|         navigate(routes.forSubscription(subscription)); |         navigate(routes.forSubscription(subscription)); | ||||||
|         await subscriptionManager.markNotificationsRead(subscription.id); |         await subscriptionManager.markNotificationsRead(subscription.id); | ||||||
|  | @ -186,7 +184,7 @@ const SubscriptionItem = (props) => { | ||||||
|     return ( |     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> |             <ListItemIcon>{icon}</ListItemIcon> | ||||||
|             <ListItemText primary={label}/> |             <ListItemText primary={displayName}/> | ||||||
|             {subscription.mutedUntil > 0 && |             {subscription.mutedUntil > 0 && | ||||||
|                 <ListItemIcon edge="end" aria-label={t("nav_button_muted")}><NotificationsOffOutlined /></ListItemIcon>} |                 <ListItemIcon edge="end" aria-label={t("nav_button_muted")}><NotificationsOffOutlined /></ListItemIcon>} | ||||||
|         </ListItemButton> |         </ListItemButton> | ||||||
|  |  | ||||||
							
								
								
									
										59
									
								
								web/src/components/SubscriptionSettingsDialog.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								web/src/components/SubscriptionSettingsDialog.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | ||||||
|  | import * as React from 'react'; | ||||||
|  | import {useState} from 'react'; | ||||||
|  | import Button from '@mui/material/Button'; | ||||||
|  | import TextField from '@mui/material/TextField'; | ||||||
|  | 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, useMediaQuery} from "@mui/material"; | ||||||
|  | import theme from "./theme"; | ||||||
|  | import api from "../app/Api"; | ||||||
|  | import {topicUrl, validTopic, validUrl} from "../app/utils"; | ||||||
|  | import userManager from "../app/UserManager"; | ||||||
|  | import subscriptionManager from "../app/SubscriptionManager"; | ||||||
|  | import poller from "../app/Poller"; | ||||||
|  | import DialogFooter from "./DialogFooter"; | ||||||
|  | import {useTranslation} from "react-i18next"; | ||||||
|  | 
 | ||||||
|  | const SubscriptionSettingsDialog = (props) => { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const subscription = props.subscription; | ||||||
|  |     const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); | ||||||
|  |     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||||
|  |     const handleSave = async () => { | ||||||
|  |         await subscriptionManager.setDisplayName(subscription.id, displayName); | ||||||
|  |         props.onClose(); | ||||||
|  |     } | ||||||
|  |     return ( | ||||||
|  |         <Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}> | ||||||
|  |             <DialogTitle>{t("subscription_settings_dialog_title")}</DialogTitle> | ||||||
|  |             <DialogContent> | ||||||
|  |                 <DialogContentText> | ||||||
|  |                     {t("subscription_settings_dialog_description")} | ||||||
|  |                 </DialogContentText> | ||||||
|  |                 <TextField | ||||||
|  |                     autoFocus | ||||||
|  |                     margin="dense" | ||||||
|  |                     id="topic" | ||||||
|  |                     placeholder={t("subscription_settings_dialog_display_name_placeholder")} | ||||||
|  |                     value={displayName} | ||||||
|  |                     onChange={ev => setDisplayName(ev.target.value)} | ||||||
|  |                     type="text" | ||||||
|  |                     fullWidth | ||||||
|  |                     variant="standard" | ||||||
|  |                     inputProps={{ | ||||||
|  |                         maxLength: 64, | ||||||
|  |                         "aria-label": t("subscription_settings_dialog_display_name_placeholder") | ||||||
|  |                     }} | ||||||
|  |                 /> | ||||||
|  |             </DialogContent> | ||||||
|  |             <DialogFooter> | ||||||
|  |                 <Button onClick={props.onClose}>{t("subscription_settings_button_cancel")}</Button> | ||||||
|  |                 <Button onClick={handleSave}>{t("subscription_settings_button_save")}</Button> | ||||||
|  |             </DialogFooter> | ||||||
|  |         </Dialog> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default SubscriptionSettingsDialog; | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue