8 var blinkPhase = false;
11 var flgOffVisModeRoom = true;
12 var flgOffVisModePerson = true;
13 var flgHiddenVisMode = true;
27 const stFFC = 8; // free-for-chat (talkative)
28 const stRealtime = 9; // unused
34 function setMyUNI (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);
47 function chatStarted (uni) {
51 function optionChanged (optname) {
54 case "/contactlist/offvisible/persons":
55 v = api.getOption("/contactlist/offvisible/persons");
56 if (flgOffVisModePerson != v) setPersonOfflineVisibility(v);
58 case "/contactlist/offvisible/places":
59 v = api.getOption("/contactlist/offvisible/places");
60 if (flgOffVisModeRoom != v) setPlaceOfflineVisibility(v);
62 case "/contactlist/showhidden":
63 v = api.getOption("/contactlist/showhidden");
64 if (flgHiddenVisMode != v) setHiddenVisibility(v);
70 function optionRemoved (optname) {
74 function strcmp (s0, s1) {
75 if (s0 < s1) return -1;
76 if (s0 > s1) return 1;
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
89 if (c1.unread < 1) return -1; // c0<c1
90 } else if (c1.unread > 0) {
91 if (c0.unread < 1) return 1; // c0>c1
94 if (c0.status > c1.status) return -1; // c0<c1
95 if (c0.status < c1.status) return 1; // c0>c1
98 if (c0.verbatim && c1.verbatim) res = strcmpI(c0.verbatim, c1.verbatim); else res = 0;
100 if (!res) res = strcmpI(c0.nick, c1.nick);
102 if (!res) res = strcmpI(c0.proto, c1.proto);
104 if (!res) res = strcmpI(c0.uni, c1.uni);
109 /* compare rooms (for sorting) */
110 function crCompare (c0, c1) {
112 return strcmpI(c0.uni, c1.uni);
116 function clistSwapDiv (c0, c1) {
117 if (!strcmpI(c0.uni, c1.uni)) return;
120 var p0 = d0.parentNode;
121 var p1 = d1.parentNode;
129 /* sort clist and rearrange divs */
130 // bubble sort! shame on me!
131 function sortCArray (sl, compFn) {
132 var swapped, was = false;
135 var len = sl.length-2;
136 for (var f = 0; f <= len; f++) {
137 var rc = compFn(sl[f], sl[f+1]);
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;
147 for (var f = 0; f <= sl.length-1; f++) {
149 div.setAttribute("class", "contact contact"+(f%2)+
150 (sl[f].chatting?" chatting":"")+
151 (sl[f].unread>0?" unread":"")+
160 function sortCList () {
161 sortCArray(clistSorted, ccCompare);
162 sortCArray(croomSorted, crCompare);
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"
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 () {
203 var sl = clistSorted;
204 var bf = !blinkPhase;
206 for (var f = sl.length-1; f >= 0; f--) {
211 } else if (d.wasBlink) {
214 setContactStatusPic(d);
216 if (!cnt && blinkTimer) {
217 clearInterval(blinkTimer);
223 function checkBlinking () {
225 var sl = clistSorted;
226 for (var f = sl.length-1; f >= 0; f--) {
228 if (d.unread > 0) { doBlink = true; break; }
232 clearInterval(blinkTimer);
235 } else if (!blinkTimer) blinkTimer = setInterval(onBlinkTimer, 300);
239 function targetToUserDiv (target) {
242 if (div.user) return div;
243 div = div.parentNode;
249 function onContactDblClick (e) {
250 var div = targetToUserDiv(e.target);
251 if (!div) return true;
254 setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
255 api.startChat(div.user.uni);
260 function onPlaceDblClick (e) {
261 var div = targetToUserDiv(e.target);
262 if (!div) return true;
265 setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
266 api.startChat(div.user.uni);
271 function deletePerson (uni) {
272 uni = uni.toLowerCase();
273 var user = clist[uni];
275 var dx = user.div.parentNode;
276 if (dx && dx.parentNode) dx.parentNode.removeChild(dx);
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];
285 api.deleteContact(uni);
289 function deNdeletePerson (uni) {
290 uni = uni.toLowerCase();
291 var user = clist[uni];
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);
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];
305 api.deleteContact(uni);
309 function deletePlace (uni) {
310 uni = uni.toLowerCase();
311 var user = croom[uni];
313 var dx = user.div.parentNode;
314 if (dx && dx.parentNode) dx.parentNode.removeChild(dx);
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];
323 api.deleteContact(uni);
328 * "action div" for users (NOT FOR PLACES!)
331 function setActionDivFor (user) {
332 function addItem (div, text, method) {
333 var i = document.createElement("div");
338 i.onclick = function (e) {
339 if (i.div.uni && i.mt) i.mt(i.div.uni);
343 i.setAttribute("class", "adiv_item");
344 } else i.setAttribute("class", "adiv_item_dis");
349 function addSeparator (div) {
350 var i = document.createElement("div");
351 i.setAttribute("class", "adiv_item_sep");
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);
361 addItem(d, "insert UNI", function (uni) { if (uni) api.insertEditorText(uni); });
363 addItem(d, "request auth", api.requestAuth);
364 addItem(d, "cancel auth", api.cancelAuth);
365 addItem(d, "send auth", api.sendAuth);
367 if (api.isInPlace()) {
368 addItem(d, "invite to room", function (uni) {
369 if (api.isInPlace()) api.invite(uni, api.chattingWith());
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); });
378 addItem(d, "OTR: start it", api.otrStart);
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); });
386 addItem(d, "delete contact", deletePerson);
387 addItem(d, "defrend and delete", deNdeletePerson);
390 actionDiv.uni = user.uni;
391 actionDiv.user = user;
395 function onContactMouseDown (e) {
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);
402 setActionDivFor(div.user);
403 div.appendChild(actionDiv);
404 } else actionDiv = null;
407 setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
415 * "action div" for places (NOT FOR USERS!)
417 function createInfoDivFor (room) {
418 function addItem (div, text, hint, method, userUNI) {
419 var i = document.createElement("div");
426 i.setAttribute("class", "rdiv_item");
428 if (!strcmpI(userUNI, myUNI)) {
429 i.setAttribute("class", "rdiv_item_self");
430 i.onclick = function (e) {
435 i.onclick = function (e) {
436 if (i.userUNI && i.mt) i.mt(i.userUNI);
442 i.onclick = function (e) {
443 if (i.div.uni && i.mt) i.mt(i.div.uni);
448 } else i.setAttribute("class", "rdiv_item_dis");
453 function addSeparator (div) {
454 var i = document.createElement("div");
455 i.setAttribute("class", "rdiv_item_sep");
460 var d = document.createElement("div");
461 d.setAttribute("class", "rdiv");
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);
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);
481 function onPlaceMouseDown (e) {
482 if (e.which != 3) return;
483 var div = targetToUserDiv(e.target);
484 if (!div) return true;
486 var doShow = !room.infoVisible;
487 room.infoVisible = doShow;
488 var idiv = room.listDiv;
491 room.listDiv = createInfoDivFor(room);
493 div.appendChild(idiv);
495 idiv.style.visibility = "visible";
497 var pn = idiv.parentNode;
498 if (pn) pn.removeChild(idiv);
503 setTimeout(function () { window.getSelection().removeAllRanges(); }, 0);
508 function addContactDiv (user) {
510 var div = document.createElement("div");
511 div.setAttribute("class", "contact contact"+(clistSorted.length%2));
513 if (user.channel) s += "/"+user.channel;
519 var imgProto = document.createElement("img");
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;
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);
561 var infoDiv = document.createElement("div");
562 infoDiv.setAttribute("class", "roomInfo");
563 infoDiv.innerText = "users: 0";
564 div.appendChild(infoDiv);
565 uu.infoDiv = infoDiv;
570 div.onmousedown = onPlaceMouseDown;
571 div.ondblclick = onPlaceDblClick;
573 div.onmousedown = onContactMouseDown;
574 div.ondblclick = onContactDblClick;
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);
584 croom[user.uni.toLowerCase()] = uu;
585 croomSorted.push(uu);
587 clist[user.uni.toLowerCase()] = uu;
588 clistSorted.push(uu);
595 function getContactVisibility (uu) {
596 if (uu.chatting || uu.showAlways || uu.editing) return true;
597 if (uu.hidden && !flgHiddenVisMode) {
599 if (uu.unread && !uu.skipUnreadCycle) return true;
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 */
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");
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");
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");
637 for (var f = croomSorted.length-1; f >= 0; f--) {
638 var uu = croomSorted[f];
639 uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
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];
664 if (!uu) { uu = addContactDiv(user); isNew = true; }
665 var prevIC = uu.chatting;
668 for (var kn in user) {
669 if (typeof(kn) != "string") continue;
673 uu.unread = user.chatting?0:user.unread;
674 if (!uu.unread && uu.wasBlink) delete uu.wasBlink;
677 if (uu.verbatim != uu.hspan.innerText) {
678 uu.hspan.innerText = uu.verbatim;
682 //uu.div.style.backgroundColor = uu.chatting ? "#0000c0" : "";
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);
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)))+">";
696 uu.hspan.style.textDecoration = uu.authorized ? "none" : "underline";
698 uu.hspan.style.fontWeight = "bold";
699 uu.hspan.style.fontStyle = uu.isOTRVerified ? "normal" : "italic";
701 uu.hspan.style.fontWeight = "normal";
702 uu.hspan.style.fontStyle = "normal";
704 setContactStatusPic(uu);
705 if (uu.unread) checkBlinking();
706 sortCArray(clistSorted, ccCompare);
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);
726 if (actionDiv && actionDiv.uni == uni.toLowerCase()) {
727 actionDiv.parentNode.removeChild(actionDiv);
729 uu.div.appendChild(actionDiv);
733 uu.div.style.display = (getContactVisibility(uu) ? "block" : "none");
739 function doEnterLeave (isEnter, placeUni, userUni, isMe) {
740 //api.printLn("enter:"+isEnter+"; place:"+placeUni+"; user:"+userUni+"; me:"+isMe);
741 var user = api.contactInfo(userUni);
743 //api.printLn(" user ok");
744 var place = api.contactInfo(placeUni);
746 //api.printLn(" place ok");
748 var ulc = place.uni.toLowerCase();
749 var room = croom[ulc];
752 if (isMe && room.entered == isEnter) return; // nothing to do
753 var usr = user.uni.toLowerCase();
755 if (room.users[usr]) return; // already here
757 room.users[usr] = user.verbatim;
759 if (!room.users[usr]) return; // already not here
761 delete room.users[usr];
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);
773 room.infoVisible = false;
782 * place guaranteed to be redrawn at least once
784 function meEnters (pluni) {
785 //api.printLn("meEnters: "+pluni+"; "+myUNI);
786 //var me = api.contactInfo(myUNI);
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);
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);
802 doEnterLeave(true, pluni, uni, false);
804 function userLeaves (pluni, uni) {
805 //var user = api.contactInfo(uni);
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]);
818 for (var n in pkt.vars) {
819 if (pkt.rvars[n]) continue;
820 api.printLn(":"+n+" "+pkt.vars[n]);
822 api.printLn(""+pkt.method);
823 api.printLn(""+pkt.body);
827 function onPSYCPacket () {
828 //dumpPacket(psycpkt);