Merge pull request #348 from binwiederhier/display-name-web
WIP: DIsplay name for the web app
This commit is contained in:
commit
bd6f3ca2e8
9 changed files with 112 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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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…
Reference in a new issue