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