binwiederhier 1 месяц назад
Родитель
Сommit
aca9a77498

+ 15 - 22
server/message_cache.go

@@ -31,7 +31,6 @@ const (
 			mid TEXT NOT NULL,
 			sid TEXT NOT NULL,
 			time INT NOT NULL,
-			mtime INT NOT NULL,
 			expires INT NOT NULL,
 			topic TEXT NOT NULL,
 			message TEXT NOT NULL,
@@ -57,7 +56,6 @@ const (
 		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_time ON messages (time);
-		CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime);
 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
 		CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
@@ -71,53 +69,53 @@ const (
 		COMMIT;
 	`
 	insertMessageQuery = `
-		INSERT INTO messages (mid, sid, time, mtime, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+		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)
+		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, mtime, 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, 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
 		FROM messages
 		WHERE mid = ?
 	`
 	selectMessagesSinceTimeQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE topic = ? AND time >= ? AND published = 1
-		ORDER BY mtime, id
+		ORDER BY time, id
 	`
 	selectMessagesSinceTimeIncludeScheduledQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE topic = ? AND time >= ?
-		ORDER BY mtime, id
+		ORDER BY time, id
 	`
 	selectMessagesSinceIDQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE topic = ? AND id > ? AND published = 1
-		ORDER BY mtime, id
+		ORDER BY time, id
 	`
 	selectMessagesSinceIDIncludeScheduledQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE topic = ? AND (id > ? OR published = 0)
-		ORDER BY mtime, id
+		ORDER BY time, id
 	`
 	selectMessagesLatestQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE topic = ? AND published = 1
 		ORDER BY time DESC, id DESC
 		LIMIT 1
 	`
 	selectMessagesDueQuery = `
-		SELECT mid, sid, time, mtime, 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, 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
 		FROM messages
 		WHERE time <= ? AND published = 0
-		ORDER BY mtime, id
+		ORDER BY time, id
 	`
 	selectMessagesExpiredQuery      = `SELECT mid FROM messages WHERE expires <= ? AND published = 1`
 	updateMessagePublishedQuery     = `UPDATE messages SET published = 1 WHERE mid = ?`
@@ -270,10 +268,8 @@ const (
 	//13 -> 14
 	migrate13To14AlterMessagesTableQuery = `
 	  ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT('');
-		ALTER TABLE messages ADD COLUMN mtime INT NOT NULL DEFAULT('0');
 		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_mtime ON messages (mtime);
 	`
 )
 
@@ -415,7 +411,6 @@ func (c *messageCache) addMessages(ms []*message) error {
 			m.ID,
 			m.SID,
 			m.Time,
-			m.MTime,
 			m.Expires,
 			m.Topic,
 			m.Message,
@@ -723,14 +718,13 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
 }
 
 func readMessage(rows *sql.Rows) (*message, error) {
-	var timestamp, mtimestamp, expires, attachmentSize, attachmentExpires int64
+	var timestamp, expires, attachmentSize, attachmentExpires int64
 	var priority, deleted int
 	var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
 	err := rows.Scan(
 		&id,
 		&sid,
 		&timestamp,
-		&mtimestamp,
 		&expires,
 		&topic,
 		&msg,
@@ -782,7 +776,6 @@ func readMessage(rows *sql.Rows) (*message, error) {
 		ID:          id,
 		SID:         sid,
 		Time:        timestamp,
-		MTime:       mtimestamp,
 		Expires:     expires,
 		Event:       messageEvent,
 		Topic:       topic,

+ 3 - 19
server/message_cache_test.go

@@ -24,11 +24,9 @@ func TestMemCache_Messages(t *testing.T) {
 func testCacheMessages(t *testing.T, c *messageCache) {
 	m1 := newDefaultMessage("mytopic", "my message")
 	m1.Time = 1
-	m1.MTime = 1000
 
 	m2 := newDefaultMessage("mytopic", "my other message")
 	m2.Time = 2
-	m2.MTime = 2000
 
 	require.Nil(t, c.AddMessage(m1))
 	require.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message")))
@@ -126,13 +124,10 @@ func testCacheMessagesScheduled(t *testing.T, c *messageCache) {
 	m1 := newDefaultMessage("mytopic", "message 1")
 	m2 := newDefaultMessage("mytopic", "message 2")
 	m2.Time = time.Now().Add(time.Hour).Unix()
-	m2.MTime = time.Now().Add(time.Hour).UnixMilli()
 	m3 := newDefaultMessage("mytopic", "message 3")
-	m3.Time = time.Now().Add(time.Minute).Unix()       // earlier than m2!
-	m3.MTime = time.Now().Add(time.Minute).UnixMilli() // earlier than m2!
+	m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2!
 	m4 := newDefaultMessage("mytopic2", "message 4")
 	m4.Time = time.Now().Add(time.Minute).Unix()
-	m4.MTime = time.Now().Add(time.Minute).UnixMilli()
 	require.Nil(t, c.AddMessage(m1))
 	require.Nil(t, c.AddMessage(m2))
 	require.Nil(t, c.AddMessage(m3))
@@ -206,25 +201,18 @@ func TestMemCache_MessagesSinceID(t *testing.T) {
 func testCacheMessagesSinceID(t *testing.T, c *messageCache) {
 	m1 := newDefaultMessage("mytopic", "message 1")
 	m1.Time = 100
-	m1.MTime = 100000
 	m2 := newDefaultMessage("mytopic", "message 2")
 	m2.Time = 200
-	m2.MTime = 200000
 	m3 := newDefaultMessage("mytopic", "message 3")
-	m3.Time = time.Now().Add(time.Hour).Unix()       // Scheduled, in the future, later than m7 and m5
-	m3.MTime = time.Now().Add(time.Hour).UnixMilli() // Scheduled, in the future, later than m7 and m5
+	m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5
 	m4 := newDefaultMessage("mytopic", "message 4")
 	m4.Time = 400
-	m4.MTime = 400000
 	m5 := newDefaultMessage("mytopic", "message 5")
-	m5.Time = time.Now().Add(time.Minute).Unix()       // Scheduled, in the future, later than m7
-	m5.MTime = time.Now().Add(time.Minute).UnixMilli() // Scheduled, in the future, later than m7
+	m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7
 	m6 := newDefaultMessage("mytopic", "message 6")
 	m6.Time = 600
-	m6.MTime = 600000
 	m7 := newDefaultMessage("mytopic", "message 7")
 	m7.Time = 700
-	m7.MTime = 700000
 
 	require.Nil(t, c.AddMessage(m1))
 	require.Nil(t, c.AddMessage(m2))
@@ -285,17 +273,14 @@ func testCachePrune(t *testing.T, c *messageCache) {
 
 	m1 := newDefaultMessage("mytopic", "my message")
 	m1.Time = now - 10
-	m1.MTime = (now - 10) * 1000
 	m1.Expires = now - 5
 
 	m2 := newDefaultMessage("mytopic", "my other message")
 	m2.Time = now - 5
-	m2.MTime = (now - 5) * 1000
 	m2.Expires = now + 5 // In the future
 
 	m3 := newDefaultMessage("another_topic", "and another one")
 	m3.Time = now - 12
-	m3.MTime = (now - 12) * 1000
 	m3.Expires = now - 2
 
 	require.Nil(t, c.AddMessage(m1))
@@ -546,7 +531,6 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
 	// Add delayed message
 	delayedMessage := newDefaultMessage("mytopic", "some delayed message")
 	delayedMessage.Time = time.Now().Add(time.Minute).Unix()
-	delayedMessage.MTime = time.Now().Add(time.Minute).UnixMilli()
 	require.Nil(t, c.AddMessage(delayedMessage))
 
 	// 10, not 11!

+ 3 - 3
server/server.go

@@ -874,7 +874,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
 		return err
 	}
 	minc(metricMessagesPublishedSuccess)
-	return s.writeJSON(w, m)
+	return s.writeJSON(w, m.forJSON())
 }
 
 func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error {
@@ -1291,7 +1291,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
 func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	encoder := func(msg *message) (string, error) {
 		var buf bytes.Buffer
-		if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
+		if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
 			return "", err
 		}
 		return buf.String(), nil
@@ -1302,7 +1302,7 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *
 func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	encoder := func(msg *message) (string, error) {
 		var buf bytes.Buffer
-		if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
+		if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
 			return "", err
 		}
 		if msg.Event != messageEvent {

+ 15 - 12
server/types.go

@@ -24,10 +24,9 @@ const (
 
 // message represents a message published to a topic
 type message struct {
-	ID          string      `json:"id"`                // Random message ID
-	SID         string      `json:"sid"`               // Message sequence ID for updating message contents
-	Time        int64       `json:"time"`              // Unix time in seconds
-	MTime       int64       `json:"mtime"`             // Unix time in milliseconds
+	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
 	Topic       string      `json:"topic"`
@@ -53,7 +52,6 @@ func (m *message) Context() log.Context {
 		"message_id":        m.ID,
 		"message_sid":       m.SID,
 		"message_time":      m.Time,
-		"message_mtime":     m.MTime,
 		"message_event":     m.Event,
 		"message_body_size": len(m.Message),
 	}
@@ -66,6 +64,16 @@ func (m *message) Context() log.Context {
 	return fields
 }
 
+// forJSON returns a copy of the message prepared for JSON output.
+// It clears SID if it equals ID (to avoid redundant output).
+func (m *message) forJSON() *message {
+	msg := *m
+	if msg.SID == msg.ID {
+		msg.SID = "" // Will be omitted due to omitempty
+	}
+	return &msg
+}
+
 type attachment struct {
 	Name    string `json:"name"`
 	Type    string `json:"type,omitempty"`
@@ -123,7 +131,6 @@ func newMessage(event, topic, msg string) *message {
 	return &message{
 		ID:      util.RandomString(messageIDLength),
 		Time:    time.Now().Unix(),
-		MTime:   time.Now().UnixMilli(),
 		Event:   event,
 		Topic:   topic,
 		Message: msg,
@@ -162,11 +169,7 @@ type sinceMarker struct {
 }
 
 func newSinceTime(timestamp int64) sinceMarker {
-	return newSinceMTime(timestamp * 1000)
-}
-
-func newSinceMTime(mtimestamp int64) sinceMarker {
-	return sinceMarker{time.UnixMilli(mtimestamp), ""}
+	return sinceMarker{time.Unix(timestamp, 0), ""}
 }
 
 func newSinceID(id string) sinceMarker {
@@ -557,7 +560,7 @@ func newWebPushPayload(subscriptionID string, message *message) *webPushPayload
 	return &webPushPayload{
 		Event:          webPushMessageEvent,
 		SubscriptionID: subscriptionID,
-		Message:        message,
+		Message:        message.forJSON(),
 	}
 }
 

+ 1 - 2
web/public/static/langs/en.json

@@ -70,8 +70,7 @@
   "notifications_delete": "Delete",
   "notifications_copied_to_clipboard": "Copied to clipboard",
   "notifications_tags": "Tags",
-  "notifications_sid": "Sequence ID",
-  "notifications_revisions": "Revisions",
+  "notifications_modified": "modified {{date}}",
   "notifications_priority_x": "Priority {{priority}}",
   "notifications_new_indicator": "New notification",
   "notifications_attachment_image": "Attachment image",

+ 0 - 3
web/public/sw.js

@@ -25,9 +25,6 @@ const addNotification = async ({ subscriptionId, message }) => {
   const db = await dbAsync();
   const populatedMessage = message;
 
-  if (!("mtime" in populatedMessage)) {
-    populatedMessage.mtime = message.time * 1000;
-  }
   if (!("sid" in populatedMessage)) {
     populatedMessage.sid = message.id;
   }

+ 22 - 19
web/src/app/SubscriptionManager.js

@@ -157,7 +157,7 @@ class SubscriptionManager {
     // killing performance. See  https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach
 
     const notifications = await this.db.notifications
-      .orderBy("mtime") // Sort by time first
+      .orderBy("time") // Sort by time
       .filter((n) => n.subscriptionId === subscriptionId)
       .reverse()
       .toArray();
@@ -167,30 +167,39 @@ class SubscriptionManager {
 
   async getAllNotifications() {
     const notifications = await this.db.notifications
-      .orderBy("mtime") // Efficient, see docs
+      .orderBy("time") // Efficient, see docs
       .reverse()
       .toArray();
 
     return this.groupNotificationsBySID(notifications);
   }
 
-  // Collapse notification updates based on sids
+  // Collapse notification updates based on sids, keeping only the latest version
+  // Also tracks the original time (earliest) for each sequence
   groupNotificationsBySID(notifications) {
-    const results = {};
+    const latestBySid = {};
+    const originalTimeBySid = {};
+
     notifications.forEach((notification) => {
       const key = `${notification.subscriptionId}:${notification.sid}`;
-      if (key in results) {
-        if ("history" in results[key]) {
-          results[key].history.push(notification);
-        } else {
-          results[key].history = [notification];
-        }
-      } else {
-        results[key] = notification;
+
+      // Track the latest notification for each sid (first one since sorted DESC)
+      if (!(key in latestBySid)) {
+        latestBySid[key] = notification;
+      }
+
+      // Track the original (earliest) time for each sid
+      const currentOriginal = originalTimeBySid[key];
+      if (currentOriginal === undefined || notification.time < currentOriginal) {
+        originalTimeBySid[key] = notification.time;
       }
     });
 
-    return Object.values(results);
+    // Return latest notifications with originalTime set
+    return Object.entries(latestBySid).map(([key, notification]) => ({
+      ...notification,
+      originalTime: originalTimeBySid[key],
+    }));
   }
 
   /** Adds notification, or returns false if it already exists */
@@ -201,9 +210,6 @@ class SubscriptionManager {
     }
     try {
       const populatedNotification = notification;
-      if (!("mtime" in populatedNotification)) {
-        populatedNotification.mtime = notification.time * 1000;
-      }
       if (!("sid" in populatedNotification)) {
         populatedNotification.sid = notification.id;
       }
@@ -227,9 +233,6 @@ class SubscriptionManager {
   async addNotifications(subscriptionId, notifications) {
     const notificationsWithSubscriptionId = notifications.map((notification) => {
       const populatedNotification = notification;
-      if (!("mtime" in populatedNotification)) {
-        populatedNotification.mtime = notification.time * 1000;
-      }
       if (!("sid" in populatedNotification)) {
         populatedNotification.sid = notification.id;
       }

+ 2 - 2
web/src/app/db.js

@@ -11,9 +11,9 @@ 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(3).stores({
+  db.version(4).stores({
     subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]",
-    notifications: "&id,sid,subscriptionId,time,mtime,new,[subscriptionId+new]", // compound key for query performance
+    notifications: "&id,sid,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance
     users: "&baseUrl,username",
     prefs: "&key",
   });

+ 1 - 1
web/src/app/notificationUtils.js

@@ -69,7 +69,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to
       badge,
       icon,
       image,
-      timestamp: message.mtime,
+      timestamp: message.time * 1000,
       tag,
       renotify: true,
       silent: false,

+ 5 - 14
web/src/components/Notifications.jsx

@@ -236,7 +236,9 @@ const NotificationItem = (props) => {
   const { t, i18n } = useTranslation();
   const { notification } = props;
   const { attachment } = notification;
-  const date = formatShortDateTime(notification.time, i18n.language);
+  const isModified = notification.originalTime && notification.originalTime !== notification.time;
+  const originalDate = formatShortDateTime(notification.originalTime || notification.time, i18n.language);
+  const modifiedDate = isModified ? formatShortDateTime(notification.time, i18n.language) : null;
   const otherTags = unmatchedTags(notification.tags);
   const tags = otherTags.length > 0 ? otherTags.join(", ") : null;
   const handleDelete = async () => {
@@ -267,8 +269,6 @@ const NotificationItem = (props) => {
   const hasUserActions = notification.actions && notification.actions.length > 0;
   const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
 
-  const showSid = notification.id !== notification.sid || notification.history;
-
   return (
     <Card sx={{ padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
       <CardContent>
@@ -289,7 +289,8 @@ const NotificationItem = (props) => {
           </Tooltip>
         )}
         <Typography sx={{ fontSize: 14 }} color="text.secondary">
-          {date}
+          {originalDate}
+          {modifiedDate && ` (${t("notifications_modified", { date: modifiedDate })})`}
           {[1, 2, 4, 5].includes(notification.priority) && (
             <img
               src={priorityFiles[notification.priority]}
@@ -325,16 +326,6 @@ const NotificationItem = (props) => {
             {t("notifications_tags")}: {tags}
           </Typography>
         )}
-        {showSid && (
-          <Typography sx={{ fontSize: 14 }} color="text.secondary">
-            {t("notifications_sid")}: {notification.sid}
-          </Typography>
-        )}
-        {notification.history && (
-          <Typography sx={{ fontSize: 14 }} color="text.secondary">
-            {t("notifications_revisions")}: {notification.history.length + 1}
-          </Typography>
-        )}
       </CardContent>
       {showActions && (
         <CardActions sx={{ paddingTop: 0 }}>