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

WIP: DIsplay name for the web app
This commit is contained in:
Philipp C. Heckel 2022-06-29 19:35:23 -04:00 committed by GitHub
commit bd6f3ca2e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 15 deletions

View File

@ -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)
@ -28,6 +28,10 @@ Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up s
## ntfy server v1.28.0 (UNRELEASED) ## ntfy server v1.28.0 (UNRELEASED)
**Features:**
* Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348))
**Bugs:** **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) * `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)

View File

@ -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",

View File

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

View File

@ -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}`);

View File

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

View File

@ -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 => {

View File

@ -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>
</> </>
); );
}; };

View File

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

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