PageSourceSearch

https://www.coursicle.com/all/js/chat/socket.js?new12

js coursicle.com collected 2026-09-24 08:48:37 UTC 20,810 bytes, 603 lines download raw bytes

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.