1window.initializeSocket = function () { 2 // note: needs to be called in afterLoginlessInit to ensure uuid 3 4 // if we're already initialized, don't do anything (this causes multiple messages to come in) 5 // apparently overriding the socket variable doesn't make this ok. Plus this should only ever 6 // happen once anyway, but on the callback to getUserData we do it, and that happens in some cases 7 if(window.socket != null){return} 8 9 // socket.io is loaded with defer, so it may not be ready yet if this is called from an AJAX callback 10 if(typeof io === 'undefined'){ 11 setTimeout(window.initializeSocket, 1000); 12 return; 13 } 14 15 window.hostSite = window.location.host.split(".")[0]; 16 if(hostSite == "www"){ 17 port = 443 18 }else if(hostSite == "test"){ 19 port = 8442 20 }else{ 21 port = 8443 22 } 23 24 window.socket = io('https://push.coursicle.com' + ":" + port, { autoConnect: false }) 25 let clientType = (!isPWA ? 'web' : 'android'); 26 27 // get the device id 28 var deviceID = null; 29 if(window.device && "fingerprint" in window.device){ 30 package = {"deviceID" : window.device["fingerprint"]} 31 if(store.get('pwaWrapperData')){ 32 updateToPWAParams(package) 33 } 34 deviceID = package["deviceID"] 35 } 36 window.socket.auth = { uuid: store.get('uuid'), clientType: clientType, deviceID: deviceID, school: retrieveSchoolParameter()} 37 38 // now that window.socket is defined, we need to setup the on() listeners 39 socketListenerSetup(); 40
41 // note: window.socket.connect() is only called in the callback to getUsersChatsFromServer() 42 // because at that point the server knows all of the chats that they're in (if it had to add 43 // them to any). This was causing a very pernicious bug. We were initializing 44 // the socket when the app first opened, before this call was made. So 45 // if it was your first ever time coming to Coursicle, you hadn't been put 46 // in the sample active chat yet (active class chat, schedule advice chat, etc.), 47 // and so the push server wouldn't join you to that room because at the time 48 // of connect you weren't in that chat yet. Had to do this on all clients. 49 // 11/8/23: this is less relevant now that we allow joining public chats without being 50 // "in" them in the relations table, because we want to show them a sample active chat 51 // when they first come to the site, but still no harm in doing the flow this way 52} 53 54window.emitTypingStartStatus = function () { 55 window.isTyping = true 56 window.socket?.emit('typingStatus', { 57 chatID: activeChatID(), 58 uuid: store.get('uuid'), 59 state: 'started', 60 name: store.get('userData').username, 61 photo: store.get('userData').userPhotoUrl, 62 }) 63} 64 65window.emitTypingStopStatus = function () { 66 window.isTyping = false 67 window.socket?.emit('typingStatus', { 68 chatID: activeChatID(), 69 uuid: store.get('uuid'), 70 state: 'stopped', 71 }) 72} 73 74// this is specifically when you add a new chat 75// you also need to tell the socket server that you want to 76// listen to events that happen in that chat 77window.emitJoinRoom = function (id) { 78 window.socket?.emit('joinRoom', id, (err, val) => { }) 79} 80 81// this tells the server to notify any currently connected clients that they need to 82// refresh their chat list immediately to show the new chat. 83window.emitNewChatCreated = function (chatID, coursicleIDs) { 84 window.socket?.emit('newChatCreated', { "chatID": chatID, "coursicleIDs": coursicleIDs }, (err, val) => { }) 85} 86 87window.emitStartedViewing = function (id) { 88 var selected = id; 89 if (id == null || id == 0) { selected = "chatTab"; } 90 window.socket?.emit('startedViewing', selected, (err, val) => { }) 91} 92 93window.emitStoppedViewing = function (id) { 94 var selected = id; 95 if (id == null || id == 0) { selected = "chatTab"; } 96 window.socket?.emit('stoppedViewing', selected, (err, val) => { }) 97} 98 99// on connection, this defaults to true, so we only have to send this 100// when the browser comes back into view from a minimized state, etc using ifVisible 101window.emitAppInView = function () { 102 if(chatEnabled){ 103 window.socket?.emit('appInView') 104 } 105} 106 107window.emitAppOutOfView = function () { 108 if(chatEnabled){ 109 window.socket?.emit('appOutOfView') 110 } 111} 112 113// ensuring chat the socket is disconnected when the user effectively stops using Coursicle 114ifvisible.on("blur", function (event) { 115 116 var timeBeforeDisconnect = 5 * 60 * 1000; // 5 minutes 117 if(window.isPWA){ 118 timeBeforeDisconnect = 0; // disconnect immediately if they're not viewing the page, mimic iOS, save on socket connections 119 } 120 121 // if after timeBeforeDisconnect minutes, the page still isn't visible, disconnect 122 emitAppOutOfView(); 123 setTimeout(function () { 124 if (!ifvisible.now()) { 125 window.socket?.disconnect() 126 } 127 }, timeBeforeDisconnect); 128}); 129 130// ensuring the socket is reconnected when they return to Coursicle (if it's been disconnected) 131ifvisible.on("focus", function () { 132 emitAppInView(); 133 // if the socket isn't connected, most likely due to inactivity, let's reconnect 134 if (!window.socket?.connected) { 135 window.socket?.connect(); 136 } 137}); 138 139// note: the time to idle is defined in global.php as checkIntervalWhenActive 140// this is usually about 10 minutes, so if it's been 10 minutes since the page was 141// used, then we consider them not really to be on Coursicle and there's no need to keep 142// a constant connection. When they return, though, we're going to have to do a full refresh 143// of the content because the socket hasn't been able to keep their data up to date. 144// Update: on idle doesn't work very well, for instance if the tab is changed then 145// it's considered idle, not waiting 600 seconds or whatever, so we're going to use blur instead 146/* 147 ifvisible.on("idle", function(){ 148 window.socket.disconnect(); 149 }); 150 151 ifvisible.on("wakeup", function(){ 152 window.socket.connect(); 153 }); 154*/ 155 156function socketListenerSetup() { 157 158 // typing message logic! 159 window.socket.on('typingStatus', status => { 160 window.typingChatID = status.chatID 161 window.typingCoursicleID = status.coursicleID 162 let chatID = window.typingChatID 163 164 // initialize chatState 165 if (!window.chatState[chatID]) { 166 window.chatState[chatID] = { 167 isTyping: [], 168 scrollPos: {}, 169 msgLoading: {}, 170 typingMessage: false, 171 } 172 } 173 174 // if it's you typing on another device, prevent any action 175 if (status.coursicleID == getCoursicleID()) return 176 177 // find the correct chat list item snippet 178 const snippet = $(`.chatListItem[data-id=${chatID}]`) 179 .find('.chatSnippet') 180 181 // if someone starts typing 182 if (status.state === 'started') { 183 // push user typing status to chat state variable 184 window.chatState[chatID].isTyping.push(status) 185 186 // render typing indicator in chat list snippet 187 snippet.html(renderTypingIndicator()) 188 189 // break if this is not the active chat 190 if (activeChatID() != chatID) { return } 191 192 if (!$('#typingMessage').length) { 193 $('#messagesContainer').append(renderTypingMessage(window.chatState[chatID].isTyping)) 194 } 195 else { 196 $('#typingMessage').replaceWith(renderTypingMessage(window.chatState[chatID].isTyping)) 197 } 198 199 window.chatState[chatID].typingMessage = true 200 201 // if only previous sender is typing, add necessary styling 202 if (window.chatState[chatID].isTyping.length == 1 && 203 $(".message:last").data('coursicleid') == status.coursicleID) { 204 $(".message:last").addClass('sameUserAsNextMsg') 205 $('#typingMessage').addClass('sameUserAsPrevMsg') 206 if(shortDelayBetweenMessageGapNeeded(Date.now() / 1000)){ 207 $(".message").last().addClass('shortDelayBetweenNextMsg') 208 } 209 } 210 // otherwise, multiple people are typing, so remove styling 211 else { $(".message:last").removeClass('sameUserAsNextMsg') } 212 213 // after render, scroll to the bottom of message container 214 // $('#messagesContainer')[0].scrollTo({ 215 // top: $('#messagesContainer')[0].scrollHeight, 216 // behavior: "smooth", 217 // }) 218 if (Math.abs( 219 $('#messagesContainer').scrollTop() + 220 $('#messagesContainer').innerHeight() - 221 $('#messagesContainer')[0].scrollHeight) < 100) { 222 223 $('#messagesContainer').animate({ 224 scrollTop: $('#messagesContainer')[0].scrollHeight - $('#messagesContainer')[0].clientHeight 225 }, 50) 226 } 227 } 228 229 // if someone stops typing 230 else if (status.state === 'stopped') { 231 let length = window.chatState[chatID].isTyping.length 232 233 // remove user typing status from chat state variable 234 window.chatState[chatID].isTyping = window.chatState[chatID].isTyping 235 .filter(user => user.coursicleID !== status.coursicleID) 236 237 // if the user has already been removed from 238 // isTyping, that means they sent their message 239 if ( 240 length == window.chatState[chatID].isTyping.length 241 || !window.chatState[chatID].isTyping.length // handles race condition 242 ) { 243 hideTypingIndicatorHelper() 244 return 245 } 246 247 248 // if someone else is still typing, update the typing message 249 if (window.chatState[chatID].isTyping.length != 0) { 250 $('#typingMessage').replaceWith(renderTypingMessage(window.chatState[chatID].isTyping)) 251 252 // if that one user still typing is the same user 253 // as the previous message, add necessary styling 254 if (window.chatState[chatID].isTyping.length == 1 && 255 window.chatState[chatID].isTyping[0].coursicleID == 256 $(".message:last").data('coursicleid')) { 257 $(".message:last").addClass('sameUserAsNextMsg') 258 $('#typingMessage').addClass('sameUserAsPrevMsg') 259 if(shortDelayBetweenMessageGapNeeded(Date.now() / 1000)){ 260 $(".message").last().addClass('shortDelayBetweenNextMsg') 261 } 262 } 263 // if multiple people are typing, remove any such styling 264 else $(".message:last").removeClass('sameUserAsNextMsg') 265 } 266 267 //'if now no one is still typing' handling has been handled in hideTypingIndicatorHelper. Flicker is reduced if typing indicator hides AFTER message rendered. 268 269 270 // $('#messagesContainer')[0].scrollTo({ 271 // top: $('.message').get(-2).offsetTop - $('#messagesContainer').offset().top, 272 // behavior: "smooth", 273 // block: 'end', 274 // }) 275 // $('.message').get(-1).scrollIntoView({ 276 // behavior: 'smooth', 277 // block: 'end', 278 // }) 279 280 // if (Math.abs($('#messagesContainer').scrollTop() 281 // + $('#messagesContainer').innerHeight() 282 // - $('#messagesContainer')[0].scrollHeight) < 100) { 283 // setTimeout(() => { 284 // $('#messagesContainer').animate({ 285 // scrollTop: $('#messagesContainer')[0].scrollHeight 286 // }, 250) 287 // }, 50) 288 // } 289 // setTimeout(() => { 290 // if (window.chatState[chatID].typingMessage) { 291 // $(".message:last").removeClass('sameUserAsNextMsg') 292 // } 293 // window.chatState[chatID].typingMessage = false 294 // }, 250) 295 296 } 297 }) 298 299 // leaving this animation code here just in case 300 // we want to use it for later: 301 302 // $('.typingMessage').animate({ "top": "-= 300px" }, "fast") 303 // $('#messagesContainer').animate({ "top": "-= 300px" }, "fast") 304 305 // $('.typingMessage').get(0).scrollIntoView({ 306 // behavior: 'smooth', 307 // // block: 'end', 308 // }) 309 310 // $('#messagesContainer')[0].scrollTo({ 311 // top: $('#messagesContainer').scrollHeight, 312 // behavior: "instant", 313 // }) 314 // $('#messagesContainer').removeClass('typing') 315 // setTimeout(() => { 316 // $('.typingMessage').removeClass('visible') 317 // }, 1000) 318 319 // $('.typingMessage').animate({ "top": "+= 200px" }, "fast") 320 // $('#messagesContainer').animate({ "top": "+= 200px" }, "fast") 321 322 // $('.message').get(-2).scrollIntoView({ 323 // behavior: 'smooth', 324 // block: 'end', 325 // }) 326 327 328 // delete message logic! 329 window.socket.on("messageDeleted", (data) => { 330 vibrateIfAble(10); 331 // remove from local storage 332 usersChatsCopy = usersChats() 333 usersChatsCopy.forEach(chat => { 334 if (chat.id == data.chatID) { 335 let msgIterCount = 0
336 chat.recentMessages.forEach(msg => { 337 if (msg.id == data.messageID) { 338 //remove from recentmessages 339 chat.recentMessages.splice(msgIterCount, 1) 340 } 341 msgIterCount += 1 342 }) 343 } 344 }) 345 store.set("usersChats", usersChatsCopy) 346 //I didn't get rid of this completely despite not knowing why this is necessary. 347 //on unsend from device A, if you're in the chat list on device B, B's activeChatID is 0 which renderUpdateSnippet does not handle gracefully 348 if(activeChatID()){ 349 renderUpdateSnippet(activeChatID()); 350 } 351 renderUpdateSnippet(Number(data.chatID)) 352 //remove from DOM 353 $(`.chatItem[data-id=${data.messageID}]`).slideUp(200, function () { 354 $(`.chatItem[data-id=${data.messageID}]`).remove() 355 }) 356 playDeletedSound() 357 }) 358 359 // someone created a chat with this user in it, so we need to refresh our chat list 360 window.socket.on("newChatCreated", (chatID) => { 361 syncUserChatDataWithServer(); 362 emitJoinRoom(chatID); 363 }) 364 365 366 // incoming message logic! 367 window.socket.on('message', msg => { 368 369 // vibrate device. You can provide patterns. Check documentation. 370 vibrateIfAble(10); 371 372 // could have been sent by me on another device 373 let sentByMeOnAnotherDevice = (msg.coursicleID == getCoursicleID()) 374 375 // could be in a chat that's not in focus 376 let sentInChatInFocus = (msg.chatID == activeChatID()) 377 378 if (!sentByMeOnAnotherDevice) { 379 if (ifvisible.now()) { 380 if (sentInChatInFocus) { 381 playInChatMessageReceived(); 382 } else { 383 playOtherChatFocusedMessageReceivedSound(); 384 } 385 } else { 386 playBannerNotificationSound(); 387 } 388 } 389 390 var message = { 391 id: msg.id, 392 authorName: msg.userName, 393 type: msg.type, 394 data: msg.data, 395 userPhoto: msg.userPhoto, 396 unixTime: msg.sent, 397 reactions: msg.reactions, 398 coursicleID: msg.coursicleID, 399 live: true 400 } 401 //this mess is necessary because we have to know whether resource view is open. 402 //e.g., try having resource view open while someone sends you a link 403 if(msg.type == "resource" || (msg.type == "text" && linkify.find(msg.data).length > 0)){ 404 if(window.isPWA){ 405 if(window.modalStack && !(window.modalStack.includes('modal#showAllResourcesModal'))){ 406 getResources(activeChatID(), false,null) 407 }else{ 408 getResources(activeChatID(), true,null) 409 } 410 }else{ 411 if(!$('#showAllResourcesModal').is(':visible')){ 412 getResources(activeChatID(), false,null) 413 }else{ 414 getResources(activeChatID(), true,null) 415 } 416 } 417 } 418 419 if (!sentByMeOnAnotherDevice) { 420 var messageHTML = renderInMessage(message) 421 } 422 else { 423 var messageHTML = renderOutMessage(message) 424 $(".delivered").remove() 425 renderDeliveredMessage(0) 426 } 427 428 // move this chat to the top of the list of chats 429 usersChatsCopy = usersChats() 430 var previousIndexOfChat = usersChatsCopy.findIndex(chat => chat.id == msg.chatID); 431 arraymove(usersChatsCopy, previousIndexOfChat, 0); 432 store.set("usersChats", usersChatsCopy) 433 434 const chatListItem = $(`.chatListItem[data-id='${msg.chatID}']`) 435 var unreadCount = 0 436 var newMessages = 0; 437 usersChatsCopy = usersChats() 438 usersChatsCopy.forEach(chat => { 439 if (!window.isChatMuted(chat)) 440 { 441 newMessages += Number(chat.unread) 442 } 443 444 if (chat.id == Number(msg.chatID)) 445 { 446 unreadCount = chat.unread 447 } 448 449 }) 450 451 // if msg for currently visible chat, add to message ul 452 // else update unread badge count for chat list item 453 if (activeChatID() == msg.chatID) { 454 if ($('.noMessages').length != 0) $('.noMessages').hide() 455 456 let mustScrollToBottom = false 457 if (Math.abs($('#messagesContainer').scrollTop() + $('#messagesContainer').innerHeight() - $('#messagesContainer')[0].scrollHeight) < 100){ 458 mustScrollToBottom = true 459 } 460 461 // if incoming message was the last message being typed 462 // replace typing message with incoming message 463 if (window.chatState[msg.chatID].typingMessage && 464 window.chatState[msg.chatID].isTyping.length == 0) { 465 window.chatState[msg.chatID].typingMessage = false 466 // debugger 467 $('#typingMessage').replaceWith(messageHTML) 468 } 469 // otherwise, just add incoming message after last received 470 else { 471 $(".message:last").after(messageHTML) 472 // $('#messagesContainer')[0].scrollTo({ 473 // top: $('#messagesContainer')[0].scrollHeight, 474 // behavior: "instant", 475 // }) 476 } 477 //If within 100 pixels of the bottom, scroll down. This prevents scroll down if someone is viewing old messages but preserves the scroll if you are already at the bottom. 478 if (mustScrollToBottom) { 479 $('#messagesContainer')[0].scrollTo({ 480 top: $('#messagesContainer')[0].scrollHeight, 481 behavior: "instant", 482 }) 483 } 484 } 485 486 // if msg is for a chat other than the one that is currently visible 487 else { 488 if (!sentByMeOnAnotherDevice && !window.isChatMuted(null, chatID = Number(msg.chatID))) { 489 $(chatListItem).find('.notificationContainer').css('display', 'flex') 490 let incrementedNumber = Number(unreadCount) + 1 491 $(chatListItem).find('.chatNotification') 492 .text((incrementedNumber > 99) ? "99+" : incrementedNumber) 493 .css({ 'display': 'flex', 'visibility': 'visible' }) 494 } 495 } 496
497 // reorder chat list items, putting most recently updated first 498 $('#chatList').prepend(chatListItem) 499 500 // scroll msg list down 501 // scrollToLastMessage("smooth") 502 503 // add it to local storage and update the relevant metadata (unread count) 504 usersChatsCopy = usersChats() 505 usersChatsCopy.forEach(chat => { 506 if (chat.id == msg.chatID) { 507 // add the message to the front 508 chat.recentMessages.unshift(msg) 509 // if they're not currently looking at this chat, that means that the unread count has incremented 510 // so update that too 511 if (msg.chatID != activeChatID() && !sentByMeOnAnotherDevice) { 512 chat.unread = Number(chat.unread) + 1 513 } 514 } 515 }) 516 517 // save it 518 store.set("usersChats", usersChatsCopy) 519 520 // update the count 521 updateGlobalChatUnreadIndicators(usersChats()); 522 523 // update chat list item snippet and timestamp 524 renderUpdateSnippet(msg.chatID) 525 $(`.chatListItem[data-id='${msg.chatID}'] .chatTimestamp`).text("Just Now") 526 $('#chatList').scrollTop(0) 527 }) 528 529 // message reactions logic 530 window.socket.on('reaction', reaction => { 531 const messageEl = $(`[data-id=${reaction.messageID}]`) 532 if (!messageEl.length) { 533 const chats = store.get('usersChats') 534 chats.forEach(chat => { 535 chat.recentMessages.forEach(msg => { 536 if (Number(msg.id) === reaction.messageID) { 537 if (reaction.action === 'add') { 538 if (!msg.reactions) { 539 msg.reactions = [{ 540 reaction: reaction.reaction, 541 reactor: reaction.coursicleID, 542 }] 543 } else { 544 msg.reactions.push({ 545 reaction: reaction.reaction, 546 reactor: reaction.coursicleID, 547 }) 548 } 549 } else { 550 const index = msg.reactions.findIndex( 551 r => Number(r.reactor) === reaction.coursicleID 552 ) 553 msg.reactions.splice(index, 1) 554 } 555 } 556 }) 557 }) 558 store.set('usersChats', chats) 559 return 560 } 561 562 const messageReactionsEl = $(messageEl).find('.messageReactions') 563 const action = reaction.action // "add" or "remove" 564 565 if (action == "add" && reaction.chatID == activeChatID()) { 566 playReactSound() 567 } else if (reaction.chatID == activeChatID()) { 568 playUndoReactSound() 569 } 570 571 //phone vibration 572 vibrateIfAble(10); 573 574 var reactionIncrement = (action == "add") ? 1 : -1 //if "add", we will later add 1 to reaction count, and the opposite. 575 const reactionCount = messageEl.attr("data-reactioncount") 576 var isImg = $('#messagesContainer').find('[data-id=' + reaction.messageID + ']').find('.imageContainer').length > 0 ? true : false 577 578 //handling the rendering of the initial heart on first like 579 //changing the whitespace in divs here changes the whitespace of a message. 580 if (!messageReactionsEl.length) { 581 $(messageEl).find('.messageReactions').remove() 582 if (messageEl.hasClass('inMessage')) { 583 $(messageEl).find('.inAuthorAndMessageContainer').append(`<div class='inMessageReactions messageReactions'>â¤ï¸</div>`) 584 $(messageEl).addClass('withReactions') 585 } else { 586 $(messageEl).find(isImg ? '.imageContainer' : '.outMessageText').append(`<div class='outMessageReactions messageReactions'>â¤ï¸</div>`) 587 $(messageEl).addClass('withReactions') 588 //move down any delivered/not delivered receipts 589 if ($(messageEl).find('.delivered').length != 0) { $(messageEl).find('.delivered').css("bottom", "-3.7vh") } 590 else if ($(messageEl).find('.notDelivered').length != 0) { $(messageEl).find('.notDelivered').css("bottom", "-3.7vh") } 591 } 592 } 593 //handling rendering of all non-1 like counts 594 if (Number(reactionCount) + reactionIncrement > 0) { 595 messageReactionsEl.text(`â¤ï¸ ${(Number(reactionCount) + reactionIncrement <= 1) ? "" : " " + (Number(reactionCount) + reactionIncrement)}`) 596 } 597 else { 598 messageReactionsEl.remove() 599 } 600 messageEl.attr("data-reactioncount", Number(reactionCount) + reactionIncrement) 601 $(".inMessageReactions.messageReactions").each(function () { correctInAuthorNameHeartOffset($(this)) }) 602 }) 603}
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.