PageSourceSearch

https://foodsocial.io/wp-content/themes/paleo/js/header_notificati…?ver=20260829.1753816875

js foodsocial.io collected 2026-09-25 21:07:29 UTC 16,552 bytes, 468 lines download raw bytes

1/*
2 * Description: Notification Widget
3 * Author: Wafik
4 * Created @ 3/8/2024
5 * enqueue this script in header.php: wp_enqueue_script('notification-script', get_template_directory_uri() . '/js/header_notifications/notifications_widget.js', array('jquery'), null, true);
6 */
7
8var globalNotificationCount = 0;
9
10/**
11 * Represents a notification.
12 * @class
13 */
14class UserNotification {
15  /**
16   * Creates a new Notification.
17   * @param {number} id - The ID of the notification.
18   * @param {string} createdAt - The creation date of the notification.
19   * @param {string} type - The type of the notification.
20   * @param {string} isRead - Whether the notification is read ("1" for true, otherwise false).
21   * @param {number} invokerId - The ID of the user who triggered the notification.
22   * @param {string} invoker_username - The username of the user who triggered the notification.
23   * @param {Object} user_avatar - The avatar of the user who triggered the notification.
24   * @param {string} redirect_url - The URL to redirect when the notification is clicked.
25   */
26  constructor(
27    id,
28    interval,
29    type,
30    isRead,
31    invokerId,
32    invoker_username,
33    user_avatar,
34    redirect_url,
35    post_photo_url,
36    content
37  ) {
38    this.id = id;
39    this.interval = interval;
40    this.type = type;
41    this.isRead = isRead === "1";
42    this.invokerId = invokerId;
43    this.invoker_username = invoker_username;
44    this.user_avatar = user_avatar;
45    this.redirect_url = redirect_url;
46    this.post_photo_url = post_photo_url;
47    this.content = content;
48    this.message = this.generateMessage();
49  }
50
51  /**
52   * Generates a message based on the notification type.
53   * @returns {string} The generated message.
54   */
55  /* Note: These types should match the notification types in jondaley-notifications/Notification.php */
56  generateMessage() {
57    switch (this.type) {
58      case "newFollower":
59        return `<b>${this.invoker_username} followed you</b>`;
60      case "newComment":
61        return `<b>${this.invoker_username} commented</b> on your recipe`;
62      case "newReply":
63        return `<b>${this.invoker_username} replied</b> to your comment`;
64      case "newRecipe":
65        return `<b>${this.invoker_username}</b> shared a <b>Recipe</b>`;
66      case "newMealPlan":
67        return `<b>${this.invoker_username}</b> shared a <b>Meal Plan</b>`;
68      case "newRegularPost":
69      case "newShortPost":
70        return `<b>${this.invoker_username}</b> shared a Post`;
71      default:
72        return "You have a new notification!";
73    }
74  }
75
76  /**
77   * Formats the timestamp into a readable format.
78   * @returns {string} The formatted timestamp.
79   */
80  formatTimestamp(diffInSeconds) {
81    const units = [
82      { name: "year", seconds: 31536000, abbreviation: "y" },
83      { name: "month", seconds: 2592000, abbreviation: "month" },
84      { name: "week", seconds: 604800, abbreviation: "w" },
85      { name: "day", seconds: 86400, abbreviation: "d" },
86      { name: "hour", seconds: 3600, abbreviation: "h" },
87      { name: "minute", seconds: 60, abbreviation: "m" },
88      { name: "second", seconds: 1, abbreviation: "s" },
89    ];
90
91    for (const unit of units) {
92      const relativeInterval = Math.floor(diffInSeconds / unit.seconds);
93      if (relativeInterval > 0) {
94        if(unit.abbreviation == "month")
95          return add_s(relativeInterval, unit.abbreviation, false);
96        else
97          return relativeInterval+unit.abbreviation;
98      }
99    }
100
101    return "just now";
102  }
103}
104
105/**
106 * Hide the notification count, and close both dropdowns on click
107 */
108const initNotificationsWidget = () => {
109  jQuery(".notification-bell").each(function () {
110    const $bell = jQuery(this);
111    $bell
112      .on("click", toggleDropDown)
113      .find(".notification-count, .notifications-dropdown")
114      .hide();
115  });
116
117  // Close notification dropdown when clicked outside of it.
118  jQuery("body").click(function (event) {
119    var $target = jQuery(event.target);
120    const $dropdowns = jQuery(".dropdown-elements");
121    $dropdowns.each(function () {
122      $dropdown = jQuery(this);
123      if (
124        !$target.closest($dropdown).length &&
125        $dropdown.is(":visible") &&
126        !$target.hasClass("bell-icon")
127      ) {
128        toggleDropDown(event);
129      }
130    });
131  });
132
133  fetchNotificationCount(); /* Initial call to fetchNotificationCount to set count on page load */
134};
135
136/**
137 * Fetches the count of unread notifications.
138 * @returns {jqXHR} The jqXHR object representing the AJAX request.
139 */
140const fetchNotificationCount = () => {
141  jQuery
142    .ajax({
143      url: ajaxurl,
144      type: "GET",
145      data: { action: "jondaley_get_unread_notifications_count" },
146    })
147    .done((response) => {
148      // Get notification count
149      const num = parseInt(response.data.count, 10);
150      globalNotificationCount = num;
151      updateNotificationCountHTML(num);
152    })
153    .fail((xhr, status, error) => {
154      console.error("Error fetching notifications count:", status, error);
155    });
156};
157
158/**
159 * Marks all notifications as read on the server.
160 * @returns {jqXHR} The jqXHR object representing the AJAX request.
161 */
162// const markAllNotificationsAsRead = () => {
163//   return jQuery
164//     .ajax({
165//       url: ajaxurl,
166//       type: "POST",
167//       data: { action: "jondaley_mark_notifications_as_read" },
168//     })
169//     .done((response) => {
170//       if (response.success) {
171//       }
172//     })
173//     .fail((xhr, status, error) => {
174//       console.error("Error marking notifications as read:", status, error);
175//     });
176// };
177
178/**
179 * Fetches new notifications from the server.
180 * @returns {jqXHR} The jqXHR object representing the AJAX request.
181 */
182const fetchNotifications = (batchSize, pageNum) => {
183  return jQuery
184    .ajax({
185      url: ajaxurl,
186      type: "GET",
187      data: {
188        action: "jondaley_get_notifications",
189        batch_size: batchSize,
190        page_no: pageNum,
191      },
192    })
193    .done((response) => {
194      //console.log(response);
195    })
196    .fail((xhr, status, error) => {
197      console.error("Error fetching new notifications:", status, error);
198    });
199};
200
201/**
202 * Updates the HTML for notification count.
203 * @param {number} notificationCount - The number of unread notifications.
204 */
205const updateNotificationCountHTML = (notificationCount) => {
206  jQuery(".notification-bell").each(function () {
207    jQuery(this)
208      .find(".notification-count")
209      .text(notificationCount > 9 ? "9+" : notificationCount)
210      .toggle(notificationCount > 0);
211  });
212
213  // If in WebView, sync app badge count with notification count
214  if (isInWebView && notificationCount > 0) {
215    window.ReactNativeWebView.postMessage(JSON.stringify({
216      type: 'notificationCount',
217      count: notificationCount
218    }));
219  }
220};
221
222/**
223 * Toggles the visibility of both dropdowns.
224 * @returns {boolean} True if the dropdowns were opened, false if they were closed.
225 */
226const changeDropdownVisibility = (event) => {
227  // Prevent toggling if the event target has the 'notifications-title' class
228  if (jQuery(event.target).closest(".notifications-title").length > 0) {
229    return false;
230  }
231
232  // Toggle the visibility of both dropdowns
233  jQuery(".notification-bell")
234    .find(".notifications-dropdown")
235    .toggle();
236
237  // Return true if the dropdowns were opened, false otherwise
238  const wasOpened = jQuery(
239    ".notifications-dropdown"
240  ).is(":visible");
241  return wasOpened;
242};
243
244/**
245 * Convert the notification content into readable format
246 * (convert common escape sequences in a string to their readable equivalents.)
247 * @param {string} str - The input string containing escape sequences.
248 * @returns {string} The converted string with readable characters.
249 */
250const convertEscapedString = (str) => {
251  return str
252    .replace(/\\'/g, "'") // Replace \' with '
253    .replace(/\\"/g, '"') // Replace \" with "
254    .replace(/\\n/g, "\n") // Replace \n with newline
255    .replace(/\\r/g, "\r") // Replace \r with carriage return
256    .replace(/\\t/g, "\t") // Replace \t with tab
257    .replace(/\\b/g, "\b") // Replace \b with backspace
258    .replace(/\\f/g, "\f") // Replace \f with form feed
259    .replace(/\\\\/g, "\\"); // Replace \\ with single backslash
260};
261
262/**
263 * Formats a comment string by adding quotation marks.
264 * If the comment string exceeds 25 characters, it truncates the string
265 * to 25 characters and appends an ellipsis (...) at the end.
266 *
267 * @param {string} commentContent - The content of the comment to format.
268 * @returns {string} - The formatted comment string.
269 */
270const formatCommentText = (commentContent) => {
271  if (commentContent.length > 25) {
272    return `"${commentContent.slice(0, 25)}..."`;
273  } else {
274    return `"${commentContent}"`;
275  }
276};
277
278/**
279 * Generates the HTML for the notifications and append it to the dropdown.
280 * @param {Notification[]} notifications - An array of Notification objects.
281 */
282const displayNotificationsFromArray = (notifications) => {
283  jQuery(".notification-bell")
284    .find(".dropdown-elements")
285    .empty()
286    .each(function () {
287      const $notificationItems = jQuery();
288
289      if (notifications.length === 0) {
290        const notificationStyle = "zero-notifications";
291        $notificationItems.push(
292          jQuery(`
293                    <div class="notification-item ${notificationStyle}">
294                        <div>
295                            <span class="notification-message">You're all caught up!</span>
296                        </div>
297                    </div>
298                `)[0]
299        );
300      } else {
301        notifications.forEach((notification) => {
302          const notificationStyle = notification.isRead
303            ? ""
304            : "new-notification";
305          const diffInSeconds = notification.interval;
306          const formattedTime = notification.formatTimestamp(diffInSeconds);
307          const avatarImgUrl = notification.user_avatar?.src || null;
308          let img_for_mealplan_or_recipe = null;
309          if (
310            notification.type == "newRecipe" ||
311            notification.type == "newMealPlan" ||
312            notification.type == "newComment" ||
313            notification.type == "newReply"
314          ) {
315            if (notification.post_photo_url) {
316              img_for_mealplan_or_recipe = notification.post_photo_url;
317            }
318          }
319
320          // TODO: we should use some form of jdp_profile_url::get(true, $jdprofiles_user, $op)
321          // at least use the /user/$username format
322          const user_profile_url = `/user/${notification.invokerId}`;
323          const notificationItemClass = "notification-item";
324
325          // TODO, this is really hard to read, I fixed it some
326          const $notificationItem =
327            jQuery(`<div class="${notificationItemClass} ${notificationStyle}" data-notification-id="${notification.id}">
328                       <div class="avatar-time-combo">
329                          ${avatarImgUrl ? `
330                              <a href="${user_profile_url}" class="avatar-img">
331                                <img src="${avatarImgUrl}" alt="User Avatar">
332                              </a>
333                            `
334                                : ""
335                            }                            
336                              <span class="notification-time">${formattedTime}</span>
337                            </div>
338                            <div class="message-img-combo">
339                              <div class="message-box">
340                                  <span class="notification-message">
341                                    <p class="message">${convertEscapedString(notification.message)}</p>
342                                    <p class="sub-message">${
343                                      notification.type === "newComment" ||
344                                      notification.type === "newReply"
345                                        ? convertEscapedString(
346                                            formatCommentText(
347                                              notification.content
348                                            )
349                                          )
350                                        : convertEscapedString(
351                                            notification.content
352                                          )
353                                    }</p>
354                                  </span>
355                              </div>
356                              <div class="img_for_mealplan_or_recipe">
357                                ${
358                                  notification.type === "newFollower"
359                                    ? '<div class="notification-follow-button-wrapper"><button class="notification-follow-button"></button></div>'
360                                    : ""
361                                }
362                                ${
363                                  img_for_mealplan_or_recipe
364                                    ? `<img src="${img_for_mealplan_or_recipe}" alt="Recipe or Mealplan photo">`
365                                    : ""
366                                }
367                              </div>
368                            </div>
369                        </div>
370                    `);
371
372          // Initialize follow button functionality
373          if (notification.type === "newFollower") {
374            const $followBtn = $notificationItem.find(".notification-follow-button");
375            const followerId = notification.invokerId;
376            //console.log(followerId);
377
378            $followBtn.click(function (e) {
379              e.stopPropagation(); // Prevent notification click event
380              paleoFollow(followerId, $followBtn);
381            });
382
383            if (window.following.includes(followerId)) {
384              $followBtn.text("Following");
385              $followBtn.addClass("following");
386            } else {
387              $followBtn.html("Follow&nbsp;Back");
388            }
389          }
390
391          $notificationItem.click(function () {
392            window.location.href = notification.redirect_url;
393          });
394
395          $notificationItems.push($notificationItem[0]);
396        });
397      }
398
399      jQuery(this).append($notificationItems);
400    });
401};
402
403/**
404 * Displays error notifications in the dropdown and append it to the dropdown.
405 */
406const displayNotificationsError = () => {
407  jQuery(".notification-bell .dropdown-elements")
408    .empty()
409    .each(function () {
410      const notificationStyle = "error-notifications";
411      const notificationHTML = `
412                    <div class="notification-item ${notificationStyle}">
413                        <div>
414                            <span class="notification-message">🚨 Oops! Something went wrong. Don't worry, we're on it!</span>
415                        </div>
416                    </div>
417                `;
418      jQuery(this).append(notificationHTML);
419    });
420};
421
422/**
423 * Toggles the dropdown visibility and fetches new notifications if opened. Resets notification count
424 * if the dropdown was closed.
425 */
426const toggleDropDown = (event) => {
427  event.preventDefault();
428  const dropdownWasOpened = changeDropdownVisibility(event);
429  if (dropdownWasOpened) {
430    /* fetch the notifications */
431    fetchNotifications(50, 1) /* fetch the first 50 notifications for now until infinite scroll is implemented */
432      .done((response) => {
433        // Parse the response and create Notification objects
434        //console.log(response);
435        const notifications = response.data.notifications.map(
436          (notificationData) =>
437            new UserNotification(
438              notificationData.id,
439              notificationData.interval,
440              notificationData.type,
441              notificationData.isRead,
442              notificationData.invoker_id,
443              notificationData.invoker_username,
444              notificationData.user_avatar,
445              notificationData.redirect_url,
446              notificationData.post_photo_url,
447              notificationData.content
448            )
449        );
450
451        displayNotificationsFromArray(notifications);
452        if (globalNotificationCount > 0) {
453          jQuery(".notifications-title:visible").text(
454            `Notifications (${globalNotificationCount})`
455          );
456        } else {
457          jQuery(".notifications-title:visible").text("Notifications");
458        }
459      })
460      .catch((error) => {
461        displayNotificationsError();
462      });
463  } else {
464    /* if dropdown was closed, hide notification count */
465    updateNotificationCountHTML(0);
466    globalNotificationCount = 0;
467  }
468};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.