--- /dev/null
+/**
+ * @file
+ * JavaScript API for the History module, with client-side caching.
+ *
+ * May only be loaded for authenticated users, with the History module enabled.
+ */
+
+(function ($, Drupal, drupalSettings, storage) {
+ const currentUserID = parseInt(drupalSettings.user.uid, 10);
+
+ // Any comment that is older than 30 days is automatically considered read,
+ // so for these we don't need to perform a request at all!
+ const thirtyDaysAgo = Math.round(new Date().getTime() / 1000) - 30 * 24 * 60 * 60;
+
+ // Use the data embedded in the page, if available.
+ let embeddedLastReadTimestamps = false;
+ if (drupalSettings.history && drupalSettings.history.lastReadTimestamps) {
+ embeddedLastReadTimestamps = drupalSettings.history.lastReadTimestamps;
+ }
+
+ /**
+ * @namespace
+ */
+ Drupal.history = {
+
+ /**
+ * Fetch "last read" timestamps for the given nodes.
+ *
+ * @param {Array} nodeIDs
+ * An array of node IDs.
+ * @param {function} callback
+ * A callback that is called after the requested timestamps were fetched.
+ */
+ fetchTimestamps(nodeIDs, callback) {
+ // Use the data embedded in the page, if available.
+ if (embeddedLastReadTimestamps) {
+ callback();
+ return;
+ }
+
+ $.ajax({
+ url: Drupal.url('history/get_node_read_timestamps'),
+ type: 'POST',
+ data: { 'node_ids[]': nodeIDs },
+ dataType: 'json',
+ success(results) {
+ for (const nodeID in results) {
+ if (results.hasOwnProperty(nodeID)) {
+ storage.setItem(`Drupal.history.${currentUserID}.${nodeID}`, results[nodeID]);
+ }
+ }
+ callback();
+ },
+ });
+ },
+
+ /**
+ * Get the last read timestamp for the given node.
+ *
+ * @param {number|string} nodeID
+ * A node ID.
+ *
+ * @return {number}
+ * A UNIX timestamp.
+ */
+ getLastRead(nodeID) {
+ // Use the data embedded in the page, if available.
+ if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+ return parseInt(embeddedLastReadTimestamps[nodeID], 10);
+ }
+ return parseInt(storage.getItem(`Drupal.history.${currentUserID}.${nodeID}`) || 0, 10);
+ },
+
+ /**
+ * Marks a node as read, store the last read timestamp client-side.
+ *
+ * @param {number|string} nodeID
+ * A node ID.
+ */
+ markAsRead(nodeID) {
+ $.ajax({
+ url: Drupal.url(`history/${nodeID}/read`),
+ type: 'POST',
+ dataType: 'json',
+ success(timestamp) {
+ // If the data is embedded in the page, don't store on the client
+ // side.
+ if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+ return;
+ }
+
+ storage.setItem(`Drupal.history.${currentUserID}.${nodeID}`, timestamp);
+ },
+ });
+ },
+
+ /**
+ * Determines whether a server check is necessary.
+ *
+ * Any content that is >30 days old never gets a "new" or "updated"
+ * indicator. Any content that was published before the oldest known reading
+ * also never gets a "new" or "updated" indicator, because it must've been
+ * read already.
+ *
+ * @param {number|string} nodeID
+ * A node ID.
+ * @param {number} contentTimestamp
+ * The time at which some content (e.g. a comment) was published.
+ *
+ * @return {bool}
+ * Whether a server check is necessary for the given node and its
+ * timestamp.
+ */
+ needsServerCheck(nodeID, contentTimestamp) {
+ // First check if the content is older than 30 days, then we can bail
+ // early.
+ if (contentTimestamp < thirtyDaysAgo) {
+ return false;
+ }
+
+ // Use the data embedded in the page, if available.
+ if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+ return contentTimestamp > parseInt(embeddedLastReadTimestamps[nodeID], 10);
+ }
+
+ const minLastReadTimestamp = parseInt(storage.getItem(`Drupal.history.${currentUserID}.${nodeID}`) || 0, 10);
+ return contentTimestamp > minLastReadTimestamp;
+ },
+ };
+}(jQuery, Drupal, drupalSettings, window.localStorage));