From 1ab7ca876c978f230dd0cd0ed26ffd394b781116 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 14:27:18 -0500 Subject: [PATCH] Rename to sequence_id --- server/errors.go | 2 +- server/message_cache.go | 38 ++++++++++---------- server/message_cache_test.go | 14 ++++---- server/server.go | 26 +++++++------- server/server_test.go | 10 +++--- server/types.go | 56 +++++++++++++++--------------- web/public/sw.js | 11 +++--- web/src/app/Poller.js | 35 ++++++++++--------- web/src/app/SubscriptionManager.js | 32 ++++++++--------- web/src/app/db.js | 7 ++-- web/src/app/notificationUtils.js | 2 +- web/src/app/utils.js | 6 ++-- web/src/components/hooks.js | 7 ++-- 13 files changed, 125 insertions(+), 121 deletions(-) diff --git a/server/errors.go b/server/errors.go index 302c06a7..950d23fc 100644 --- a/server/errors.go +++ b/server/errors.go @@ -125,7 +125,7 @@ var ( errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} - errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: SID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} + errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/message_cache.go b/server/message_cache.go index 1752c2c9..c00c67c8 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -29,7 +29,7 @@ const ( CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, mid TEXT NOT NULL, - sid TEXT NOT NULL, + sequence_id TEXT NOT NULL, time INT NOT NULL, expires INT NOT NULL, topic TEXT NOT NULL, @@ -54,7 +54,7 @@ const ( deleted INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); - CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); + CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); CREATE INDEX IF NOT EXISTS idx_time ON messages (time); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires); @@ -69,50 +69,50 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) + INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesLatestQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -267,9 +267,9 @@ const ( //13 -> 14 migrate13To14AlterMessagesTableQuery = ` - ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT(''); + ALTER TABLE messages ADD COLUMN sequence_id TEXT NOT NULL DEFAULT(''); ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0'); - CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); + CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); ` ) @@ -409,7 +409,7 @@ func (c *messageCache) addMessages(ms []*message) error { } _, err := stmt.Exec( m.ID, - m.SID, + m.SequenceID, m.Time, m.Expires, m.Topic, @@ -720,11 +720,11 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string var deleted bool err := rows.Scan( &id, - &sid, + &sequenceID, ×tamp, &expires, &topic, @@ -773,13 +773,13 @@ func readMessage(rows *sql.Rows) (*message, error) { URL: attachmentURL, } } - // Clear SID if it equals ID (we do not want the SID in the message output) - if sid == id { - sid = "" + // Clear SequenceID if it equals ID (we do not want the SequenceID in the message output) + if sequenceID == id { + sequenceID = "" } return &message{ ID: id, - SID: sid, + SequenceID: sequenceID, Time: timestamp, Expires: expires, Event: messageEvent, diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 64203136..1e285605 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -319,7 +319,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires1 := time.Now().Add(-4 * time.Hour).Unix() // Expired m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" - m.SID = "m1" + m.SequenceID = "m1" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "flower.jpg", @@ -333,7 +333,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires2 := time.Now().Add(2 * time.Hour).Unix() // Future m = newDefaultMessage("mytopic", "sending you a car") m.ID = "m2" - m.SID = "m2" + m.SequenceID = "m2" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "car.jpg", @@ -347,7 +347,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires3 := time.Now().Add(1 * time.Hour).Unix() // Future m = newDefaultMessage("another-topic", "sending you another car") m.ID = "m3" - m.SID = "m3" + m.SequenceID = "m3" m.User = "u_BAsbaAa" m.Sender = netip.MustParseAddr("5.6.7.8") m.Attachment = &attachment{ @@ -403,13 +403,13 @@ func TestMemCache_Attachments_Expired(t *testing.T) { func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" - m.SID = "m1" + m.SequenceID = "m1" m.Expires = time.Now().Add(time.Hour).Unix() require.Nil(t, c.AddMessage(m)) m = newDefaultMessage("mytopic", "message with attachment") m.ID = "m2" - m.SID = "m2" + m.SequenceID = "m2" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -422,7 +422,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic", "message with external attachment") m.ID = "m3" - m.SID = "m3" + m.SequenceID = "m3" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -434,7 +434,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic2", "message with expired attachment") m.ID = "m4" - m.SID = "m4" + m.SequenceID = "m4" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "expired-car.jpg", diff --git a/server/server.go b/server/server.go index edae9f2a..c60154ec 100644 --- a/server/server.go +++ b/server/server.go @@ -917,13 +917,13 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { return errHTTPTooManyRequestsLimitMessages.With(t) } - sid, e := s.sidFromPath(r.URL.Path) + sequenceID, e := s.sequenceIDFromPath(r.URL.Path) if e != nil { return e.With(t) } - // Create a delete message: empty body, same SID, deleted flag set + // Create a delete message: empty body, same SequenceID, deleted flag set m := newDefaultMessage(t.ID, deletedMessageBody) - m.SID = sid + m.SequenceID = sequenceID m.Deleted = true m.Sender = v.IP() m.User = v.MaybeUserID() @@ -944,7 +944,7 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor if err := s.messageCache.AddMessage(m); err != nil { return err } - logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with SID %s", sid) + logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with sequence ID %s", sequenceID) s.mu.Lock() s.messages++ s.mu.Unlock() @@ -1009,21 +1009,21 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) { if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) { - pathSID, err := s.sidFromPath(r.URL.Path) + pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path) if err != nil { return false, false, "", "", "", false, err } - m.SID = pathSID + m.SequenceID = pathSequenceID } else { - sid := readParam(r, "x-sequence-id", "sequence-id", "sid") - if sid != "" { - if sidRegex.MatchString(sid) { - m.SID = sid + sequenceID := readParam(r, "x-sequence-id", "sequence-id", "sid") + if sequenceID != "" { + if sidRegex.MatchString(sequenceID) { + m.SequenceID = sequenceID } else { return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid } } else { - m.SID = m.ID + m.SequenceID = m.ID } } cache = readBoolParam(r, true, "x-cache", "cache") @@ -1764,8 +1764,8 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { return topics, parts[1], nil } -// sidFromPath returns the SID from a POST path like /mytopic/sidHere -func (s *Server) sidFromPath(path string) (string, *errHTTP) { +// sequenceIDFromPath returns the sequence ID from a POST path like /mytopic/sequenceIdHere +func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { parts := strings.Split(path, "/") if len(parts) != 3 { return "", errHTTPBadRequestSIDInvalid diff --git a/server/server_test.go b/server/server_test.go index c1b78c63..2978947f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -684,7 +684,7 @@ func TestServer_PublishWithSIDInPath(t *testing.T) { response := request(t, s, "POST", "/mytopic/sid", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SID) + require.Equal(t, "sid", msg.SequenceID) } func TestServer_PublishWithSIDInHeader(t *testing.T) { @@ -695,7 +695,7 @@ func TestServer_PublishWithSIDInHeader(t *testing.T) { }) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SID) + require.Equal(t, "sid", msg.SequenceID) } func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { @@ -706,7 +706,7 @@ func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { }) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) // SID in path has priority over SID in header + require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header } func TestServer_PublishWithSIDInQuery(t *testing.T) { @@ -715,7 +715,7 @@ func TestServer_PublishWithSIDInQuery(t *testing.T) { response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) + require.Equal(t, "sid1", msg.SequenceID) } func TestServer_PublishWithSIDViaGet(t *testing.T) { @@ -724,7 +724,7 @@ func TestServer_PublishWithSIDViaGet(t *testing.T) { response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) + require.Equal(t, "sid1", msg.SequenceID) } func TestServer_PublishWithInvalidSIDInPath(t *testing.T) { diff --git a/server/types.go b/server/types.go index b68721a3..e9c0fdb8 100644 --- a/server/types.go +++ b/server/types.go @@ -24,11 +24,11 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) - Time int64 `json:"time"` // Unix time in seconds - Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) - Event string `json:"event"` // One of the above + ID string `json:"id"` // Random message ID + SequenceID string `json:"sequence_id,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) + Time int64 `json:"time"` // Unix time in seconds + Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) + Event string `json:"event"` // One of the above Topic string `json:"topic"` Title string `json:"title,omitempty"` Message string `json:"message"` // Allow empty message body @@ -48,12 +48,12 @@ type message struct { func (m *message) Context() log.Context { fields := map[string]any{ - "topic": m.Topic, - "message_id": m.ID, - "message_sid": m.SID, - "message_time": m.Time, - "message_event": m.Event, - "message_body_size": len(m.Message), + "topic": m.Topic, + "message_id": m.ID, + "message_sequence_id": m.SequenceID, + "message_time": m.Time, + "message_event": m.Event, + "message_body_size": len(m.Message), } if m.Sender.IsValid() { fields["message_sender"] = m.Sender.String() @@ -94,23 +94,23 @@ func newAction() *action { // publishMessage is used as input when publishing as JSON type publishMessage struct { - Topic string `json:"topic"` - SID string `json:"sid"` - Title string `json:"title"` - Message string `json:"message"` - Priority int `json:"priority"` - Tags []string `json:"tags"` - Click string `json:"click"` - Icon string `json:"icon"` - Actions []action `json:"actions"` - Attach string `json:"attach"` - Markdown bool `json:"markdown"` - Filename string `json:"filename"` - Email string `json:"email"` - Call string `json:"call"` - Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) - Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) - Delay string `json:"delay"` + Topic string `json:"topic"` + SequenceID string `json:"sequence_id"` + Title string `json:"title"` + Message string `json:"message"` + Priority int `json:"priority"` + Tags []string `json:"tags"` + Click string `json:"click"` + Icon string `json:"icon"` + Actions []action `json:"actions"` + Attach string `json:"attach"` + Markdown bool `json:"markdown"` + Filename string `json:"filename"` + Email string `json:"email"` + Call string `json:"call"` + Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) + Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) + Delay string `json:"delay"` } // messageEncoder is a function that knows how to encode a message diff --git a/web/public/sw.js b/web/public/sw.js index 3a67da58..38dbc9c1 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -8,7 +8,7 @@ import { dbAsync } from "../src/app/db"; import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; -import { messageWithSID } from "../src/app/utils"; +import { messageWithSequenceId } from "../src/app/utils"; /** * General docs for service workers and PWAs: @@ -27,14 +27,15 @@ const addNotification = async ({ subscriptionId, message }) => { // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too - // Delete existing notification with same SID (if any) - if (message.sid) { - await db.notifications.where({ subscriptionId, sid: message.sid }).delete(); + // Delete existing notification with same sequence ID (if any) + const sequenceId = message.sequence_id || message.id; + if (sequenceId) { + await db.notifications.where({ subscriptionId, sequenceId }).delete(); } // Add notification to database await db.notifications.add({ - ...messageWithSID(message), + ...messageWithSequenceId(message), subscriptionId, new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 5c7d2e2d..aa0e6dba 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -47,22 +47,25 @@ class Poller { // Filter out notifications older than the prune threshold const deleteAfterSeconds = await prefs.deleteAfter(); const pruneThresholdTimestamp = deleteAfterSeconds > 0 ? Math.round(Date.now() / 1000) - deleteAfterSeconds : 0; - const recentNotifications = pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications; + const recentNotifications = + pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications; // Find the latest notification for each sequence ID - const latestBySid = this.latestNotificationsBySid(recentNotifications); + const latestBySequenceId = this.latestNotificationsBySequenceId(recentNotifications); // Delete all existing notifications for which the latest notification is marked as deleted - const deletedSids = Object.entries(latestBySid) + const deletedSequenceIds = Object.entries(latestBySequenceId) .filter(([, notification]) => notification.deleted) - .map(([sid]) => sid); - if (deletedSids.length > 0) { - console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSids); - await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid))); + .map(([sequenceId]) => sequenceId); + if (deletedSequenceIds.length > 0) { + console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds); + await Promise.all( + deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId)) + ); } // Add only the latest notification for each non-deleted sequence - const notificationsToAdd = Object.values(latestBySid).filter((n) => !n.deleted); + const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => !n.deleted); if (notificationsToAdd.length > 0) { console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`); await subscriptionManager.addNotifications(subscription.id, notificationsToAdd); @@ -82,18 +85,18 @@ class Poller { } /** - * Groups notifications by sid and returns only the latest (highest time) for each sequence. - * Returns an object mapping sid -> latest notification. + * Groups notifications by sequenceId and returns only the latest (highest time) for each sequence. + * Returns an object mapping sequenceId -> latest notification. */ - latestNotificationsBySid(notifications) { - const latestBySid = {}; + latestNotificationsBySequenceId(notifications) { + const latestBySequenceId = {}; notifications.forEach((notification) => { - const sid = notification.sid || notification.id; - if (!(sid in latestBySid) || notification.time >= latestBySid[sid].time) { - latestBySid[sid] = notification; + const sequenceId = notification.sequence_id || notification.id; + if (!(sequenceId in latestBySequenceId) || notification.time >= latestBySequenceId[sequenceId].time) { + latestBySequenceId[sequenceId] = notification; } }); - return latestBySid; + return latestBySequenceId; } } diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 4a4c6f54..772b30a7 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -2,7 +2,7 @@ import api from "./Api"; import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; -import { messageWithSID, topicUrl } from "./utils"; +import { messageWithSequenceId, topicUrl } from "./utils"; class SubscriptionManager { constructor(dbImpl) { @@ -15,7 +15,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), })) ); } @@ -83,7 +83,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null + last: null, }; await this.db.subscriptions.put(subscription); @@ -101,7 +101,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation // May be null! + reservation, // May be null! }); return local.id; @@ -183,15 +183,15 @@ class SubscriptionManager { // Add notification to database await this.db.notifications.add({ - ...messageWithSID(notification), + ...messageWithSequenceId(notification), subscriptionId, - new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id + last: notification.id, }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -202,12 +202,12 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { const notificationsWithSubscriptionId = notifications.map((notification) => { - return { ...messageWithSID(notification), subscriptionId }; + return { ...messageWithSequenceId(notification), subscriptionId }; }); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId + last: lastNotificationId, }); } @@ -228,8 +228,8 @@ class SubscriptionManager { await this.db.notifications.delete(notificationId); } - async deleteNotificationBySid(subscriptionId, sid) { - await this.db.notifications.where({ subscriptionId, sid }).delete(); + async deleteNotificationBySequenceId(subscriptionId, sequenceId) { + await this.db.notifications.where({ subscriptionId, sequenceId }).delete(); } async deleteNotifications(subscriptionId) { @@ -240,8 +240,8 @@ class SubscriptionManager { await this.db.notifications.where({ id: notificationId }).modify({ new: 0 }); } - async markNotificationReadBySid(subscriptionId, sid) { - await this.db.notifications.where({ subscriptionId, sid }).modify({ new: 0 }); + async markNotificationReadBySequenceId(subscriptionId, sequenceId) { + await this.db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 }); } async markNotificationsRead(subscriptionId) { @@ -250,19 +250,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName + displayName, }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation + reservation, }); } diff --git a/web/src/app/db.js b/web/src/app/db.js index 1bda553f..7e3c47e3 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,12 +11,11 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(6).stores({ - // FIXME Should be 3 + db.version(3).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sid]", + notifications: "&id,sequenceId,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sequenceId]", users: "&baseUrl,username", - prefs: "&key", + prefs: "&key" }); return db; diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 6cb8bc37..65b5bd3d 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -62,7 +62,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to icon, image, timestamp: message.time * 1000, - tag: message.sid || message.id, // Update notification if there is a sequence ID + tag: message.sequence_id || message.id, // Update notification if there is a sequence ID renotify: true, silent: false, // This is used by the notification onclick event diff --git a/web/src/app/utils.js b/web/src/app/utils.js index d9f851b4..9aeada05 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -103,9 +103,9 @@ export const maybeActionErrors = (notification) => { return actionErrors; }; -export const messageWithSID = (message) => { - if (!message.sid) { - message.sid = message.id; +export const messageWithSequenceId = (message) => { + if (!message.sequenceId) { + message.sequenceId = message.sequence_id || message.id; } return message; }; diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 3b171eab..5b50f0a8 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -53,9 +53,10 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived() // and FirebaseService::handleMessage(). - // Delete existing notification with same sid, if any - if (notification.sid) { - await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); + // Delete existing notification with same sequenceId, if any + const sequenceId = notification.sequence_id || notification.id; + if (sequenceId) { + await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId); } // Add notification to database if (!notification.deleted) {