Make web push toggle global
parent
a8db08c7d4
commit
46798ac322
|
@ -95,6 +95,7 @@
|
|||
"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>.",
|
||||
"notification_toggle_mute": "Mute",
|
||||
"notification_toggle_unmute": "Unmute",
|
||||
"notification_toggle_background": "Background notifications",
|
||||
"display_name_dialog_title": "Change display name",
|
||||
|
@ -369,6 +370,10 @@
|
|||
"prefs_reservations_dialog_description": "Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
||||
"prefs_reservations_dialog_topic_label": "Topic",
|
||||
"prefs_reservations_dialog_access_label": "Access",
|
||||
"prefs_notifications_web_push_title": "Enable web push notifications",
|
||||
"prefs_notifications_web_push_description": "Enable this to receive notifications in the background even when ntfy isn't running",
|
||||
"prefs_notifications_web_push_enabled": "Enabled",
|
||||
"prefs_notifications_web_push_disabled": "Disabled",
|
||||
"reservation_delete_dialog_description": "Removing a reservation gives up ownership over the topic, and allows others to reserve it. You can keep, or delete existing messages and attachments.",
|
||||
"reservation_delete_dialog_action_keep_title": "Keep cached messages and attachments",
|
||||
"reservation_delete_dialog_action_keep_description": "Messages and attachments that are cached on the server will become publicly visible for people with knowledge of the topic name.",
|
||||
|
|
|
@ -45,15 +45,11 @@ class ConnectionManager {
|
|||
return;
|
||||
}
|
||||
console.log(`[ConnectionManager] Refreshing connections`);
|
||||
const subscriptionsWithUsersAndConnectionId = subscriptions
|
||||
.map((s) => {
|
||||
const [user] = users.filter((u) => u.baseUrl === s.baseUrl);
|
||||
const connectionId = makeConnectionId(s, user);
|
||||
return { ...s, user, connectionId };
|
||||
})
|
||||
// background notifications don't need this as they come over web push.
|
||||
// however, if they are muted, we again need the ws while the page is active
|
||||
.filter((s) => !s.webPushEnabled && s.mutedUntil !== 1);
|
||||
const subscriptionsWithUsersAndConnectionId = subscriptions.map((s) => {
|
||||
const [user] = users.filter((u) => u.baseUrl === s.baseUrl);
|
||||
const connectionId = makeConnectionId(s, user);
|
||||
return { ...s, user, connectionId };
|
||||
});
|
||||
|
||||
console.log();
|
||||
const targetIds = subscriptionsWithUsersAndConnectionId.map((s) => s.connectionId);
|
||||
|
|
|
@ -114,6 +114,11 @@ class Notifier {
|
|||
return this.pushSupported() && this.contextSupported() && this.granted() && !this.iosSupportedButInstallRequired();
|
||||
}
|
||||
|
||||
async pushEnabled() {
|
||||
const enabled = await prefs.webPushEnabled();
|
||||
return this.pushPossible() && enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API
|
||||
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
|
||||
|
|
|
@ -31,6 +31,15 @@ class Prefs {
|
|||
const deleteAfter = await this.db.prefs.get("deleteAfter");
|
||||
return deleteAfter ? Number(deleteAfter.value) : 604800; // Default is one week
|
||||
}
|
||||
|
||||
async webPushEnabled() {
|
||||
const obj = await this.db.prefs.get("webPushEnabled");
|
||||
return obj?.value ?? false;
|
||||
}
|
||||
|
||||
async setWebPushEnabled(enabled) {
|
||||
await this.db.prefs.put({ key: "webPushEnabled", value: enabled });
|
||||
}
|
||||
}
|
||||
|
||||
const prefs = new Prefs(getDb());
|
||||
|
|
|
@ -21,8 +21,16 @@ class SubscriptionManager {
|
|||
}
|
||||
|
||||
async webPushTopics() {
|
||||
const subscriptions = await this.db.subscriptions.where({ webPushEnabled: 1, mutedUntil: 0 }).toArray();
|
||||
return subscriptions.map(({ topic }) => topic);
|
||||
// the Promise.resolve wrapper is not superfluous, without it the live query breaks:
|
||||
// https://dexie.org/docs/dexie-react-hooks/useLiveQuery()#calling-non-dexie-apis-from-querier
|
||||
if (!(await Promise.resolve(notifier.pushEnabled()))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const subscriptions = await this.db.subscriptions.where({ mutedUntil: 0, baseUrl: config.base_url }).toArray();
|
||||
|
||||
// internal is currently a bool, it could be a 0/1 to be indexable, but for now just filter them out here
|
||||
return subscriptions.filter(({ internal }) => !internal).map(({ topic }) => topic);
|
||||
}
|
||||
|
||||
async get(subscriptionId) {
|
||||
|
@ -49,7 +57,6 @@ class SubscriptionManager {
|
|||
* @param {string} topic
|
||||
* @param {object} opts
|
||||
* @param {boolean} opts.internal
|
||||
* @param {boolean} opts.webPushEnabled
|
||||
* @returns
|
||||
*/
|
||||
async add(baseUrl, topic, opts = {}) {
|
||||
|
@ -67,7 +74,6 @@ class SubscriptionManager {
|
|||
topic,
|
||||
mutedUntil: 0,
|
||||
last: null,
|
||||
webPushEnabled: opts.webPushEnabled ? 1 : 0,
|
||||
};
|
||||
|
||||
await this.db.subscriptions.put(subscription);
|
||||
|
@ -211,12 +217,6 @@ class SubscriptionManager {
|
|||
});
|
||||
}
|
||||
|
||||
async toggleBackgroundNotifications(subscription) {
|
||||
await this.db.subscriptions.update(subscription.id, {
|
||||
webPushEnabled: subscription.webPushEnabled === 1 ? 0 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
async setDisplayName(subscriptionId, displayName) {
|
||||
await this.db.subscriptions.update(subscriptionId, {
|
||||
displayName,
|
||||
|
|
|
@ -14,7 +14,7 @@ const getDbBase = (username) => {
|
|||
const db = new Dexie(dbName);
|
||||
|
||||
db.version(2).stores({
|
||||
subscriptions: "&id,baseUrl,[webPushEnabled+mutedUntil]",
|
||||
subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]",
|
||||
notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance
|
||||
users: "&baseUrl,username",
|
||||
prefs: "&key",
|
||||
|
|
|
@ -69,6 +69,16 @@ const Layout = () => {
|
|||
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
|
||||
const users = useLiveQuery(() => userManager.all());
|
||||
const subscriptions = useLiveQuery(() => subscriptionManager.all());
|
||||
const webPushTopics = useLiveQuery(() => subscriptionManager.webPushTopics());
|
||||
|
||||
const websocketSubscriptions = useMemo(
|
||||
() => (subscriptions && webPushTopics ? subscriptions.filter((s) => !webPushTopics.includes(s.topic)) : []),
|
||||
// websocketSubscriptions should stay stable unless the list of subscription ids changes.
|
||||
// without the memoization, the connection listener calls a refresh for no reason.
|
||||
// this isn't a problem due to the makeConnectionId, but it triggers an
|
||||
// unnecessary recomputation for every received message.
|
||||
[JSON.stringify({ subscriptions: subscriptions?.map(({ id }) => id), webPushTopics })]
|
||||
);
|
||||
const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal);
|
||||
const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0;
|
||||
const [selected] = (subscriptionsWithoutInternal || []).filter(
|
||||
|
@ -77,7 +87,7 @@ const Layout = () => {
|
|||
(config.base_url === s.baseUrl && params.topic === s.topic)
|
||||
);
|
||||
|
||||
useConnectionListeners(account, subscriptions, users);
|
||||
useConnectionListeners(account, websocketSubscriptions, users);
|
||||
useAccountListener(setAccount);
|
||||
useBackgroundProcesses();
|
||||
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
|
||||
|
|
|
@ -48,6 +48,7 @@ import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite
|
|||
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import { subscribeTopic } from "./SubscribeDialog";
|
||||
import notifier from "../app/Notifier";
|
||||
|
||||
const maybeUpdateAccountSettings = async (payload) => {
|
||||
if (!session.exists()) {
|
||||
|
@ -85,6 +86,7 @@ const Notifications = () => {
|
|||
<Sound />
|
||||
<MinPriority />
|
||||
<DeleteAfter />
|
||||
<WebPushEnabled />
|
||||
</PrefGroup>
|
||||
</Card>
|
||||
);
|
||||
|
@ -232,6 +234,35 @@ const DeleteAfter = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const WebPushEnabled = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefWebPushEnabled";
|
||||
const defaultEnabled = useLiveQuery(async () => prefs.webPushEnabled());
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setWebPushEnabled(ev.target.value);
|
||||
};
|
||||
|
||||
// while loading
|
||||
if (defaultEnabled == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!notifier.pushPossible()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Pref labelId={labelId} title={t("prefs_notifications_web_push_title")} description={t("prefs_notifications_web_push_description")}>
|
||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||
<Select value={defaultEnabled} onChange={handleChange} aria-labelledby={labelId}>
|
||||
<MenuItem value>{t("prefs_notifications_web_push_enabled")}</MenuItem>
|
||||
<MenuItem value={false}>{t("prefs_notifications_web_push_disabled")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Pref>
|
||||
);
|
||||
};
|
||||
|
||||
const Users = () => {
|
||||
const { t } = useTranslation();
|
||||
const [dialogKey, setDialogKey] = useState(0);
|
||||
|
|
|
@ -28,7 +28,6 @@ import ReserveTopicSelect from "./ReserveTopicSelect";
|
|||
import { AccountContext } from "./App";
|
||||
import { TopicReservedError, UnauthorizedError } from "../app/errors";
|
||||
import { ReserveLimitChip } from "./SubscriptionPopup";
|
||||
import notifier from "../app/Notifier";
|
||||
|
||||
const publicBaseUrl = "https://ntfy.sh";
|
||||
|
||||
|
@ -53,12 +52,10 @@ const SubscribeDialog = (props) => {
|
|||
const [showLoginPage, setShowLoginPage] = useState(false);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const handleSuccess = async (webPushEnabled) => {
|
||||
const handleSuccess = async () => {
|
||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
|
||||
const actualBaseUrl = baseUrl || config.base_url;
|
||||
const subscription = await subscribeTopic(actualBaseUrl, topic, {
|
||||
webPushEnabled,
|
||||
});
|
||||
const subscription = await subscribeTopic(actualBaseUrl, topic, {});
|
||||
poller.pollInBackground(subscription); // Dangle!
|
||||
props.onSuccess(subscription);
|
||||
};
|
||||
|
@ -99,12 +96,6 @@ const SubscribePage = (props) => {
|
|||
const reserveTopicEnabled =
|
||||
session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0));
|
||||
|
||||
const [backgroundNotificationsEnabled, setBackgroundNotificationsEnabled] = useState(false);
|
||||
|
||||
const handleBackgroundNotificationsChanged = (e) => {
|
||||
setBackgroundNotificationsEnabled(e.target.checked);
|
||||
};
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
const user = await userManager.get(baseUrl); // May be undefined
|
||||
const username = user ? user.username : t("subscribe_dialog_error_user_anonymous");
|
||||
|
@ -142,15 +133,12 @@ const SubscribePage = (props) => {
|
|||
}
|
||||
|
||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
||||
props.onSuccess(backgroundNotificationsEnabled);
|
||||
props.onSuccess();
|
||||
};
|
||||
|
||||
const handleUseAnotherChanged = (e) => {
|
||||
props.setBaseUrl("");
|
||||
setAnotherServerVisible(e.target.checked);
|
||||
if (e.target.checked) {
|
||||
setBackgroundNotificationsEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const subscribeButtonEnabled = (() => {
|
||||
|
@ -256,22 +244,6 @@ const SubscribePage = (props) => {
|
|||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
{notifier.pushPossible() && !anotherServerVisible && (
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={handleBackgroundNotificationsChanged}
|
||||
checked={backgroundNotificationsEnabled}
|
||||
inputProps={{
|
||||
"aria-label": t("subscribe_dialog_subscribe_enable_background_notifications_label"),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={t("subscribe_dialog_subscribe_enable_background_notifications_label")}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
|
||||
|
|
|
@ -15,19 +15,17 @@ import {
|
|||
MenuItem,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Check,
|
||||
Clear,
|
||||
ClearAll,
|
||||
Edit,
|
||||
EnhancedEncryption,
|
||||
Lock,
|
||||
LockOpen,
|
||||
Notifications,
|
||||
NotificationsOff,
|
||||
RemoveCircle,
|
||||
Send,
|
||||
|
@ -44,7 +42,6 @@ import api from "../app/Api";
|
|||
import { AccountContext } from "./App";
|
||||
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import notifier from "../app/Notifier";
|
||||
|
||||
export const SubscriptionPopup = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -169,8 +166,8 @@ export const SubscriptionPopup = (props) => {
|
|||
return (
|
||||
<>
|
||||
<PopupMenu horizontal={placement} anchorEl={props.anchor} open={!!props.anchor} onClose={props.onClose}>
|
||||
{notifier.pushPossible() && <NotificationToggle subscription={subscription} />}
|
||||
<Divider />
|
||||
<NotificationToggle subscription={subscription} />
|
||||
|
||||
<MenuItem onClick={handleChangeDisplayName}>
|
||||
<ListItemIcon>
|
||||
<Edit fontSize="small" />
|
||||
|
@ -334,44 +331,27 @@ const DisplayNameDialog = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const checkedItem = (
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
);
|
||||
|
||||
const NotificationToggle = ({ subscription }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleToggleBackground = async () => {
|
||||
try {
|
||||
await subscriptionManager.toggleBackgroundNotifications(subscription);
|
||||
} catch (e) {
|
||||
console.error("[NotificationToggle] Error setting notification type", e);
|
||||
}
|
||||
const handleToggleMute = async () => {
|
||||
const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future
|
||||
await subscriptionManager.setMutedUntil(subscription.id, mutedUntil);
|
||||
};
|
||||
|
||||
const unmute = async () => {
|
||||
await subscriptionManager.setMutedUntil(subscription.id, 0);
|
||||
};
|
||||
|
||||
if (subscription.mutedUntil === 1) {
|
||||
return (
|
||||
<MenuItem onClick={unmute}>
|
||||
<ListItemIcon>
|
||||
<NotificationsOff />
|
||||
</ListItemIcon>
|
||||
{t("notification_toggle_unmute")}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem>
|
||||
{subscription.webPushEnabled === 1 && checkedItem}
|
||||
<ListItemText inset={subscription.webPushEnabled !== 1} onClick={handleToggleBackground}>
|
||||
{t("notification_toggle_background")}
|
||||
</ListItemText>
|
||||
return subscription.mutedUntil ? (
|
||||
<MenuItem onClick={handleToggleMute}>
|
||||
<ListItemIcon>
|
||||
<Notifications />
|
||||
</ListItemIcon>
|
||||
{t("notification_toggle_unmute")}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={handleToggleMute}>
|
||||
<ListItemIcon>
|
||||
<NotificationsOff />
|
||||
</ListItemIcon>
|
||||
{t("notification_toggle_mute")}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue