Просмотр исходного кода

Merge pull request #348 from binwiederhier/display-name-web

WIP: DIsplay name for the web app
Philipp C. Heckel 3 лет назад
Родитель
Сommit
bd6f3ca2e8

+ 5 - 1
docs/releases.md

@@ -14,7 +14,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 **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))
 * 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)
 
@@ -28,6 +28,10 @@ Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up s
 
 ## ntfy server v1.28.0 (UNRELEASED)
 
+**Features:**
+
+* Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348))
+
 **Bugs:**
 
 * `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting)

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

@@ -2,6 +2,7 @@
   "action_bar_show_menu": "Show menu",
   "action_bar_logo_alt": "ntfy logo",
   "action_bar_settings": "Settings",
+  "action_bar_subscription_settings": "Subscription settings",
   "action_bar_send_test_notification": "Send test notification",
   "action_bar_clear_notifications": "Clear all notifications",
   "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_example": "Example",
   "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 …",
   "publish_dialog_title_topic": "Publish to {{topic}}",
   "publish_dialog_title_no_topic": "Publish notification",

+ 2 - 3
web/src/app/Api.js

@@ -1,13 +1,12 @@
 import {
-    basicAuth,
-    encodeBase64,
     fetchLinesIterator,
     maybeWithBasicAuth,
     topicShortUrl,
     topicUrl,
     topicUrlAuth,
     topicUrlJsonPoll,
-    topicUrlJsonPollWithSince, userStatsUrl
+    topicUrlJsonPollWithSince,
+    userStatsUrl
 } from "./utils";
 import userManager from "./UserManager";
 

+ 3 - 2
web/src/app/Notifier.js

@@ -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 subscriptionManager from "./SubscriptionManager";
 import logo from "../img/ntfy.png";
@@ -18,8 +18,9 @@ class Notifier {
             return;
         }
         const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
+        const displayName = topicDisplayName(subscription);
         const message = formatMessage(notification);
-        const title = formatTitleWithDefault(notification, shortUrl);
+        const title = formatTitleWithDefault(notification, displayName);
 
         // Show notification
         console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);

+ 6 - 0
web/src/app/SubscriptionManager.js

@@ -133,6 +133,12 @@ class SubscriptionManager {
         });
     }
 
+    async setDisplayName(subscriptionId, displayName) {
+        await db.subscriptions.update(subscriptionId, {
+            displayName: displayName
+        });
+    }
+
     async pruneNotifications(thresholdTimestamp) {
         await db.notifications
             .where("time").below(thresholdTimestamp)

+ 9 - 0
web/src/app/utils.js

@@ -38,6 +38,15 @@ export const disallowedTopic = (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)
 const emojis = {};
 rawEmojis.forEach(emoji => {

+ 17 - 2
web/src/components/ActionBar.js

@@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography";
 import * as React from "react";
 import {useEffect, useRef, useState} from "react";
 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 ClickAwayListener from '@mui/material/ClickAwayListener';
 import Grow from '@mui/material/Grow';
@@ -24,13 +24,14 @@ import subscriptionManager from "../app/SubscriptionManager";
 import logo from "../img/ntfy.svg";
 import {useTranslation} from "react-i18next";
 import {Portal, Snackbar} from "@mui/material";
+import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog";
 
 const ActionBar = (props) => {
     const { t } = useTranslation();
     const location = useLocation();
     let title = "ntfy";
     if (props.selected) {
-        title = topicShortUrl(props.selected.baseUrl, props.selected.topic);
+        title = topicDisplayName(props.selected);
     } else if (location.pathname === "/settings") {
         title = t("action_bar_settings");
     }
@@ -79,6 +80,7 @@ const SettingsIcons = (props) => {
     const navigate = useNavigate();
     const [open, setOpen] = useState(false);
     const [snackOpen, setSnackOpen] = useState(false);
+    const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false);
     const anchorRef = useRef(null);
     const subscription = props.subscription;
 
@@ -116,6 +118,10 @@ const SettingsIcons = (props) => {
         }
     };
 
+    const handleSubscriptionSettings = async () => {
+        setSubscriptionSettingsOpen(true);
+    }
+
     const handleSendTestMessage = async () => {
         const baseUrl = props.subscription.baseUrl;
         const topic = props.subscription.topic;
@@ -201,6 +207,7 @@ const SettingsIcons = (props) => {
                         <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>
@@ -218,6 +225,14 @@ const SettingsIcons = (props) => {
                     message={t("message_bar_error_publishing")}
                 />
             </Portal>
+            <Portal>
+                <SubscriptionSettingsDialog
+                    key={`subscriptionSettingsDialog${subscription.id}`}
+                    open={subscriptionSettingsOpen}
+                    subscription={subscription}
+                    onClose={() => setSubscriptionSettingsOpen(false)}
+                />
+            </Portal>
         </>
     );
 };

+ 5 - 7
web/src/components/Navigation.js

@@ -14,7 +14,7 @@ import SubscribeDialog from "./SubscribeDialog";
 import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader} from "@mui/material";
 import Button from "@mui/material/Button";
 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 {ConnectionState} from "../app/Connection";
 import {useLocation, useNavigate} from "react-router-dom";
@@ -173,12 +173,10 @@ const SubscriptionItem = (props) => {
     const icon = (subscription.state === ConnectionState.Connecting)
         ? <CircularProgress size="24px"/>
         : <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
-    const label = (subscription.baseUrl === window.location.origin)
-        ? subscription.topic
-        : topicShortUrl(subscription.baseUrl, subscription.topic);
+    const displayName = topicDisplayName(subscription);
     const ariaLabel = (subscription.state === ConnectionState.Connecting)
-        ? `${label} (${t("nav_button_connecting")})`
-        : label;
+        ? `${displayName} (${t("nav_button_connecting")})`
+        : displayName;
     const handleClick = async () => {
         navigate(routes.forSubscription(subscription));
         await subscriptionManager.markNotificationsRead(subscription.id);
@@ -186,7 +184,7 @@ const SubscriptionItem = (props) => {
     return (
         <ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite">
             <ListItemIcon>{icon}</ListItemIcon>
-            <ListItemText primary={label}/>
+            <ListItemText primary={displayName}/>
             {subscription.mutedUntil > 0 &&
                 <ListItemIcon edge="end" aria-label={t("nav_button_muted")}><NotificationsOffOutlined /></ListItemIcon>}
         </ListItemButton>

+ 59 - 0
web/src/components/SubscriptionSettingsDialog.js

@@ -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;