PageSourceSearch

http://scripting.com/chat/code/chatapp.js

js scripting.com collected 2026-10-01 08:31:21 UTC 29,639 bytes, 932 lines download raw bytes

1var appPrefs = {
2	savedTextArea: "",
3	flPostOnReturnKey: false,
4	flEchoPostsToTwitter: false,
5	hashTagForTwitter: "", flSendHashTagToTwitter: true
6	};
7
8function chatApp (options, callback) {
9	var chatOptions = {
10		flReadOnlyMode: false,
11		flAutoSave: true, minSecsBetwAutoSaves: 1,
12		urlChatServer: undefined,
13		urlChatSocket: undefined,
14		editorPlaceholderText: "",
15		userInfoFromTwitter: undefined, //this should come from the identity system
16		flEveryoneFollowsEnabled: false, //5/8/19 by DW -- turn the everyoneFollows feature on by setting this true
17		everyoneFollows: {"davewiner": true}, //8/23/17 by DW -- screennames of users everyone follows
18		defaultEditorButtons: ["bold", "italic", "anchor", "h4", "orderedlist", "unorderedlist", "quote"],
19		maxTweetLength: 280, //1/7/18 by DW
20		flJustViewMsg: false, //5/11/19 by DW
21		newMessageCallback: function (jstruct) {
22			},
23		updatedMessageCallback: function (jstruct) {
24			},
25		getConfigCallback: function () {
26			var emptyConfig = {
27				};
28			return (emptyConfig);
29			},
30		dynamicElementsCallback: function () {
31			},
32		processTextCallback: function (s) {
33			return (s);
34			}
35		};
36	var chatGlobals = {
37		chatlog: new Array (),
38		chatlogSerialnum: 0,
39		myChatLogSocket: undefined,
40		idLastTweet: undefined,
41		flMsgBeingEdited: false, 
42		idMsgBeingEdited: undefined,
43		objMsgBeingEdited: undefined, //4/20/19 by DW
44		flSetupDynamicElements: false,
45		flMessageNotPosted: undefined,
46		whenLastAutoSave: new Date (),
47		urlDefaultIcon: "http://static.scripting.com/larryKing/images/2012/10/16/clarus.gif", //8/21/17 by DW
48		initialEditorText: "<p><br></p>",
49		flPrefsChanged: false,
50		flJsonDisplayMode: false,
51		serverStats: undefined //a copy of the stats object returned from the server
52		};
53	
54	function settingsCommand () {
55		prefsDialogShow (function () {
56			prefsChanged ();
57			});
58		}
59	function chatGetWhen (jstruct) {
60		var when;
61		if (jstruct.pubDate !== undefined) {
62			when = jstruct.pubDate;
63			}
64		else {
65			when = jstruct.when;
66			}
67		return (new Date (when));
68		}
69	function getWhenLink (jstruct) {
70		var whenstring = getFacebookTimeString (chatGetWhen (jstruct));
71		if (jstruct.permalink !== undefined) {
72			whenstring = "<a href=\"" + jstruct.permalink + "\">" + whenstring + "</a>";
73			}
74		return (whenstring);
75		}
76	function viewChatMsg (jstruct) {
77		var htmltext = "", indentlevel = 0, flAddContainer = false;
78		
79		function add (s) {
80			htmltext += filledString ("\t", indentlevel) + s + "\n";
81			}
82		function getBodyText () {
83			function removeSchmutzFromText (theText) { //8/24/17 by DW -- remove extra stuff added by medium-editor
84				if (endsWith (theText, chatGlobals.initialEditorText)) {
85					theText = stringMid (theText, 1, theText.length - chatGlobals.initialEditorText.length);
86					}
87				return (theText);
88				}
89			var bodytext = removeSchmutzFromText (jstruct.text);
90			
91			function getTitle (flPeriod) {
92				let theTitle = jstruct.title;
93				if ((theTitle !== undefined) && (theTitle.length > 0)) {
94					if (flPeriod) {
95						theTitle = addPeriodAtEnd (theTitle);
96						}
97					return ("<span class=\"spChatTitle\">" + theTitle + "</span>");
98					}
99				else {
100					return ("");
101					}
102				}
103			
104			if (jstruct.enclosure !== undefined) { //see if it's an image
105				if (jstruct.enclosure.type !== undefined) {
106					if (beginsWith (jstruct.enclosure.type, "image/")) {
107						let htmltext = getTitle (false) + "<img src=\"" + jstruct.enclosure.url + "\">";
108						return (htmltext);
109						}
110					}
111				}
112			
113			if (jstruct.outline !== undefined) {
114				if (jstruct.outline.subs !== undefined) {
115					let htmltext = renderOutlineBrowser (jstruct.outline, false, undefined, undefined, true);
116					return (htmltext);
117					}
118				else {
119					bodytext = jstruct.outline.text; //single-headline outline, keep it simple --7/28/17 by DW
120					if (jstruct.outline.image !== undefined) {
121						bodytext = "<img src=\"" + jstruct.outline.image + "\" style=\"float: right; margin-left: 1px;
121 margin-top: 1px;\">" + bodytext;
122						}
123					}
124				}
125			bodytext = getTitle (true) + bodytext;
126			
127			bodytext = chatOptions.processTextCallback (bodytext);
128			
129			//fancy stuff with show more/show less, disabled
130				if (false) {
131					var maxchars = 400, showMore = "", remainingText = "";
132					if (bodytext.length > maxchars) {
133						var idRemainingText = "rt" + rtSerialnum, idShowMoreLink = "sm" + rtSerialnum++;
134						showMore = " <a id=\"" + idShowMoreLink + "\" class=\"aShowMoreLink\" onclick=\"showMore ('" + idRemainingText + "', '" + idShowMoreLink + "')\">Show more</a>";
135						bodytext = maxStringLength (bodytext, maxchars, true, false);
136						remainingText = "</p><span class=\"spHiddenText\" id=\"" + idRemainingText + "\"><p>" + stringMid (jstruct.text, bodytext.length, jstruct.text.length - bodytext.length + 1) + "</span>";
137						}
138					bodytext = bodytext  + remainingText + showMore
139					}
140			
141			if (jstruct.screenname == twGetScreenName ()) {
142				bodytext = "<div class=\"divClickableMsg\" data-idmsg=\"" + jstruct.id + "\">" + bodytext + "</div>";
143				chatGlobals.flSetupDynamicElements = true;
144				}
145			return (bodytext);
146			}
147		function getIcon () {
148			if (jstruct.urlIcon == "http://pbs.twimg.com/profile_images/888857400886906881/TfGkETi-_normal.jpg") {
149				jstruct.urlIcon = undefined;
150				}
151			if (jstruct.urlIcon === undefined) {
152				return (chatGlobals.urlDefaultIcon); //1/1/18 by DW
153				}
154			else {
155				return (jstruct.urlIcon);
156				}
157			}
158		function getAuthorLink (linktext) {
159			var authorlink;
160			if (jstruct.screenname !== undefined) {
161				authorlink = "<a href=\"https://twitter.com/" + jstruct.screenname + "/\" target=\"_blank\">" + linktext + "</a>";
162				}
163			else {
164				authorlink = linktext;
165				}
166			return (authorlink);
167			}
168		function getCheckbox () { //1/13/18 by DW
169			if (userIsSysop ()) {
170				var theChecked = ""; //not checked initially
171				var theCheckbox = "<input type=\"checkbox\" data-id=\"" + jstruct.id + "\" value=\"xxx\" " + theChecked + ">";
172				return ("<div class=\"divChatMsgCheckbox\">" + theCheckbox + "</div>");
173				}
174			else {
175				return ("");
176				}
177			}
178		
179		if (jstruct.text.length == 0) { //8/25/17 by DW
180			return (""); 
181			}
182		
183		if (jstruct.idHtmlObject === undefined) {
184			jstruct.serialnum = chatGlobals.chatlogSerialnum++;
185			jstruct.idHtmlObject = "idChatMsg" + jstruct.serialnum;
186			flAddContainer = true;
187			}
188		if (flAddContainer) {
189			add ("<div class=\"divChatMsgContainer\" id=\"" + jstruct.idHtmlObject + "\">"); indentlevel++;
190			}
191		
192		if (chatGlobals.flJsonDisplayMode) { //1/8/18; by DW
193			let jsontext = jsonStringify (jstruct);
194			let jstructcopy = JSON.parse (jsontext);
195			
196			function deleteValue (name) {
197				try {
198					delete jstructcopy [name];
199					}
200				catch (err) {
201					}
202				}
203			deleteValue ("urlIcon");
204			deleteValue ("serialnum");
205			deleteValue ("idHtmlObject");
206			
207			jsontext = jsonStringify (jstructcopy);
208			
209			jsontext = replaceAll (jsontext, "<", "&lt;");
210			add ("<div class=\"divJsonText\"><pre>" + jsontext + "</pre></div>");
211			}
212		else {
213			add (getCheckbox ());
214			add ("<table>"); indentlevel++;
215			add ("<tr>"); indentlevel++;
216			//icon
217				add ("<td class=\"tdIconInChat\">"); indentlevel++;
218				add ("<div class=\"divUserIcon\">" + getAuthorLink ("<img src=\"" + getIcon () + "\" width=\"48\" height=\"48\" border=\"0\">") + "</div>");
219				add ("</td>"); indentlevel--;
220			//text
221				add ("<td class=\"tdTextInChat\">"); indentlevel++;
222				//top line
223					add ("<div class=\"divChatTopLine\">"); indentlevel++;
224					add ("<span class=\"spChatAuthorName\">" + getAuthorLink (jstruct.authorname) + "</span>");
225					add ("<span class=\"spChatWhen\" id=\"idWhen" + jstruct.serialnum + "\">" + getWhenLink (jstruct) + "</span>");
226					
227					if (jstruct.idTweet !== undefined) { //9/28/16 by DW
228						var urlForTweet = "https://twitter.com/" + jstruct.screenname + "/status/" + jstruct.idTweet;
229						add ("<span class=\"spChatTweetLink\"><a href=\"" + urlForTweet + "\" target=\"_blank\">" + twStorageConsts.fontAwesomeIcon + "</a></span>");
230						}
231					
232					add ("</div>"); indentlevel--;
233				add ("<div class=\"divChatBody\">" + getBodyText () + "</div>");
234				//linktext -- 8/15/17 by DW
235					var linktext = "";
236					if ((jstruct.link != undefined) && (jstruct.link.length > 0)) {
237						var splitUrl = urlSplitter (trimLeading (jstruct.link, " "));
238						var host = splitUrl.host;
239						if (beginsWith (host, "www.")) {
240							host = stringDelete (host, 1, 4);
241							}
242						linktext = "<span class=\"spLink\"><a class=\"aHost\" href=\"" + jstruct.link + "\" target=\"blank\">
242" + host + "</a></span>";
243						}
244				//likes -- 10/8/16 by DW
245					(function () {
246						var ct = 0, likenames = "", thumbDirection = "up";
247						if (jstruct.likes !== undefined) {
248							for (var x in jstruct.likes) {
249								ct++;
250								likenames += x + ", ";
251								}
252							}
253						
254						var thumbUp = "";
255						if (twIsTwitterConnected ()) {
256							thumbUp = "<span class=\"spThumb\"><a class=\"aLikeIcon\" data-idmsg=\"" + jstruct.id + "\"><i class=\"fa fa-thumbs-" + thumbDirection + "\"></i></a></span>&nbsp;";
257							}
258						
259						var ctLikes = ct + " like";
260						
261						if (ct == 0) {
262							ctLikes = ""; //get rid of 0 Likes -- 8/24/17 by DW
263							}
264						else {
265							if (ct != 1) {
266								ctLikes += "s";
267								}
268							if (ct > 0) {
269								likenames = stringMid (likenames, 1, likenames.length - 2); //pop off comma and blank at end
270								ctLikes = "<span rel=\"tooltip\" title=\"" + likenames + "\">" + ctLikes + "</span>";
271								}
272							}
273						
274						add ("<div class=\"divChatBottomLine\">" + linktext + "<span class=\"spLikes\">" + thumbUp + ctLikes + "</span></div>");
275						}) ();
276				add ("</td>"); indentlevel--;
277			add ("</tr>"); indentlevel--;
278			add ("</table>"); indentlevel--;
279			}
280		
281		if (flAddContainer) {
282			add ("</div>"); indentlevel--;
283			}
284		return (htmltext);
285		}
286	function chatSocketStart (urlSocket, callback) {
287		if (chatGlobals.socket === undefined) {
288			chatGlobals.socket = new WebSocket (urlSocket); 
289			chatGlobals.socket.onopen = function (evt) {
290				console.log ("chatGlobals.socket is open, sending \"watch chatlog\" message.");
291				viewServerStats ();
292				chatGlobals.socket.send ("watch chatlog");
293				};
294			chatGlobals.socket.onmessage = function (evt) {
295				var s = evt.data;
296				if (s !== undefined) { //no error
297					var verb = stringNthField (s, "\r", 1);
298					s = stringDelete (s, 1, verb.length + 1);
299					callback (verb, s);
300					}
301				};
302			chatGlobals.socket.onclose = function (evt) {
303				console.log ("chatGlobals.socket was closed.");
304				chatGlobals.socket = undefined;
305				};
306			chatGlobals.socket.onerror = function (evt) {
307				console.log ("chatGlobals.socket received an error");
308				};
309			}
310		}
311	function checkChatSocket () {
312		chatSocketStart (chatOptions.urlChatSocket, function (verb, s) {
313			console.log ("checkChatSocket: verb == " + verb);
314			switch (verb.toLowerCase ()) {
315				case "update":
316					var jstruct = JSON.parse (s);
317					for (var i = 0; i < chatGlobals.chatlog.messages.length; i++) { //see if an existing message was updated
318						var item = chatGlobals.chatlog.messages [i];
319						if (item.id == jstruct.id) {
320							for (var x in jstruct) { //8/27/17 by DW -- don't eliminate items that aren't present in jstruct
321								item [x] = jstruct [x];
322								}
323							
324							if (isPostVisible (item)) {
325								$("#" + item.idHtmlObject).html (viewChatMsg (item)); 
326								}
327							console.log ("chatlogEverySecond: existing object updated == " + jsonStringify (jstruct));
328							chatOptions.updatedMessageCallback (jstruct); //8/11/17 by DW
329							chatGlobals.flSetupDynamicElements = true;
330							return;
331							}
332						}
333					console.log ("chatlogEverySecond: new object == " + jsonStringify (jstruct));
334					chatGlobals.chatlog.messages.unshift (jstruct);
335					if (isPostVisible (jstruct)) {
336						$("#idChatlogViewer").prepend (viewChatMsg (jstruct));
337						chatOptions.newMessageCallback (jstruct); //8/11/17 by DW
338						}
339					break;
340				case "stats": //5/11/19 by DW
341					chatGlobals.serverStats = JSON.parse (s);
342					viewServerStats ();
343					console.log ("checkChatSocket: chatGlobals.serverStats == " + jsonStringify (chatGlobals.serverStats));
344					break;
345				case "rollover":
346					chatlogStart ();
347					break;
348				case "reload":
349					window.location.href  = window.location.href; 
350					break;
351				case "deleteitem":
352					var jstruct = JSON.parse (s);
353					deleteMessage (jstruct.id)
354					break;
355				}
356			});
357		}
358	function serverCall (verb, params, flAuthenticated, callback, method, postbody) {
359		if (flAuthenticated === undefined) {
360			flAuthenticated = true;
361			}
362		if (params === undefined) {
363			params = new Object ();
364			}
365		if (method === undefined) {
366			method = "GET";
367			}
368		if (flAuthenticated) {
369			params.oauth_token = localStorage.twOauthToken;
370			params.oauth_token_secret = localStorage.twOauthTokenSecret;
371			}
372		
373		var apiUrl = chatOptions.urlChatServer + verb;
374		var paramString = twBuildParamList (params);
375		if (paramString.length > 0) {
376			apiUrl += "?" + paramString;
377			}
378		
379		console.log ("serverCall: verb == " + verb + ", apiUrl == " + apiUrl);
380		
381		$.ajax ({
382			type: method,
383			url: apiUrl,
384			data: postbody, 
385			success: function (data) {
386				if (callback !== undefined) {
387					callback (undefined, data);
388					}
389				},
390			error: function (status, something, otherthing) { 
391				console.log ("serverCall: error == " + JSON.stringify (status, undefined, 4));
392				if (callback !== undefined) {
393					var err = {
394						code: status.status,
395						message: JSON.parse (status.responseText).message
396						};
397					if (callback !== undefined) {
398						callback (err);
399						}
400					}
401				},
402			dataType: "json"
403			});
404		}
405	function isPostVisible (jstruct) { //8/23/17 by DW
406		var theName = jstruct.screenname;
407		if (theName === undefined) { //the post came from scripting news, or linkblog, or...
408			return (true);
409			}
410		if (theName == twGetScreenName ()) { //it's the user's own post
411			return (true);
412			}
413		if (!getBoolean (chatOptions.flEveryoneFollowsEnabled)) { //5/8/19 by DW
414			return (true);
415			}
416		if (getBoolean (chatOptions.everyoneFollows [theName])) {
417			return (true);
418			}
419		if (twGetScreenName () == "davewiner") { //for now I see everything
420			return (true);
421			}
422		return (false);
423		}
424	function prefsChanged () {
425		chatGlobals.flPrefsChanged = true;
426		}
427	function getPrefs (callback) {
428		if (twIsTwitterConnected ()) {
429			serverCall ("getprefs", undefined, true, function (err, data) {
430				if (!err) {
431					appPrefs = data;
432					console.log ("getPrefs: appPrefs == " + jsonStringify (appPrefs));
433					}
434				callback ();
435				});
436			}
437		else {
438			callback ();
439			}
440		}
441	function savePrefs (callback) {
442		var whenstart = new Date ();
443		var params = {
444			jsontext: jsonStringify (appPrefs)
445			}
446		serverCall ("saveprefs", params, true, function (err, data) {
447			if (!err) {
448				console.log ("savePrefs: " + secondsSince (whenstart) + " secs");
449				}
450			if (callback !== undefined) {
451				callback ();
452				}
453			});
454		}
455	function getConfig () {
456		return (chatOptions.getConfigCallback ());
457		}
458	function showEditControls () {
459		$("#idStuffThatsHiddenWhenNotActive").css ("visibility", "visible");
460		$("#idPostButton").css ("visibility", "visible");
461		}
462	function hideEditControls () {
463		$("#idStuffThatsHiddenWhenNotActive").css ("visibility", "hidden");
464		$("#idPostButton").css ("visibility", "hidden");
465		}
466	function visitCheckedMessages (callback) {
467		$(".divChatMsgCheckbox input").each (function () {
468			if ($(this).prop ("checked")) {
469				var id = $(this).data ("id");
470				console.log ("visitCheckedMessages: id == " + id);
471				if (callback !== undefined) {
472					callback (id);
473					}
474				}
475			});
476		}
477	function likeClick (idMessage, callback) {
478		var params = {
479			id: idMessage
480			};
481		serverCall ("like", params, true, callback);
482		}
483	function chatlogDeleteItem (idMessage, callback) {
484		var params = {
485			id: idMessage
486			};
487		serverCall ("delete", params, true, callback);
488		}
489	function getChatlog (callback) {
490		serverCall ("getchatlog", undefined, false, callback);
491		}
492	function getMyScreenName (callback) {
493		serverCall ("getmyscreenname", undefined, true, function (err, data) {
494			callback (data);
495			});
496		}
497	function getMainEditorText (flStripMarkup) {
498		var s = $("#idBodyEditor").html ();
499		if (flStripMarkup) {
500			s = stripMarkup (s);
501			}
502		s = replaceAll (s, "&nbsp;", " "); //sometimes medium-editor appends a &nbsp; at the end
503		return (s);
504		}
505	function getMainEditorTextLength (flStripMarkup) {
506		if (flStripMarkup === undefined) {
507			flStripMarkup = true;
508			}
509		return (getMainEditorText (flStripMarkup).length);
510		}
511	function getHashTag () { //includes the space separating the tag from the text
512		var s = trimWhitespace (appPrefs.hashTagForTwitter);
513		if (s.length == 0) {
514			return ("");
515			}
516		return (" " + s);
517		}
518	function setEditorText (bodytext) {
519		$("#idBodyEditor").html (bodytext);
520		var editorBody = new MediumEditor (".divBodyEditor", {
521			placeholder: {
522				text: " " + chatOptions.editorPlaceholderText
523				},
524			toolbar: {
525				buttons: chatOptions.defaultEditorButtons,
526				},
527			buttonLabels: "fontawesome",
528			imageDragging: false, //10/18/16 by DW
529			autoLink: true
530			});
531		}
532	function showMore (idRemainingText, idShowMoreLink) {
533		console.log ("showMore: idRemainingText == " + idRemainingText + ", idShowMoreLink == " + idShowMoreLink);
534		if ($("#" + idRemainingText).css ("display") == "inline") { //it's visible, hide it
535			$("#" + idRemainingText).css ("display", "none");
536			$("#" + idShowMoreLink).text ("Show more");
537			}
538		else { //it's hidden, make it visible
539			$("#" + idRemainingText).css ("display", "inline");
540			$("#" + idShowMoreLink).text ("Show less");
541			}
542		}
543	function getEditorJstruct () {
544		var jstruct = {
545			text: getMainEditorText (),
546			authorname: chatOptions.userInfoFromTwitter.name,
547			screenname: twGetScreenName (),
548			urlIcon: chatOptions.userInfoFromTwitter.profile_image_url,
549			when: new Date (),
550			idTweet: chatGlobals.idLastTweet
551			};
552		return (jstruct);
553		}
554	function chatlogRollover (callback) {
555		serverCall ("rollover", undefined, true, callback);
556		}
557	function chatlogReload (callback) {
558		serverCall ("reload", undefined, true, callback);
559		}
560	function updateCharCount () {
561		var ct = getMainEditorTextLength ();
562		if (appPrefs.flSendHashTagToTwitter) {
563			ct += getHashTag ().length
564			}
565		if (appPrefs.flEchoPostsToTwitter) {
566			if (ct > chatOptions.maxTweetLength) {
567				ct = "<span class=\"spRedCharCount\">" + ct + "</span>";
568				}
569			}
570		$("#idCharCount").html (ct);
571		}
572	function postNewMsg (callback) {
573		if (getMainEditorTextLength () > 0) {
574			var postbody = jsonStringify (getEditorJstruct ());
575			serverCall ("post", undefined, true, callback, "POST", postbody);
576			}
577		else {
578			if (callback !== undefined) {
579				callback (undefined);
580				}
581			}
582		}
583	function updateExistingMsg (callback) {
584		console.log ("updateExistingMsg: chatGlobals.idMsgBeingEdited == " + chatGlobals.idMsgBeingEdited);
585		var params = {
586			id: chatGlobals.idMsgBeingEdited,
587			text: getMainEditorText ()
588			};
589		serverCall ("updatetext", params, true, callback);
590		}
591	function resetEditor () {
592		setEditorText (chatGlobals.initialEditorText);   
593		updateCharCount ();
594		setPostButtonText ();
595		hideEditControls ();
596		$("#idBodyEditor").blur (); //take the focus off the editor
597		}
598	function cancelEdit () {
599		chatGlobals.flMsgBeingEdited = false; 
600		chatGlobals.idMsgBeingEdited = undefined;
601		if (chatGlobals.objMsgBeingEdited) { //4/20/19 by DW
602			$(chatGlobals.objMsgBeingEdited).removeClass ("divMsgBeingEdited"); //4/20/19 by DW
603			chatGlobals.objMsgBeingEdited = undefined;
604			}
605		resetEditor ();
606		}
607	function postButtonClick () {
608		if (chatOptions.flReadOnlyMode) {
609			alertDialog ("Sorry, " + getConfig ().productnameForDisplay + " is operating in read-only mode at this time.");
610			}
611		else {
612			function doPost () {
613				postNewMsg (function (err, data) {
614					console.log ("postButtonClick: the server returned == " + jsonStringify (data));
615					resetEditor ();
616					});
617				}
618			if (chatGlobals.flMsgBeingEdited) {
619				updateExistingMsg (function (err, data) {
620					console.log ("postButtonClick: the server returned == " + jsonStringify (data));
621					chatGlobals.flMsgBeingEdited = false;
622					chatGlobals.idMsgBeingEdited = undefined;
623					resetEditor ();
624					});
625				}
626			else {
627				if (appPrefs.flEchoPostsToTwitter) {
628					var s = getMainEditorText (true);
629					s = replaceAll (s, "&nbsp;", " ");
630					s = trimWhitespace (s);
631					if (s.length > 0) {
632						if (appPrefs.flSendHashTagToTwitter) {
633							s += getHashTag ();
634							}
635						console.log ("postButtonClick: what we're tweeting == " + s);
636						twTweet (s, "", function (data) {
637							chatGlobals.idLastTweet = data.id_str; //set global
638							doPost ();
639							});
640						}
641					}
642				else {
643					chatGlobals.idLastTweet = undefined;
644					doPost ();
645					}
646				}
647			}
648		}
649	function cancelButtonClick () {
650		if (chatGlobals.flMsgBeingEdited) {
651			cancelEdit ();
652			}
653		}
654	function getServerStats (callback) {
655		serverCall ("stats", undefined, false, function (err, server) {
656			chatGlobals.serverStats = server; 
657			if (callback !== undefined) {
658				callback (err, server);
659				}
660			});
661		}
662	function setupDynamicElements () {
663		$("#idChatlogViewer *").off (); //remove all pre-existing handlers attached to the chatlog -- 1/7/18 by DW
664		$('[rel="tooltip"]').tooltip ({
665			animation: true,
666			placement: "right"
667			});
668		$("a[rel=tooltip]").tooltip ({
669			live: true
670			});
671		$(".divClickableMsg").on ("click", function (event) {
672			var idmsg = $(this).data ("idmsg");
673			$(this).addClass ("divMsgBeingEdited"); //4/20/19 by DW
674			chatGlobals.objMsgBeingEdited = this; //4/20/19 by DW
675			editMessage (idmsg);
676			event.stopPropagation ();
677			});
678		$(".aLikeIcon").on ("click", function (event) {
679			var idmsg = $(this).data ("idmsg");
680			console.log ("You clicked on a .aLikeIcon, iidmsg == " + idmsg);
681			likeClick (idmsg);
682			event.stopPropagation ();
683			});
684		$("#idBodyEditor").focus (function (event) {
685			showEditControls (); //4/20/19 by DW
686			chatGlobals.flMessageNotPosted = true;
687			showEditControls ();
688			});
689		$("#idBodyEditor").blur (function (event) {
690			console.log ("You clicked away from #idBodyEditor.");
691			if ($("#medium-editor-toolbar-1").css ("visibility") == "hidden") { //don't hide controls if the toolbar got the click -- 1/12/18 by DW
692				hideEditControls ();
693				}
694			});
695		$("#idBodyEditor").keyup (function (event) {
696			if (event.which == 27) { //escape key
697				console.log ("You pressed the ESC key in an editing text area.");
698				if (chatGlobals.flMsgBeingEdited) {
699					cancelEdit ();
700					}
701				else {
702					hideEditControls ();
703					}
704				$("#idBodyEditor").blur (); //take the focus off the editor
705				event.stopPropagation ();
706				return;
707				}
708			updateCharCount ();
709			});
710		$("#idBodyEditor").keydown (function () {
711			if (event.which == 13) { //return key
712				if (appPrefs.flPostOnReturnKey) {
713					postButtonClick ();
714					event.stopPropagation ();
715					return;
716					}
717				}
718			});
719		$("#idTweetCheckbox").click (function () {
720			console.log ("You clicked the checkbox, its value is " + this.checked);
721			appPrefs.flEchoPostsToTwitter = getBoolean (this.checked);
722			updateCharCount ();
723			prefsChanged ();
724			});
725		
726		$("#idPostButton").mousedown (function (event) {
727			console.log ("You clicked on the post button.");
728			if (chatGlobals.flMessageNotPosted) { //it's a new click
729				postButtonClick ();
730				event.stopPropagation ();
731				event.preventDefault ();
732				chatGlobals.flMessageNotPosted = false;
733				}
734			else {
735				console.log ("Repeat mousedown event, not processed.");
736				}
737			});
738		$("#idCancelButton").mousedown (function (event) {
739			cancelButtonClick ();
740			$("#idBodyEditor").blur (); //take the focus off the editor
741			event.stopPropagation ();
742			event.preventDefault ();
743			});
744		
745		if (chatOptions.dynamicElementsCallback !== undefined) { //8/21/17 by DW
746			chatOptions.dynamicElementsCallback ();
747			}
748		}
749	function findMessage (id, callback) {
750		for (var i = 0; i < chatGlobals.chatlog.messages.length; i++) {
751			var item = chatGlobals.chatlog.messages [i];
752			if (item.id == id) {
753				callback (item, i);
754				return;
755				}
756			}
757		callback (undefined);
758		}
759	function viewChatlog () {
760		var htmltext = "";
761		for (var i = 0; i < chatGlobals.chatlog.messages.length; i++) {
762			let item = chatGlobals.chatlog.messages [i];
763			if (isPostVisible (item)) {
764				item.idHtmlObject = undefined;
765				htmltext += viewChatMsg (item);
766				}
767			}
768		$("#idChatlogViewer").html (htmltext);
769		chatGlobals.flSetupDynamicElements = true;
770		}
771	function toggleJsonDisplayMode () {
772		chatGlobals.flJsonDisplayMode = !chatGlobals.flJsonDisplayMode;
773		viewChatlog ();
774		}
775	function chatlogStart (callback) {
776		const whenstart = new Date ();
777		getServerStats (function () {
778			getChatlog (function (err, chatlogSubset) {
779				if (!err) {
780					console.log ("chatlogStart: " + chatlogSubset.messages.length + " messages, took " + secondsSince (whenstart) + " secs.");
781					chatGlobals.chatlog = chatlogSubset;
782					viewChatlog ();
783					if (callback !== undefined) {
784						callback ();
785						}
786					}
787				});
788			});
789		}
790	function userIsSysop () { //4/21/19 by DW
791		if (chatGlobals.serverStats === undefined) { //5/11/19 by DW
792			return (false);
793			}
794		else {
795			return (chatGlobals.serverStats.owner == twGetScreenName ());
796			}
797		}
798	function viewSysopMenu (server) {
799		var att = (userIsSysop ()) ? "block" : "none";
800		$("#idSysopMenu").css ("display", att);
801		}
802	function viewServerStats () {
803		var server = chatGlobals.serverStats;
804		$("#idServerStats").html ("Server: " + server.productName + " v" + server.version + ", open sockets: " + server.ctSockets + ".");
805		$("#idVersionNumber").text ("v" + server.version);
806		viewSysopMenu (server); //depends on server.owner
807		}
808	function setPostButtonText () {
809		var theText, cancelVisible;
810		if (chatGlobals.flMsgBeingEdited) {
811			theText = "Update";
812			cancelVisible = "visible";
813			}
814		else {
815			theText = "Post";
816			cancelVisible = "hidden";
817			}
818		$("#idPostButton").html ("<i class=\"fa fa-check\"></i> " + theText);
819		
820		$("#idCancelButton").css ("visibility", cancelVisible);
821		}
822	function editMessage (idmsg) {
823		findMessage (idmsg, function (item) {
824			console.log ("editMessage: item == " + jsonStringify (item));
825			setEditorText (item.text);
826			$("#idBodyEditor").focus ();
827			chatGlobals.flMsgBeingEdited = true;
828			chatGlobals.idMsgBeingEdited = idmsg;
829			setPostButtonText ();
830			});
831		}
832	function deleteMessage (idmsg) {
833		findMessage (idmsg, function (item, ix) {
834			$("#" + item.idHtmlObject).remove ();
835			chatGlobals.chatlog.messages.splice (ix, 1);
836			});
837		}
838	function showHideEditor () {
839		var displayval;
840		if (twIsTwitterConnected ()) {
841			displayval = "block";
842			}
843		else {
844			displayval = "none";
845			}
846		if ($("#idEditorSection").css ("display") !== displayval) { //you only see the editor if you're logged in
847			$("#idEditorSection").css ("display", displayval); 
848			}
849		
850		
851		}
852	function initEditor (initialText) {
853		if (initialText !== undefined) {
854			setEditorText (initialText);  //9/22/16 by DW
855			}
856		$("#idTweetCheckbox").attr ("checked", appPrefs.flEchoPostsToTwitter);
857		$("#idHashtagCheckbox").attr ("checked", appPrefs.flSendHashTagToTwitter);
858		updateCharCount (); //9/23/16 by DW
859		showHideEditor ();
860		}
861	function setValuesFromConfig () {
862		$("#idBodyEditor").attr ("data-placeholder", " " + chatOptions.editorPlaceholderText);
863		}
864	function everyMinute () {
865		var now = new Date ();
866		for (var i = chatGlobals.chatlog.messages.length - 1; i >= 0; i--) {
867			var item = chatGlobals.chatlog.messages [i];
868			$("#idWhen" + item.serialnum).html (getWhenLink (item));
869			}
870		}
871	function everySecond () {
872		checkChatSocket ();
873		if (chatGlobals.flSetupDynamicElements) {
874			setupDynamicElements ();
875			chatGlobals.flSetupDynamicElements = false;
876			}
877		if (chatOptions.flAutoSave) {
878			if (!chatGlobals.flMsgBeingEdited) {
879				if (secondsSince (chatGlobals.whenLastAutoSave) > chatOptions.minSecsBetwAutoSaves) {
880					var chattext = getMainEditorText ();
881					if (chattext != appPrefs.savedTextArea) {
882						appPrefs.savedTextArea = chattext;
883						chatGlobals.whenLastAutoSave = new Date ();
884						prefsChanged ();
885						}
886					}
887				}
888			}
889		if (chatGlobals.flPrefsChanged) {
890			savePrefs ();
891			chatGlobals.flPrefsChanged = false;
892			}
893		showHideEditor (); //1/3/18 by DW
894		}
895	
896	console.log ("chatApp");
897	this.rollover = chatlogRollover;
898	this.reload = chatlogReload;
899	this.deleteItem = chatlogDeleteItem;
900	this.likeClick = likeClick;
901	this.serverCall = serverCall; //1/4/18 by DW
902	this.initEditor = initEditor; //1/4/18 by DW
903	this.chatGlobals = chatGlobals; //1/7/18 by DW
904	this.prefs = appPrefs; //1/7/18 by DW
905	this.prefsChanged = prefsChanged; //1/9/18 by DW
906	this.toggleJsonDisplayMode = toggleJsonDisplayMode; //1/8/18 by DW
907	this.settingsCommand = settingsCommand; //1/9/18 by DW
908	this.visitCheckedMessages = visitCheckedMessages; //1/13/18 by DW
909	this.viewChatMsg = viewChatMsg; //5/11/19 by DW
910	this.getOptions = function () {
911		return (chatOptions);
912		};
913	for (var x in options) {
914		chatOptions [x] = options [x];
915		}
916	if (!chatOptions.flJustViewMsg) {
917		showHideEditor (); //4/20/19 by DW -- moved this to the beginning as an experiment
918		chatlogStart (function () {
919			setValuesFromConfig ();
920			getPrefs (function () {
921				initEditor (appPrefs.savedTextArea);  
922				self.setInterval (everySecond, 1000); 
923				runEveryMinute (everyMinute); 
924				callback ();
925				});
926			});
927		}
928	else { //5/11/19 by DW
929		callback (this);
930		}
931	}
932

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.