Format emojis in the service worker directly
parent
44913c1668
commit
4648f83669
|
@ -261,8 +261,11 @@ Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-ser
|
||||||
--web-push-subscriptions-file=/tmp/subscriptions.db
|
--web-push-subscriptions-file=/tmp/subscriptions.db
|
||||||
```
|
```
|
||||||
|
|
||||||
3. In `web/public/config.js` set `base_url` to `http://localhost`. This is required as web push can only be used
|
3. In `web/public/config.js`:
|
||||||
with the server matching the `base_url`
|
|
||||||
|
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
|
||||||
|
|
||||||
|
- Set the `web_push_public_key` correctly.
|
||||||
|
|
||||||
4. Run `ENABLE_DEV_PWA=1 npm run start` - this enables the dev service worker
|
4. Run `ENABLE_DEV_PWA=1 npm run start` - this enables the dev service worker
|
||||||
|
|
||||||
|
@ -270,7 +273,7 @@ Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-ser
|
||||||
|
|
||||||
- Chrome:
|
- Chrome:
|
||||||
|
|
||||||
Open Chrome with special flags allowing insecure localhost service worker testing (regularly dismissing SSL warnings is not enough)
|
Open Chrome with special flags allowing insecure localhost service worker testing insecurely
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# for example, macOS
|
# for example, macOS
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/SherClockHolmes/webpush-go"
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
"heckel.io/ntfy/log"
|
"heckel.io/ntfy/log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleTopicWebPushSubscribe(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleTopicWebPushSubscribe(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
@ -55,27 +54,6 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := log.Context{"topic": m.Topic, "message_id": m.ID, "total_count": len(subscriptions)}
|
|
||||||
|
|
||||||
// Importing the emojis in the service worker would add unnecessary complexity,
|
|
||||||
// simply do it here for web push notifications instead
|
|
||||||
var titleWithDefault, formattedTitle string
|
|
||||||
emojis, _, err := toEmojis(m.Tags)
|
|
||||||
if err != nil {
|
|
||||||
logvm(v, m).Err(err).Fields(ctx).Debug("Unable to publish web push message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if m.Title == "" {
|
|
||||||
titleWithDefault = m.Topic
|
|
||||||
} else {
|
|
||||||
titleWithDefault = m.Title
|
|
||||||
}
|
|
||||||
if len(emojis) > 0 {
|
|
||||||
formattedTitle = fmt.Sprintf("%s %s", strings.Join(emojis[:], " "), titleWithDefault)
|
|
||||||
} else {
|
|
||||||
formattedTitle = titleWithDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, xi := range subscriptions {
|
for i, xi := range subscriptions {
|
||||||
go func(i int, sub webPushSubscription) {
|
go func(i int, sub webPushSubscription) {
|
||||||
ctx := log.Context{"endpoint": sub.BrowserSubscription.Endpoint, "username": sub.UserID, "topic": m.Topic, "message_id": m.ID}
|
ctx := log.Context{"endpoint": sub.BrowserSubscription.Endpoint, "username": sub.UserID, "topic": m.Topic, "message_id": m.ID}
|
||||||
|
@ -83,7 +61,6 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||||
payload := &webPushPayload{
|
payload := &webPushPayload{
|
||||||
SubscriptionID: fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic),
|
SubscriptionID: fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic),
|
||||||
Message: *m,
|
Message: *m,
|
||||||
FormattedTitle: formattedTitle,
|
|
||||||
}
|
}
|
||||||
jsonPayload, err := json.Marshal(payload)
|
jsonPayload, err := json.Marshal(payload)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "embed" // required by go:embed
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
|
@ -128,3 +130,25 @@ This message was sent by {ip} at {time} via {topicURL}`
|
||||||
body = strings.ReplaceAll(body, "{ip}", senderIP)
|
body = strings.ReplaceAll(body, "{ip}", senderIP)
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed "mailer_emoji_map.json"
|
||||||
|
emojisJSON string
|
||||||
|
)
|
||||||
|
|
||||||
|
func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
|
||||||
|
var emojiMap map[string]string
|
||||||
|
if err = json.Unmarshal([]byte(emojisJSON), &emojiMap); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tagsOut = make([]string, 0)
|
||||||
|
emojisOut = make([]string, 0)
|
||||||
|
for _, t := range tags {
|
||||||
|
if emoji, ok := emojiMap[t]; ok {
|
||||||
|
emojisOut = append(emojisOut, emoji)
|
||||||
|
} else {
|
||||||
|
tagsOut = append(tagsOut, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -469,7 +469,6 @@ type apiStripeSubscriptionDeletedEvent struct {
|
||||||
type webPushPayload struct {
|
type webPushPayload struct {
|
||||||
SubscriptionID string `json:"subscription_id"`
|
SubscriptionID string `json:"subscription_id"`
|
||||||
Message message `json:"message"`
|
Message message `json:"message"`
|
||||||
FormattedTitle string `json:"formatted_title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type webPushSubscription struct {
|
type webPushSubscription struct {
|
||||||
|
|
|
@ -2,8 +2,6 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed" // required by go:embed
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
|
@ -135,25 +133,3 @@ func maybeDecodeHeader(header string) string {
|
||||||
}
|
}
|
||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed "mailer_emoji_map.json"
|
|
||||||
emojisJSON string
|
|
||||||
)
|
|
||||||
|
|
||||||
func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
|
|
||||||
var emojiMap map[string]string
|
|
||||||
if err = json.Unmarshal([]byte(emojisJSON), &emojiMap); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
tagsOut = make([]string, 0)
|
|
||||||
emojisOut = make([]string, 0)
|
|
||||||
for _, t := range tags {
|
|
||||||
if emoji, ok := emojiMap[t]; ok {
|
|
||||||
emojisOut = append(emojisOut, emoji)
|
|
||||||
} else {
|
|
||||||
tagsOut = append(tagsOut, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
|
||||||
import { NetworkFirst } from "workbox-strategies";
|
import { NetworkFirst } from "workbox-strategies";
|
||||||
|
|
||||||
import { getDbAsync } from "../src/app/getDb";
|
import { getDbAsync } from "../src/app/getDb";
|
||||||
|
import { formatMessage, formatTitleWithDefault } from "../src/app/notificationUtils";
|
||||||
|
|
||||||
// See WebPushWorker, this is to play a sound on supported browsers,
|
// See WebPushWorker, this is to play a sound on supported browsers,
|
||||||
// if the app is in the foreground
|
// if the app is in the foreground
|
||||||
|
@ -27,11 +28,11 @@ self.addEventListener("pushsubscriptionchange", (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("push", (event) => {
|
self.addEventListener("push", (event) => {
|
||||||
console.log("[ServiceWorker] Received Web Push Event", { event });
|
|
||||||
// server/types.go webPushPayload
|
// server/types.go webPushPayload
|
||||||
const data = event.data.json();
|
const data = event.data.json();
|
||||||
|
console.log("[ServiceWorker] Received Web Push Event", { event, data });
|
||||||
|
|
||||||
const { formatted_title: formattedTitle, subscription_id: subscriptionId, message } = data;
|
const { subscription_id: subscriptionId, message } = data;
|
||||||
broadcastChannel.postMessage(message);
|
broadcastChannel.postMessage(message);
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
|
@ -53,9 +54,9 @@ self.addEventListener("push", (event) => {
|
||||||
db.subscriptions.update(subscriptionId, {
|
db.subscriptions.update(subscriptionId, {
|
||||||
last: message.id,
|
last: message.id,
|
||||||
}),
|
}),
|
||||||
self.registration.showNotification(formattedTitle, {
|
self.registration.showNotification(formatTitleWithDefault(message, message.topic), {
|
||||||
tag: subscriptionId,
|
tag: subscriptionId,
|
||||||
body: message.message,
|
body: formatMessage(message),
|
||||||
icon: "/static/images/ntfy.png",
|
icon: "/static/images/ntfy.png",
|
||||||
data,
|
data,
|
||||||
}),
|
}),
|
||||||
|
@ -106,6 +107,7 @@ precacheAndRoute(self.__WB_MANIFEST);
|
||||||
cleanupOutdatedCaches();
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
// to allow work offline
|
// to allow work offline
|
||||||
registerRoute(new NavigationRoute(createHandlerBoundToURL("/")));
|
if (import.meta.env.MODE !== "development") {
|
||||||
|
registerRoute(new NavigationRoute(createHandlerBoundToURL("/")));
|
||||||
registerRoute(({ url }) => url.pathname === "/config.js", new NetworkFirst());
|
registerRoute(({ url }) => url.pathname === "/config.js", new NetworkFirst());
|
||||||
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ class Api {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: maybeWithAuth({}, user),
|
headers: maybeWithAuth({}, user),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
endpoint: subscription.webPushEndpoint
|
endpoint: subscription.webPushEndpoint,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
import { openUrl, playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
||||||
|
import { formatMessage, formatTitleWithDefault } from "./notificationUtils";
|
||||||
import prefs from "./Prefs";
|
import prefs from "./Prefs";
|
||||||
import logo from "../img/ntfy.png";
|
import logo from "../img/ntfy.png";
|
||||||
import api from "./Api";
|
import api from "./Api";
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { rawEmojis } from "./emojis";
|
||||||
|
|
||||||
|
// Format emojis (see emoji.js)
|
||||||
|
export default Object.fromEntries(rawEmojis.flatMap((emoji) => emoji.aliases.map((alias) => [alias, emoji.emoji])));
|
|
@ -0,0 +1,35 @@
|
||||||
|
// This is a separate file since the other utils import `config.js`, which depends on `window`
|
||||||
|
// and cannot be used in the service worker
|
||||||
|
|
||||||
|
import emojisMapped from "./emojisMapped";
|
||||||
|
|
||||||
|
const toEmojis = (tags) => {
|
||||||
|
if (!tags) return [];
|
||||||
|
return tags.filter((tag) => tag in emojisMapped).map((tag) => emojisMapped[tag]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTitle = (m) => {
|
||||||
|
const emojiList = toEmojis(m.tags);
|
||||||
|
if (emojiList.length > 0) {
|
||||||
|
return `${emojiList.join(" ")} ${m.title}`;
|
||||||
|
}
|
||||||
|
return m.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTitleWithDefault = (m, fallback) => {
|
||||||
|
if (m.title) {
|
||||||
|
return formatTitle(m);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatMessage = (m) => {
|
||||||
|
if (m.title) {
|
||||||
|
return m.message;
|
||||||
|
}
|
||||||
|
const emojiList = toEmojis(m.tags);
|
||||||
|
if (emojiList.length > 0) {
|
||||||
|
return `${emojiList.join(" ")} ${m.message}`;
|
||||||
|
}
|
||||||
|
return m.message;
|
||||||
|
};
|
|
@ -1,5 +1,4 @@
|
||||||
import { Base64 } from "js-base64";
|
import { Base64 } from "js-base64";
|
||||||
import { rawEmojis } from "./emojis";
|
|
||||||
import beep from "../sounds/beep.mp3";
|
import beep from "../sounds/beep.mp3";
|
||||||
import juntos from "../sounds/juntos.mp3";
|
import juntos from "../sounds/juntos.mp3";
|
||||||
import pristine from "../sounds/pristine.mp3";
|
import pristine from "../sounds/pristine.mp3";
|
||||||
|
@ -8,6 +7,7 @@ import dadum from "../sounds/dadum.mp3";
|
||||||
import pop from "../sounds/pop.mp3";
|
import pop from "../sounds/pop.mp3";
|
||||||
import popSwoosh from "../sounds/pop-swoosh.mp3";
|
import popSwoosh from "../sounds/pop-swoosh.mp3";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
|
import emojisMapped from "./emojisMapped";
|
||||||
|
|
||||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
|
@ -56,48 +56,9 @@ export const topicDisplayName = (subscription) => {
|
||||||
return topicShortUrl(subscription.baseUrl, subscription.topic);
|
return topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format emojis (see emoji.js)
|
|
||||||
const emojis = {};
|
|
||||||
rawEmojis.forEach((emoji) => {
|
|
||||||
emoji.aliases.forEach((alias) => {
|
|
||||||
emojis[alias] = emoji.emoji;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const toEmojis = (tags) => {
|
|
||||||
if (!tags) return [];
|
|
||||||
return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatTitle = (m) => {
|
|
||||||
const emojiList = toEmojis(m.tags);
|
|
||||||
if (emojiList.length > 0) {
|
|
||||||
return `${emojiList.join(" ")} ${m.title}`;
|
|
||||||
}
|
|
||||||
return m.title;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatTitleWithDefault = (m, fallback) => {
|
|
||||||
if (m.title) {
|
|
||||||
return formatTitle(m);
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatMessage = (m) => {
|
|
||||||
if (m.title) {
|
|
||||||
return m.message;
|
|
||||||
}
|
|
||||||
const emojiList = toEmojis(m.tags);
|
|
||||||
if (emojiList.length > 0) {
|
|
||||||
return `${emojiList.join(" ")} ${m.message}`;
|
|
||||||
}
|
|
||||||
return m.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unmatchedTags = (tags) => {
|
export const unmatchedTags = (tags) => {
|
||||||
if (!tags) return [];
|
if (!tags) return [];
|
||||||
return tags.filter((tag) => !(tag in emojis));
|
return tags.filter((tag) => !(tag in emojisMapped));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encodeBase64 = (s) => Base64.encode(s);
|
export const encodeBase64 = (s) => Base64.encode(s);
|
||||||
|
|
|
@ -24,17 +24,8 @@ import { useLiveQuery } from "dexie-react-hooks";
|
||||||
import InfiniteScroll from "react-infinite-scroll-component";
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import {
|
import { formatBytes, formatShortDateTime, maybeAppendActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
|
||||||
formatBytes,
|
import { formatMessage, formatTitle } from "../app/notificationUtils";
|
||||||
formatMessage,
|
|
||||||
formatShortDateTime,
|
|
||||||
formatTitle,
|
|
||||||
maybeAppendActionErrors,
|
|
||||||
openUrl,
|
|
||||||
shortUrl,
|
|
||||||
topicShortUrl,
|
|
||||||
unmatchedTags,
|
|
||||||
} from "../app/utils";
|
|
||||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import priority1 from "../img/priority-1.svg";
|
import priority1 from "../img/priority-1.svg";
|
||||||
|
|
Loading…
Reference in New Issue