fixes in hidden contact visibility
[dyskinesia.git] / html / js / clist.js
blob995869e5931caf2bd7c8c37402789549e7d56673
1 /*
2  * global vars
3  */
4 var clist = {};
5 var croom = {};
6 var clistSorted = [];
7 var croomSorted = [];
8 var blinkPhase = false;
9 var blinkTimer;
10 var myUNI;
11 var flgOffVisModeRoom = true;
12 var flgOffVisModePerson = true;
13 var flgHiddenVisMode = true;
14 var globalCnt = 1;
17 // statuses
18 const stInternal = 0;
19 const stOffline = 1;
20 const stVacation = 2;
21 const stAway = 3;
22 // others are avail
23 const stDND = 4;
24 const stNearby = 5;
25 const stBusy = 6;
26 const stHere = 7;
27 const stFFC = 8; // free-for-chat (talkative)
28 const stRealtime = 9; // unused
32  *
33  */
34 function setMyUNI (uni) {
35   myUNI = uni;
36   setPersonOfflineVisibility(api.getOption("/contactlist/offvisible/persons"));
37   setPlaceOfflineVisibility(api.getOption("/contactlist/offvisible/places"));
38   setHiddenVisibility(api.getOption("/contactlist/showhidden"));
39   //api.printLn(flgOffVisModeRoom);
40   //api.printLn(flgOffVisModePerson);
45  *
46  */
47 function chatStarted (uni) {
51 function optionChanged (optname) {
52   var v;
53   switch (optname) {
54     case "/contactlist/offvisible/persons":
55       v = api.getOption("/contactlist/offvisible/persons");
56       if (flgOffVisModePerson != v) setPersonOfflineVisibility(v);
57       break;
58     case "/contactlist/offvisible/places":
59       v = api.getOption("/contactlist/offvisible/places");
60       if (flgOffVisModeRoom != v) setPlaceOfflineVisibility(v);
61       break;
62     case "/contactlist/showhidden":
63       v = api.getOption("/contactlist/showhidden");
64       if (flgHiddenVisMode != v) setHiddenVisibility(v);
65       break;
66   }
70 function optionRemoved (optname) {
74 function strcmp (s0, s1) {
75   if (s0 < s1) return -1;
76   if (s0 > s1) return 1;
77   return 0;
80 function strcmpI (s0, s1) {
81   return strcmp(s0.toLowerCase(), s1.toLowerCase());
85 /* compare contacts (for sorting) */
86 function ccCompare (c0, c1) {
87   // contacts with unread messages comes first
88   if (c0.unread > 0) {
89     if (c1.unread < 1) return -1; // c0<c1
90   } else if (c1.unread > 0) {
91     if (c0.unread < 1) return 1; // c0>c1
92   }
93   // then by status
94   if (c0.status > c1.status) return -1; // c0<c1
95   if (c0.status < c1.status) return 1; // c0>c1
96   // then by verbatim
97   var res;
98   if (c0.verbatim && c1.verbatim) res = strcmpI(c0.verbatim, c1.verbatim); else res = 0;
99   // then by nick
100   if (!res) res = strcmpI(c0.nick, c1.nick);
101   // then by proto
102   if (!res) res = strcmpI(c0.proto, c1.proto);
103   // then by uni
104   if (!res) res = strcmpI(c0.uni, c1.uni);
105   return res;
109 /* compare rooms (for sorting) */
110 function crCompare (c0, c1) {
111   // by uni
112   return strcmpI(c0.uni, c1.uni);
116 function clistSwapDiv (c0, c1) {
117   if (!strcmpI(c0.uni, c1.uni)) return;
118   var d0 = c0.div;
119   var d1 = c1.div;
120   var p0 = d0.parentNode;
121   var p1 = d1.parentNode;
122   p0.removeChild(d0);
123   p1.removeChild(d1);
124   p0.appendChild(d1);
125   p1.appendChild(d0);
129 /* sort clist and rearrange divs */
130 // bubble sort! shame on me!
131 function sortCArray (sl, compFn) {
132   var swapped, was = false;
133   do {
134     swapped = false;
135     var len = sl.length-2;
136     for (var f = 0; f <= len; f++) {
137       var rc = compFn(sl[f], sl[f+1]);
138       if (rc > 0) {
139         clistSwapDiv(sl[f], sl[f+1]);
140         swapped = was = true;
141         var t = sl[f]; sl[f] = sl[f+1]; sl[f+1] = t;
142       }
143     }
144   } while (swapped);
145   //if (was) {
146   if (true) {
147     for (var f = 0; f <= sl.length-1; f++) {
148       var div = sl[f].div;
149       div.setAttribute("class", "contact contact"+(f%2)+
150         (sl[f].chatting?" chatting":"")+
151         (sl[f].unread>0?" unread":"")+
152         ""
153       );
154     }
155   }
156   return was;
160 function sortCList () {
161   sortCArray(clistSorted, ccCompare);
162   sortCArray(croomSorted, crCompare);
166 var statusInfo = {
167  images:[
168   "dys://theme/img/message.png",
169   "dys://theme/img/status/offline.png",
170   "dys://theme/img/status/vacation.png",
171   "dys://theme/img/status/away.png",
172   "dys://theme/img/status/dnd.png",
173   "dys://theme/img/status/nearby.png",
174   "dys://theme/img/status/busy.png",
175   "dys://theme/img/status/here.png",
176   "dys://theme/img/status/ffc.png",
177   "dys://theme/img/status/realtime.png"
178  ],
179  text:[
180   "",
181   "offline",
182   "vacation (n/a)",
183   "away",
184   "do not disturb",
185   "nearby",
186   "busy",
187   "here",
188   "free for chat",
189   "realtime"
193 function setContactStatusPic (cc) {
194   var div = cc.div, status = cc.status;
195   cc.imgStatus.title = statusInfo.text[status];
196   if (cc.unread > 0 && (blinkPhase || cc.skipUnreadCycle)) status = 0;
197   cc.imgStatus.src = statusInfo.images[status];
201 function onBlinkTimer () {
202   var cnt = 0;
203   var sl = clistSorted;
204   var bf = !blinkPhase;
205   blinkPhase = bf;
206   for (var f = sl.length-1; f >= 0; f--) {
207     var d = sl[f];
208     if (d.unread > 0) {
209       cnt++;
210       d.wasBlink = true;
211     } else if (d.wasBlink) {
212       delete d.wasBlink;
213     }
214     setContactStatusPic(d);
215   }
216   if (!cnt && blinkTimer) {
217     clearInterval(blinkTimer);
218     blinkTimer = null;
219   }
223 function checkBlinking () {
224   var doBlink = false;
225   var sl = clistSorted;
226   for (var f = sl.length-1; f >= 0; f--) {
227     var d = sl[f];
228     if (d.unread > 0) { doBlink = true; break; }
229   }
230   if (!doBlink) {
231     if (blinkTimer) {
232       clearInterval(blinkTimer);
233       blinkTimer = null;
234     }
235   } else if (!blinkTimer) blinkTimer = setInterval(onBlinkTimer, 300);
239 function targetToUserDiv (target) {
240   var div = target;
241   while (div) {
242     if (div.user) return div;
243     div = div.parentNode;
244   }
245   return false;
249 function onContactDblClick (e) {
250   var div = targetToUserDiv(e.target);
251   if (!div) return true;
252   e.preventDefault();
253   // remove selection
254   setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
255   api.startChat(div.user.uni);
256   return false;
260 function onPlaceDblClick (e) {
261   var div = targetToUserDiv(e.target);
262   if (!div) return true;
263   e.preventDefault();
264   // remove selection
265   setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
266   api.startChat(div.user.uni);
267   return false;
271 function deletePerson (uni) {
272   uni = uni.toLowerCase();
273   var user = clist[uni];
274   if (!user) return;
275   var dx = user.div.parentNode;
276   if (dx && dx.parentNode) dx.parentNode.removeChild(dx);
277   delete clist[uni];
278   for (var f = 0; f < clistSorted.length;) {
279     if (clistSorted[f].uni.toLowerCase() == uni) {
280       for (var c = f+1; c < clistSorted.length; c++) clistSorted[c-1] = clistSorted[c];
281       clistSorted.pop();
282     } else f++;
283   }
284   api.refresh();
285   api.deleteContact(uni);
289 function deNdeletePerson (uni) {
290   uni = uni.toLowerCase();
291   var user = clist[uni];
292   if (!user) return;
293   if (user.chatting) api.startChat("");
294   api.doPSYC("/unfr "+uni);
295   var dx = user.div.parentNode;
296   if (dx && dx.parentNode) dx.parentNode.removeChild(dx);
297   delete clist[uni];
298   for (var f = 0; f < clistSorted.length;) {
299     if (clistSorted[f].uni.toLowerCase() == uni) {
300       for (var c = f+1; c < clistSorted.length; c++) clistSorted[c-1] = clistSorted[c];
301       clistSorted.pop();
302     } else f++;
303   }
304   api.refresh();
305   api.deleteContact(uni);
309 function deletePlace (uni) {
310   uni = uni.toLowerCase();
311   var user = croom[uni];
312   if (!user) return;
313   var dx = user.div.parentNode;
314   if (dx && dx.parentNode) dx.parentNode.removeChild(dx);
315   delete croom[uni];
316   for (var f = 0; f < croomSorted.length;) {
317     if (croomSorted[f].uni.toLowerCase() == uni) {
318       for (var c = f+1; c < croomSorted.length; c++) croomSorted[c-1] = croomSorted[c];
319       croomSorted.pop();
320     } else f++;
321   }
322   api.refresh();
323   api.deleteContact(uni);
328  * "action div" for users (NOT FOR PLACES!)
329  */
330 var actionDiv;
331 function setActionDivFor (user) {
332   function addItem (div, text, method) {
333     var i = document.createElement("div");
334     i.div = div;
335     i.innerText = text;
336     i.mt = method;
337     if (i.mt) {
338       i.onclick = function (e) {
339         if (i.div.uni && i.mt) i.mt(i.div.uni);
340         e.preventDefault();
341         return false;
342       };
343       i.setAttribute("class", "adiv_item");
344     } else i.setAttribute("class", "adiv_item_dis");
345     div.appendChild(i);
346     return i;
347   }
349   function addSeparator (div) {
350     var i = document.createElement("div");
351     i.setAttribute("class", "adiv_item_sep");
352     div.appendChild(i);
353     return i;
354   }
356   var d = document.createElement("div");
357   d.setAttribute("class", "adiv");
358   addItem(d, "chat", api.startChat);
359   if (user.unread > 0) addItem(d, "mark as read", api.markAsRead);
360   addSeparator(d);
361   addItem(d, "insert UNI", function (uni) { if (uni) api.insertEditorText(uni); });
362   addSeparator(d);
363   addItem(d, "request auth", api.requestAuth);
364   addItem(d, "cancel auth", api.cancelAuth);
365   addItem(d, "send auth", api.sendAuth);
366   addSeparator(d);
367   if (api.isInPlace()) {
368     addItem(d, "invite to room", function (uni) {
369       if (api.isInPlace()) api.invite(uni, api.chattingWith());
370     });
371     addSeparator(d);
372   }
373   if (user.isOTR) {
374     addItem(d, "OTR: end it", api.otrEnd);
375     if (user.isOTRVerified) addItem(d, "OTR: untrust", function (uni) { api.otrSetTrust(uni, false); });
376     else addItem(d, "OTR: trust", function (uni) { api.otrSetTrust(uni, true); });
377   } else {
378     addItem(d, "OTR: start it", api.otrStart);
379   }
381   addSeparator(d);
382   addItem(d, "visible to...", function (uni) { api.doPSYC("nf "+uni+" i"); });
383   addItem(d, "invisible to...", function (uni) { api.doPSYC("nf "+uni+" i"); api.doPSYC("nf "+uni); });
385   addSeparator(d);
386   addItem(d, "delete contact", deletePerson);
387   addItem(d, "defrend and delete", deNdeletePerson);
389   actionDiv = d;
390   actionDiv.uni = user.uni;
391   actionDiv.user = user;
395 function onContactMouseDown (e) {
396   if (e.which == 3) {
397     var div = targetToUserDiv(e.target);
398     if (!div) return true;
399     var my = (actionDiv && actionDiv.parentNode && !strcmpI(actionDiv.uni, div.user.uni));
400     if (actionDiv && actionDiv.parentNode) actionDiv.parentNode.removeChild(actionDiv);
401     if (!my) {
402       setActionDivFor(div.user);
403       div.appendChild(actionDiv);
404     } else actionDiv = null;
405     e.preventDefault();
406     // remove selection
407     setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
408     return false;
409   }
410   //return true;
415  * "action div" for places (NOT FOR USERS!)
416  */
417 function createInfoDivFor (room) {
418   function addItem (div, text, hint, method, userUNI) {
419     var i = document.createElement("div");
420     i.div = div;
421     i.userUNI = userUNI;
422     i.innerText = text;
423     i.title = hint;
424     i.mt = method;
425     if (i.mt) {
426       i.setAttribute("class", "rdiv_item");
427       if (userUNI) {
428         if (!strcmpI(userUNI, myUNI)) {
429           i.setAttribute("class", "rdiv_item_self");
430           i.onclick = function (e) {
431             e.preventDefault();
432             return false;
433           };
434         } else {
435           i.onclick = function (e) {
436             if (i.userUNI && i.mt) i.mt(i.userUNI);
437             e.preventDefault();
438             return false;
439           };
440         }
441       } else {
442         i.onclick = function (e) {
443           if (i.div.uni && i.mt) i.mt(i.div.uni);
444           e.preventDefault();
445           return false;
446         };
447       }
448     } else i.setAttribute("class", "rdiv_item_dis");
449     div.appendChild(i);
450     return i;
451   }
453   function addSeparator (div) {
454     var i = document.createElement("div");
455     i.setAttribute("class", "rdiv_item_sep");
456     div.appendChild(i);
457     return i;
458   }
460   var d = document.createElement("div");
461   d.setAttribute("class", "rdiv");
462   var a = [];
463   for (var k in room.users) if (typeof(k) == "string") a.push({uni:k, nick:room.users[k]});
464   a.sort(function (u0, u1) { return strcmpI(u0.nick, u1.nick); });
466   for (var f = 0; f < a.length; f++) {
467     addItem(d, a[f].nick, a[f].uni, api.startChat, a[f].uni);
468   }
469   addSeparator(d);
470   if (room.unread > 0) addItem(d, "mark as read", "mark all messages as read", api.markAsRead);
471   if (!room.entered) addItem(d, "enter", "enter room", api.enterPlace);
472   if (room.entered) addItem(d, "leave", "leave room", api.leavePlace);
473   addItem(d, "delete", "delete room", deletePlace);
475   d.uni = room.uni;
476   d.room = room;
477   return d;
481 function onPlaceMouseDown (e) {
482   if (e.which != 3) return;
483   var div = targetToUserDiv(e.target);
484   if (!div) return true;
485   var room = div.user;
486   var doShow = !room.infoVisible;
487   room.infoVisible = doShow;
488   var idiv = room.listDiv;
489   if (doShow) {
490     if (!idiv) {
491       room.listDiv = createInfoDivFor(room);
492       idiv = room.listDiv;
493       div.appendChild(idiv);
494     }
495     idiv.style.visibility = "visible";
496   } else if (idiv) {
497     var pn = idiv.parentNode;
498     if (pn) pn.removeChild(idiv);
499     delete room.listDiv;
500   }
501   e.preventDefault();
502   // remove selection
503   setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
504   return false;
508 function addContactDiv (user) {
509   globalCnt++;
510   var div = document.createElement("div");
511   div.setAttribute("class", "contact contact"+(clistSorted.length%2));
512   var s = user.uni;
513   if (user.channel) s += "/"+user.channel;
514   div.title = s;
515   div.user = {};
516   var uu = div.user;
517   uu.div = div;
519   var imgProto = document.createElement("img");
520   var pp = user.proto;
521   imgProto.src = pp == "xmpp"?"dys://theme/img/protos/xmpp.png":
522     pp == "irc"?"dys://theme/img/protos/irc.png":
523     "dys://theme/img/protos/psyc.png";
524   imgProto.setAttribute("style", "float:right");
525   imgProto.title = pp+" (click here to rename contact)";
527   var ei = "hshandle_"+globalCnt;
528   imgProto.onclick = function () {
529     div.user.editing = true;
530     sz = div.user.verbatim.length;
531     if (sz < 10) sz = 10; else if (sz > 20) sz = 20;
532     var ined = new midoriInlineEdit({id:ei, size:sz, maxlen:64,
533       callback: function (txt) {
534         div.user.editing = false;
535         //api.printLn("["+txt+"]");
536         //div.user.hspan.innerText = txt;
537         if (txt && div.user.verbatim != txt) {
538           div.user.hspan.innerText = txt;
539           api.setContactVerbatim(div.user.uni, txt);
540         } else div.user.hspan.innerText = div.user.verbatim;
541       }
542     });
543     ined.edit();
544   };
546   div.appendChild(imgProto);
547   uu.imgProto = imgProto;
549   var imgStatus = document.createElement("img");
550   div.appendChild(imgStatus);
551   uu.imgStatus = imgStatus;
553   var hspan = document.createElement("span");
554   hspan.innerText = user.verbatim;
555   hspan.setAttribute("style", "padding:2px");
556   hspan.setAttribute("id", ei);
557   div.appendChild(hspan);
558   uu.hspan = hspan;
560   if (user.place) {
561     var infoDiv = document.createElement("div");
562     infoDiv.setAttribute("class", "roomInfo");
563     infoDiv.innerText = "users: 0";
564     div.appendChild(infoDiv);
565     uu.infoDiv = infoDiv;
566     uu.usercount = 0;
567     uu.entered = false;
568     uu.users = {};
570     div.onmousedown = onPlaceMouseDown;
571     div.ondblclick = onPlaceDblClick;
572   } else {
573     div.onmousedown = onContactMouseDown;
574     div.ondblclick = onContactDblClick;
575   }
577   var dddx = document.createElement("div");
578   dddx.appendChild(div);
580   var cd = document.getElementById(user.place?"places":"contacts");
581   if (cd) cd.appendChild(dddx); else document.body.appendChild(dddx);
583   if (user.place) {
584     croom[user.uni.toLowerCase()] = uu;
585     croomSorted.push(uu);
586   } else {
587     clist[user.uni.toLowerCase()] = uu;
588     clistSorted.push(uu);
589   }
591   return uu;
595 function getContactVisibility (uu) {
596   if (uu.chatting || uu.showAlways || uu.editing) return true;
597   if (uu.hidden && !flgHiddenVisMode) {
598     /* hidden contact */
599     if (uu.unread && !uu.skipUnreadCycle) return true;
600     return false;
601   }
602   /* have unread messages? */
603   if (uu.unread && !uu.skipUnreadCycle) return true;
604   /* is contact is offline? */
605   if (uu.status <= stOffline) return ((uu.place && flgOffVisModeRoom) || (!uu.place && flgOffVisModePerson)); /* is corresponding 'show offline' mode is off? */
606   /* this contact is online and not hidden: show it */
607   return true;
611 function setPersonOfflineVisibility (vismode) {
612   flgOffVisModePerson = !!vismode;
613   for (var f = clistSorted.length-1; f >= 0; f--) {
614     var uu = clistSorted[f];
615     uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
616   }
617   api.refresh();
621 function setPlaceOfflineVisibility (vismode) {
622   flgOffVisModeRoom = !!vismode;
623   for (var f = croomSorted.length-1; f >= 0; f--) {
624     var uu = croomSorted[f];
625     uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
626   }
627   api.refresh();
631 function setHiddenVisibility (vismode) {
632   flgHiddenVisMode = !!vismode;
633   for (var f = clistSorted.length-1; f >= 0; f--) {
634     var uu = clistSorted[f];
635     uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
636   }
637   for (var f = croomSorted.length-1; f >= 0; f--) {
638     var uu = croomSorted[f];
639     uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
640   }
641   api.refresh();
645 function space2nbsp (s) {
646   return s.replace(/\s+/g, "\u00a0");
650 function date2str (d) {
651   function pad2 (n) { return (n<10?"0":"")+n; }
652   const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
653   const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
654   return d.getDate()+" "+monthNames[d.getMonth()]+" ("+dayNames[d.getDay()]+"), "+pad2(d.getHours())+":"+pad2(d.getMinutes());
658 function redrawContact (uni) {
659   var user = api.contactInfo(uni);
660   if (!uni) return; // no such user
661   var ulc = user.uni.toLowerCase();
662   var uu = user.place ? croom[ulc] : clist[ulc];
663   var isNew = false;
664   if (!uu) { uu = addContactDiv(user); isNew = true; }
665   var prevIC = uu.chatting;
667   // copy user to uu
668   for (var kn in user) {
669     if (typeof(kn) != "string") continue;
670     uu[kn] = user[kn];
671   }
672   // fix unread
673   uu.unread = user.chatting?0:user.unread;
674   if (!uu.unread && uu.wasBlink) delete uu.wasBlink;
676   var verbFix = false;
677   if (uu.verbatim != uu.hspan.innerText) {
678     uu.hspan.innerText = uu.verbatim;
679     verbFix = true;
680   }
682   //uu.div.style.backgroundColor = uu.chatting ? "#0000c0" : "";
683   if (uu.place) {
684     uu.authorized = true;
685     uu.imgStatus.title = "";
686     uu.imgStatus.src = statusInfo.images[uu.unread?0:(uu.entered?7:1)];
687     sortCArray(croomSorted, crCompare);
688   } else {
689     var s = uu.uni;
690     if (user.channel) s += "/"+space2nbsp(uu.channel);
691     var t = user.statusText;
692     t = t.replace(/^\s*(.*?)\s*$/, "$1");
693     if (t != "") s += "\n["+space2nbsp(t)+"]";
694     if (user.lastseen && user.lastseen>0) s += "\n<"+space2nbsp("last seen: "+date2str(new Date(user.lastseen*1000)))+">";
695     uu.div.title = s;
696     uu.hspan.style.textDecoration = uu.authorized ? "none" : "underline";
697     if (uu.isOTR) {
698       uu.hspan.style.fontWeight = "bold";
699       uu.hspan.style.fontStyle = uu.isOTRVerified ? "normal" : "italic";
700     } else {
701       uu.hspan.style.fontWeight = "normal";
702       uu.hspan.style.fontStyle = "normal";
703     }
704     setContactStatusPic(uu);
705     if (uu.unread) checkBlinking();
706     sortCArray(clistSorted, ccCompare);
707     // fix name
708     var ll = uu.uni.toLowerCase();
709     for (var f = croomSorted.length-1; f >= 0; f--) {
710       var usrs = croomSorted[f].users;
711       for (var uni in usrs) {
712         if (typeof(uni) != "string") continue;
713         if (uni != ll) continue;
714         usrs[uni] = uu.verbatim;
715         var room = croomSorted[f];
716         //api.printLn("fixed! "+room.uni);
717         if (!room.infoVisible) continue;
718         var idiv = room.listDiv;
719         var pn = idiv.parentNode;
720         if (pn) pn.removeChild(idiv);
721         room.listDiv = createInfoDivFor(room);
722         room.div.appendChild(room.listDiv);
723       }
724     }
726     if (actionDiv && actionDiv.uni == uni.toLowerCase()) {
727       actionDiv.parentNode.removeChild(actionDiv);
728       setActionDivFor(uu);
729       uu.div.appendChild(actionDiv);
730     }
731   }
733   uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
735   api.refresh();
739 function doEnterLeave (isEnter, placeUni, userUni, isMe) {
740   //api.printLn("enter:"+isEnter+"; place:"+placeUni+"; user:"+userUni+"; me:"+isMe);
741   var user = api.contactInfo(userUni);
742   if (!user) return;
743   //api.printLn(" user ok");
744   var place = api.contactInfo(placeUni);
745   if (!place) return;
746   //api.printLn(" place ok");
748   var ulc = place.uni.toLowerCase();
749   var room = croom[ulc];
750   if (!room) return;
752   if (isMe && room.entered == isEnter) return; // nothing to do
753   var usr = user.uni.toLowerCase();
754   if (isEnter) {
755     if (room.users[usr]) return; // already here
756     room.usercount++;
757     room.users[usr] = user.verbatim;
758   } else {
759     if (!room.users[usr]) return; // already not here
760     room.usercount--;
761     delete room.users[usr];
762   }
763   room.infoDiv.innerText = "users: "+room.usercount;
764   if (isMe) room.entered = isEnter;
765   if (room.infoVisible) {
766     var idiv = room.listDiv;
767     var pn = idiv.parentNode;
768     if (pn) pn.removeChild(idiv);
769     if (isEnter || !isMe) {
770       room.listDiv = createInfoDivFor(room);
771       room.div.appendChild(room.listDiv);
772     } else {
773       room.infoVisible = false;
774       delete room.listDiv;
775     }
776   }
777   api.refresh();
782  * place guaranteed to be redrawn at least once
783  */
784 function meEnters (pluni) {
785   //api.printLn("meEnters: "+pluni+"; "+myUNI);
786   //var me = api.contactInfo(myUNI);
787   //if (!me) return;
788   //api.printLn(" *meEnters: "+pluni+"; "+myUNI);
789   doEnterLeave(true, pluni, myUNI, true);
791 function meLeaves (pluni) {
792   //api.printLn("meLeaves: "+pluni+"; "+myUNI);
793   //var me = api.contactInfo(myUNI);
794   //if (!me) return;
795   //api.printLn(" *meLeaves: "+pluni+"; "+myUNI);
796   doEnterLeave(false, pluni, myUNI, true);
798 function userEnters (pluni, uni) {
799   //api.printLn("enters: "+pluni+"; "+uni);
800   //var user = api.contactInfo(uni);
801   //if (!user) return;
802   doEnterLeave(true, pluni, uni, false);
804 function userLeaves (pluni, uni) {
805   //var user = api.contactInfo(uni);
806   //if (!user) return;
807   doEnterLeave(false, pluni, uni, false);
811 function dumpPacket (pkt) {
812   api.printLn("\n===========================");
813   for (var n in pkt.vars) {
814     if (!pkt.rvars[n]) continue;
815     api.printLn(":"+n+" "+pkt.vars[n]);
816   }
817   api.printLn("");
818   for (var n in pkt.vars) {
819     if (pkt.rvars[n]) continue;
820     api.printLn(":"+n+" "+pkt.vars[n]);
821   }
822   api.printLn(""+pkt.method);
823   api.printLn(""+pkt.body);
827 function onPSYCPacket () {
828   //dumpPacket(psycpkt);