Format emojis in the service worker directly

pull/751/head
nimbleghost 2023-05-31 18:27:32 +02:00
parent 44913c1668
commit 4648f83669
12 changed files with 85 additions and 112 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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