fixed url detection bug
[bioacid.git] / bioacid.d
blob465dbb28978b46e9a5d28eb03b1d9db21f1b7aaa
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module bioacid is aliced;
19 import std.datetime;
21 import arsd.color;
22 import arsd.image;
23 import arsd.simpledisplay;
25 import iv.cmdcon;
26 import iv.cmdcon.gl;
27 import iv.gxx;
28 import iv.meta;
29 import iv.nanovega;
30 import iv.nanovega.blendish;
31 import iv.nanovega.textlayouter;
32 import iv.strex;
33 import iv.tox;
34 import iv.txtser;
35 import iv.sdpyutil;
36 import iv.unarray;
37 import iv.utfutil;
38 import iv.vfs.io;
40 version(sfnt_test) import iv.nanovega.simplefont;
42 import accdb;
43 import fonts;
44 import popups;
45 import icondata;
46 import notifyicon;
47 import toxproto;
48 import miniedit;
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared bool mainWindowActive = false;
53 __gshared bool mainWindowVisible = false;
56 shared static this () {
57 toxCoreSendEvent = delegate (Object msg) {
58 if (msg is null) return; // just in case
59 try {
60 if (glconCtlWindow is null || glconCtlWindow.closed) return;
61 glconCtlWindow.postEvent(msg);
62 } catch (Exception e) {}
67 // ////////////////////////////////////////////////////////////////////////// //
68 string getBrowserCommand (bool forceOpera=false) {
69 __gshared string browser;
70 if (forceOpera) return "opera";
71 if (browser.length == 0) {
72 import core.stdc.stdlib : getenv;
73 const(char)* evar = getenv("BROWSER");
74 if (evar !is null && evar[0]) {
75 import std.string : fromStringz;
76 browser = evar.fromStringz.idup;
77 } else {
78 browser = "opera";
81 return browser;
85 void openUrl (ConString url, bool forceOpera=false) {
86 if (url.length) {
87 import std.stdio : File;
88 import std.process;
89 try {
90 auto frd = File("/dev/null");
91 auto fwr = File("/dev/null", "w");
92 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
93 } catch (Exception e) {
94 conwriteln("ERROR executing URL viewer (", e.msg, ")");
100 // ////////////////////////////////////////////////////////////////////////// //
101 struct UrlInfo {
102 int pos = -1, len = 0;
104 @property bool valid () const pure nothrow @safe @nogc => (pos >= 0 && len > 0);
105 @property int end () const pure nothrow @safe @nogc => (pos >= 0 && len > 0 ? pos+len : 0);
107 static UrlInfo Invalid () pure nothrow @safe @nogc => UrlInfo.init;
111 UrlInfo urlDetect (const(char)[] text) nothrow @trusted @nogc {
112 import iv.strex;
113 UrlInfo res;
114 auto dlpos = text.indexOf("://");
115 if (dlpos < 3) return res;
117 //{ import core.stdc.stdio; printf("det: <%.*s>\n", cast(uint)text.length, text.ptr); }
119 bool isProto (const(char)[] prt) nothrow @trusted @nogc {
120 if (dlpos < prt.length) return false;
121 if (!strEquCI(prt, text[dlpos-prt.length..dlpos])) return false;
122 // check word boundary
123 if (dlpos == prt.length) return true;
124 return !isalpha(text[dlpos-prt.length-1]);
127 if (isProto("ftp")) res.pos = cast(int)(dlpos-3);
128 else if (isProto("http")) res.pos = cast(int)(dlpos-4);
129 else if (isProto("https")) res.pos = cast(int)(dlpos-5);
130 else return res;
132 dlpos += 3; // skip "://"
134 // skip host name
135 for (; dlpos < text.length; ++dlpos) {
136 char ch = text[dlpos];
137 if (ch == '/') break;
138 if (!(isalnum(ch) || ch == '.' || ch == '-' || ch == ':' || ch == '@')) break;
141 // skip path
142 char[64] brcStack;
143 int brcSP = 0;
144 bool wasSharp = false;
146 for (; dlpos < text.length; ++dlpos) {
147 char ch = text[dlpos];
148 // hash
149 if (ch == '#') {
150 if (wasSharp) break;
151 wasSharp = true;
152 brcSP = 0;
153 continue;
155 // opening bracket
156 if (ch == '(' || ch == '[' || ch == '{') {
157 final switch (ch) {
158 case '(': ch = ')'; break;
159 case '[': ch = ']'; break;
160 case '{': ch = '}'; break;
162 if (brcSP < brcStack.length) brcStack[brcSP++] = ch;
163 continue;
165 // closing bracket
166 if (ch == ')' || ch == ']' || ch == '}') {
167 if (brcSP == 0 || brcStack[brcSP-1] != ch) break;
168 --brcSP;
169 continue;
171 if (ch == '.') {
172 if (brcSP == 0) {
173 if (dlpos == text.length || (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_')) break;
175 continue;
177 if (ch <= ' ' || ch >= 127) break;
180 res.len = cast(int)(dlpos-res.pos);
182 return res;
186 // ////////////////////////////////////////////////////////////////////////// //
187 alias LayTextClass = LayTextD;
190 // ////////////////////////////////////////////////////////////////////////// //
191 __gshared string accBaseDir = ".";
193 __gshared NVGContext nvg = null;
194 __gshared NVGImage nvgSkullsImg;
195 __gshared NVGImage kittyOut, kittyMsg;
197 __gshared LayTextClass lay;
199 __gshared int optCListWidth = -1;
200 __gshared int lastWindowWidth = -1;
202 __gshared NVGImage[5] statusImgId;
205 shared static ~this () {
206 //{ import core.stdc.stdio; printf("******************************\n"); }
207 nvgSkullsImg.clear();
208 //{ import core.stdc.stdio; printf("---\n"); }
209 foreach (ref img; statusImgId[]) img.clear();
210 kittyOut.clear();
211 kittyMsg.clear();
215 void buildStatusImages () {
216 version(none) {
217 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
218 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, ctiOnline[], NVGImageFlags.NoFiltering);
219 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, ctiAway[], NVGImageFlags.NoFiltering);
220 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
221 } else {
222 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
223 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, baph16Gray[], NVGImageFlags.NoFiltering);
224 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
225 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, baph16Online[], NVGImageFlags.NoFiltering);
226 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
227 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, baph16Away[], NVGImageFlags.NoFiltering);
228 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
229 statusImgId[ContactStatus.Busy] = nvg.createImageRGBA(16, 16, baph16Busy[], NVGImageFlags.NoFiltering);
230 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
231 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, baph16Orange[], NVGImageFlags.NoFiltering);
232 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
233 kittyOut = nvg.createImageRGBA(16, 16, kittyOutgoing[], NVGImageFlags.NoFiltering);
234 kittyMsg = nvg.createImageRGBA(16, 16, kittyMessage[], NVGImageFlags.NoFiltering);
239 // ////////////////////////////////////////////////////////////////////////// //
240 void loadFonts (NVGContext vg) {
241 vg.fonsContext.fonsAddStashFonts(fstash);
242 bndSetFont(vg.findFont("ui"));
246 // ////////////////////////////////////////////////////////////////////////// //
247 class MessageStart : LayObject {
248 long msgid; // >0: outgoing, unacked yet
250 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
252 override int width () => 0;
253 override int spacewidth () => 0;
254 override int height () => 0;
255 override int ascent () => 0;
256 override int descent () => 0;
257 override bool canbreak () => true;
258 override bool spaced () => false;
259 // y is at baseline
260 override void draw (NVGContext ctx, float x, float y) {}
264 class MessageOutMark : LayObject {
265 long msgid; // >0: outgoing, unacked yet
267 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
269 override int width () => kittyOut.width;
270 override int spacewidth () => 4;
271 override int height () => kittyOut.height;
272 override int ascent () => height;
273 override int descent () => 0;
274 override bool canbreak () => true;
275 override bool spaced () => false;
276 // y is at baseline
277 override void draw (NVGContext ctx, float x, float y) {
278 if (msgid > 0) {
279 nvg.save();
280 scope(exit) nvg.restore;
281 nvg.newPath();
282 nvg.rect(x+0.5, y+0.5, width, height);
283 nvg.fillPaint(nvg.imagePattern(0, 0, width, height, 0, kittyOut));
284 nvg.fill();
290 // ////////////////////////////////////////////////////////////////////////// //
291 static struct LayUrl {
292 string url;
293 uint wordidx; // first word
297 __gshared LayUrl[uint] layUrlList; // for each word
300 void wipeLog () {
301 lay.wipeAll(true); // clear log, but delete objects
302 layUrlList.clear();
306 void addDividerLine (bool doflushgui=false) {
307 if (glconCtlWindow is null || glconCtlWindow.closed) return;
310 bool inFrame = nvg.inFrame;
311 if (!inFrame) {
312 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
313 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
315 scope(exit) {
316 if (!inFrame) {
317 nvg.endFrame();
318 if (doflushgui) flushGui();
319 glconCtlWindow.releaseCurrentOpenGlContext();
323 lay.fontStyle.fontsize = 2;
324 lay.fontStyle.color = NVGColor.k8orange.asUint;
325 lay.fontStyle.bgcolor = NVGColor("#aa0").asUint;
326 lay.fontStyle.monospace = true;
328 lay.putExpander();
329 lay.endPara();
330 lay.finalize();
332 // redraw
333 glconPostScreenRepaint();
337 // `ct` can be `null` for "my message" or "system message"
338 void addTextToLog (Account acc, Contact ct, LogFile.Msg.Kind kind, bool action, const(char)[] msg, SysTime time, long msgid=-1, bool doflushgui=false) {
339 if (glconCtlWindow is null || glconCtlWindow.closed) return;
342 bool inFrame = nvg.inFrame;
343 if (!inFrame) {
344 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
345 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
347 scope(exit) {
348 if (!inFrame) {
349 nvg.endFrame();
350 if (doflushgui) flushGui();
351 glconCtlWindow.releaseCurrentOpenGlContext();
355 // add "message start" mark
356 lay.putObject(new MessageStart(msgid));
358 lay.fontStyle.fontsize = 16;
359 lay.fontStyle.color = NVGColor.k8orange.asUint;
360 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
361 lay.fontStyle.monospace = true;
363 NVGColor textColor;
365 final switch (kind) {
366 case LogFile.Msg.Kind.Outgoing: textColor = NVGColor.k8orange; lay.fontStyle.color = NVGColor("#c40").asUint; lay.put(acc.info.nick); break;
367 case LogFile.Msg.Kind.Incoming: textColor = NVGColor("#ccc"); lay.fontStyle.color = NVGColor("#666").asUint; lay.put(ct.info.nick); break;
368 case LogFile.Msg.Kind.Notification: textColor = NVGColor("#0c0"); lay.fontStyle.color = textColor.asUint; lay.put("*system*"); break;
371 lay.fontStyle.monospace = false;
372 //lay.putHardSpace(64);
373 lay.putExpander();
374 // add "message outgoing" mark
375 if (kind == LogFile.Msg.Kind.Outgoing && msgid > 0) {
376 //lay.put("!");
377 //conwriteln("msgoutmark");
378 lay.putObject(new MessageOutMark(msgid));
379 lay.putExpander();
383 import std.datetime;
384 import std.format : format;
385 auto dt = cast(DateTime)time;
386 string tstr = "%04u/%02u/%02u".format(dt.year, dt.month, dt.day);
387 lay.put(tstr);
388 lay.putNBSP();
391 lay.fontStyle.color = NVGColor("#aaa").asUint;
392 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
394 import std.datetime;
395 import std.format : format;
396 auto dt = cast(DateTime)time;
397 string tstr = "%02u:%02u:%02u".format(dt.hour, dt.minute, dt.second);
398 lay.put(tstr);
399 lay.putNBSP();
401 lay.endPara();
403 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
404 lay.fontStyle.fontsize = 20;
406 if (action) {
407 lay.fontStyle.color = NVGColor("#fff").asUint;
408 lay.put("/me ");
411 void xput (const(char)[] str) {
412 while (str.length) {
413 auto nl = str.indexOf('\n');
414 if (nl < 0) { lay.put(str); break; }
415 if (nl > 0) lay.put(str[0..nl]);
416 lay.endPara();
417 str = str[nl+1..$];
421 msg = msg.xstripright;
423 lay.fontStyle.color = textColor.asUint;
424 while (msg.length) {
425 auto nfo = urlDetect(msg);
426 if (!nfo.valid) {
427 xput(msg);
428 break;
430 // url found
431 xput(msg[0..nfo.pos]);
432 string url = msg[nfo.pos..nfo.end].idup;
433 msg = msg[nfo.end..$];
434 auto stword = lay.nextWordIndex;
435 lay.pushStyles();
436 auto c = lay.fontStyle.color;
437 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
438 lay.fontStyle.href = true;
439 lay.fontStyle.underline = true;
440 lay.fontStyle.color = NVGColor("#06f").asUint;
441 lay.put(url);
442 while (stword < lay.nextWordIndex) {
443 layUrlList[stword] = LayUrl(url, stword);
444 ++stword;
448 lay.endPara();
449 lay.finalize();
451 // redraw
452 glconPostScreenRepaint();
456 void addTextToLog (Account acc, Contact ct, in ref LogFile.Msg msg, long msgid=-1, bool doflushgui=false) {
457 import iv.utfutil : utf8Encode;
458 char[] text;
459 text.reserve(4096);
460 scope(exit) delete text;
461 // decode text
462 foreach (dchar dc; msg.byDChar) {
463 char[4] buf = void;
464 auto len = utf8Encode(buf[], dc);
465 text ~= buf[0..len];
467 addTextToLog(acc, ct, msg.kind, msg.isMe, text, msg.time, msgid, doflushgui);
471 void ackLogMessage (long msgid) {
472 if (msgid <= 0) return;
473 foreach (immutable uint widx; 0..lay.wordCount) {
474 auto w = lay.wordByIndex(widx);
475 int oidx = w.objectIdx;
476 if (oidx >= 0) {
477 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
478 if (maw.msgid == msgid) {
479 maw.msgid = -1; // reset mark
480 glconPostScreenRepaint(); // redraw
488 // ////////////////////////////////////////////////////////////////////////// //
489 final class Group {
490 private:
491 this (Account aOwner) { acc = aOwner; }
493 private:
494 bool mDirty; // true: write contact's config
495 string diskFileName;
497 public:
498 Account acc;
499 GroupOptions info;
501 public:
502 void markDirty () pure nothrow @safe @nogc => mDirty = true;
504 void save () {
505 import std.file : mkdirRecurse;
506 import std.path : dirName;
507 // save this contact
508 assert(acc !is null);
509 // create disk name
510 if (diskFileName.length == 0) {
511 diskFileName = acc.basePath~"/contacts/groups.rc";
513 mkdirRecurse(diskFileName.dirName);
514 if (serialize(info, diskFileName)) mDirty = false;
517 @property bool visible () const nothrow @trusted @nogc {
518 if (!hideIfNoVisibleMembers) return true; // always visible
519 // check if we have any visible members
520 foreach (const(Contact) c; acc.contacts.byValue) {
521 if (c.gid != info.gid) continue;
522 if (c.visibleNoGroupCheck) return true;
524 return false; // nobody's here
527 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
528 enum lo = "info."~fld;
529 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
530 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
533 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
534 enum lo = "info."~fld;
535 if (mixin(lo) >= 0) return mixin(lo);
536 return mixin("acc.info."~fld);
539 @property nothrow @safe {
540 uint gid () const pure @nogc => info.gid;
542 bool opened () const @nogc => info.opened;
543 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
545 string name () const @nogc => info.name;
546 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
548 string note () const @nogc => info.note;
549 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
551 @nogc {
552 bool showOffline () const => getTriOpt!"showOffline";
553 bool showPopup () const => getTriOpt!"showPopup";
554 bool blinkActivity () const => getTriOpt!"blinkActivity";
555 bool skipUnread () const => getTriOpt!"skipUnread";
556 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
557 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
558 int resendRotDays () const => getIntOpt!"resendRotDays";
559 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
565 // ////////////////////////////////////////////////////////////////////////// //
566 final class Contact {
567 public:
568 // `Connecting` for non-account means "awaiting authorization"
570 static struct XMsg {
571 bool isMe; // "/me" message?
572 SysTime time;
573 string text;
574 long msgid; // ==0: unknown yet
575 int resendCount = 0;
577 MonoTime nextSendTime;
580 private:
581 this (Account aOwner) { acc = aOwner; edit = new MiniEdit(); }
583 private:
584 bool mDirty; // true: write contact's config
586 public:
587 Account acc;
588 string diskFileName;
589 ContactInfo info;
590 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
591 MiniEdit edit;
592 XMsg[] resendQueue;
593 int unreadCount;
595 public:
596 void markDirty () pure nothrow @safe @nogc => mDirty = true;
598 void loadUnreadCount () nothrow {
599 assert(diskFileName.length);
600 assert(acc !is null);
601 try {
602 import std.path : dirName;
603 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
604 unreadCount = fi.readNum!int;
605 } catch (Exception e) {
606 unreadCount = 0;
610 void saveUnreadCount () nothrow {
611 assert(diskFileName.length);
612 assert(acc !is null);
613 try {
614 import std.path : dirName;
615 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
616 fo.writeNum(unreadCount);
617 } catch (Exception e) {
621 void loadResendQueue () {
622 import std.path : dirName;
623 string fname = diskFileName.dirName~"/logs/resend.log";
624 LogFile lf;
625 lf.load(fname);
626 auto ctt = MonoTime.currTime;
627 foreach (const ref lmsg; lf.messages) {
628 XMsg xmsg;
629 xmsg.isMe = lmsg.isMe;
630 xmsg.time = lmsg.time;
631 xmsg.text = lmsg.text;
632 xmsg.msgid = 0;
633 xmsg.nextSendTime = ctt;
634 resendQueue ~= xmsg;
638 void saveResendQueue () {
639 import std.file : mkdirRecurse, remove;
640 import std.path : dirName;
641 assert(diskFileName.length);
642 assert(acc !is null);
643 mkdirRecurse(diskFileName.dirName~"/logs");
644 string fname = diskFileName.dirName~"/logs/resend.log";
645 try { remove(fname); } catch (Exception e) {}
646 if (resendQueue.length) {
647 foreach (const ref msg; resendQueue) {
648 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
653 void save () {
654 import std.file : mkdirRecurse;
655 import std.path : dirName;
656 // save this contact
657 assert(acc !is null);
658 // create disk name
659 if (diskFileName.length == 0) {
660 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
661 acc.contacts[info.pubkey] = this;
663 mkdirRecurse(diskFileName.dirName);
664 mkdirRecurse(diskFileName.dirName~"/avatars");
665 mkdirRecurse(diskFileName.dirName~"/files");
666 mkdirRecurse(diskFileName.dirName~"/fileparts");
667 mkdirRecurse(diskFileName.dirName~"/logs");
668 saveUnreadCount();
669 if (serialize(info, diskFileName)) mDirty = false;
672 public:
673 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (showOffline || status != ContactStatus.Offline);
675 @property bool visible () const nothrow @trusted @nogc {
676 if (!showOffline && status == ContactStatus.Offline) return false;
677 auto grp = acc.groupById(gid);
678 return grp.visible;
681 @property nothrow @safe {
682 ContactInfo.Kind kind () const @nogc => info.kind;
683 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
685 uint gid () const @nogc => info.gid;
686 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
688 string nick () const @nogc => info.nick;
689 void nick (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unknown>"; if (info.nick != v) { info.nick = v; markDirty(); } }
691 string visnick () const @nogc => info.visnick;
692 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
694 string displayNick () const @nogc => (info.visnick.length ? info.visnick : info.nick);
696 string statusmsg () const @nogc => info.statusmsg;
697 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
699 void setLastOnlineNow () {
700 try {
701 auto ut = Clock.currTime.toUTC().toUnixTime();
702 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
703 } catch (Exception e) {}
706 string note () const @nogc => info.note;
707 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
710 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
711 enum lo = "info.opts."~fld;
712 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
713 auto grp = acc.groupById(info.gid);
714 enum go = "grp.info."~fld;
715 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
716 return mixin("acc.info."~fld);
719 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
720 enum lo = "info.opts."~fld;
721 if (mixin(lo) >= 0) return mixin(lo);
722 auto grp = acc.groupById(info.gid);
723 enum go = "grp.info."~fld;
724 if (mixin(go) >= 0) return mixin(go);
725 return mixin("acc.info."~fld);
728 @property nothrow @safe @nogc {
729 bool showOffline () const => getTriOpt!"showOffline";
730 bool showPopup () const => getTriOpt!"showPopup";
731 bool blinkActivity () const => getTriOpt!"blinkActivity";
732 bool skipUnread () const => getTriOpt!"skipUnread";
733 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
734 int resendRotDays () const => getIntOpt!"resendRotDays";
735 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
738 void loadLogInto (ref LogFile lf) {
739 import std.file : exists;
740 import std.path : dirName;
741 string lname = diskFileName.dirName~"/logs/hugelog.log";
742 if (lname.exists) lf.load(lname); else lf.clear();
745 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
746 import std.path : dirName;
747 string lname = diskFileName.dirName~"/logs/hugelog.log";
748 LogFile.appendLine(lname, kind, text, isMe, time);
751 void ackReceived (long msgid) {
752 if (msgid <= 0) return; // wtf?!
753 bool changed = false;
754 usize idx = 0;
755 while (idx < resendQueue.length) {
756 if (resendQueue[idx].msgid == msgid) {
757 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
758 resendQueue[$-1] = XMsg.init;
759 resendQueue.length -= 1;
760 resendQueue.assumeSafeAppend;
761 changed = true;
762 } else {
763 ++idx;
766 if (changed) saveResendQueue();
769 void processResendQueue () {
770 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
771 bool doSave = false;
772 auto ctt = MonoTime.currTime+30.seconds;
773 foreach (ref XMsg msg; resendQueue) {
774 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
775 if (msgid < 0) break;
776 msg.msgid = msgid;
777 msg.nextSendTime = ctt;
778 if (msg.resendCount++ != 0) doSave = true;
780 if (doSave) saveResendQueue();
783 void send (const(char)[] text) {
784 void sendOne (const(char)[] text, bool action) {
785 if (text.length == 0) return; // just in case
787 SysTime now = Clock.currTime;
788 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
789 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
791 // add this to resend queue
792 XMsg xmsg;
793 xmsg.isMe = action;
794 xmsg.time = now;
795 xmsg.msgid = msgid;
796 xmsg.nextSendTime = MonoTime.currTime+30.seconds;
797 xmsg.resendCount = 0;
800 import std.datetime;
801 import std.format : format;
802 auto dt = cast(DateTime)now;
803 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
806 resendQueue ~= xmsg;
808 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
809 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
812 bool action = text.startsWith("/me ");
813 if (action) text = text[4..$].xstripleft;
815 while (text.length) {
816 auto ep = text.indexOf('\n');
817 if (ep < 0) ep = text.length; else ++ep; // include '\n'
818 // remove line if it contains only spaces
819 bool hasNonSpace = false;
820 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
821 if (hasNonSpace) break;
822 text = text[ep..$];
824 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
825 if (text.length == 0) return; // nothing to do
827 // now split it
828 //TODO: split at word boundaries
829 enum ReservedSpace = 23+3+3;
831 char[TOX_MAX_MESSAGE_LENGTH+8] tmpbuf = void;
833 bool first = true;
834 while (text.length) {
835 int epos = TOX_MAX_MESSAGE_LENGTH-ReservedSpace;
836 if (epos < text.length) {
837 // find utf start
838 while (epos > 0) {
839 if (text[epos-1] < 128) break;
840 if ((text[epos-1]&0xc0) == 0xc0) break;
841 --epos;
843 } else {
844 epos = cast(int)text.length;
846 assert(epos > 0);
847 if (first && epos >= text.length) {
848 sendOne(text[0..epos], action);
849 } else {
850 int ofs = 0;
851 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
852 tmpbuf[ofs..ofs+epos] = text[0..epos];
853 tmpbuf[ofs+epos..ofs+epos+3] = "...";
854 sendOne(tmpbuf[0..ofs+epos+3], action);
856 first = false;
857 text = text[epos..$];
860 //saveResendQueue();
865 // ////////////////////////////////////////////////////////////////////////// //
866 final class Account {
867 public:
868 void saveGroups () {
869 import std.algorithm : sort;
870 GroupOptions[] glist;
871 scope(exit) delete glist;
872 foreach (Group g; groups) glist ~= g.info;
873 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
874 glist.serialize(basePath~"contacts/groups.rc");
877 private:
878 ContactStatus mStatus = ContactStatus.Offline;
880 public:
881 PubKey toxpk = toxCoreEmptyKey;
882 string toxDataDiskName;
883 string basePath; // with trailing "/"
884 ProtoOptions protoOpts;
885 AccountConfig info;
886 Group[] groups;
887 Contact[PubKey] contacts;
889 public:
890 bool mIAmConnecting = false;
891 bool mIAmOnline = false;
892 bool forceOnline = true; // set to `false` to stop autoreconnecting
893 //bool restoreOnline = false; // will be set to `true` if we need to reconnect
894 //bool doRefreshNicks = false;
896 public:
897 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
899 @property ContactStatus status () const nothrow @safe @nogc {
900 if (!toxpk.isValidKey) return ContactStatus.Offline;
901 if (mIAmConnecting) return ContactStatus.Connecting;
902 if (!mIAmOnline) return ContactStatus.Offline;
903 return mStatus;
906 @property bool isOnline () const nothrow @safe @nogc {
907 if (!toxpk.isValidKey) return false;
908 if (mIAmConnecting) return false;
909 if (!mIAmOnline) return false;
910 return true;
913 @property void status (ContactStatus v) {
914 if (!toxpk.isValidKey) return;
915 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
916 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
917 if (v == ContactStatus.Offline) {
918 forceOnline = false;
919 } else {
920 forceOnline = true;
922 if (mStatus == ContactStatus.Offline) {
923 if (v != ContactStatus.Offline) mIAmConnecting = true;
925 toxCoreSetStatus(toxpk, v);
926 mStatus = v;
927 glconPostScreenRepaint();
929 fixTrayIcon();
932 void processResendQueue () {
933 if (!isOnline) return;
934 foreach (Contact ct; contacts.byValue) ct.processResendQueue();
937 void saveResendQueue () {
938 foreach (Contact ct; contacts.byValue) ct.saveResendQueue();
941 private:
942 void toxCreate () {
943 toxpk = toxCoreOpenAccount(toxDataDiskName);
944 if (!toxpk.isValidKey) {
945 conwriteln("creating new Tox account...");
946 string nick = info.nick;
947 if (nick.length > 0) {
948 //FIXME: utf
949 if (nick.length > TOX_MAX_NAME_LENGTH) nick = nick[0..TOX_MAX_NAME_LENGTH];
950 } else {
951 nick = "anonymous";
953 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
954 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
958 // load contacts from ToxCore data and add 'em to contact database
959 void toxLoadKnownContacts () {
960 if (!toxpk.isValidKey) return;
961 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] nick) {
962 if (nick.length == 0) nick = "<unknown>";
963 auto c = (frpub in contacts ? contacts[frpub] : null);
964 if (c is null) {
965 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
966 c = new Contact(this);
967 c.info.gid = 0;
968 c.info.nick = nick.idup;
969 c.info.pubkey[] = frpub[];
970 c.info.opts.showOffline = TriOption.Yes;
971 contacts[c.info.pubkey] = c;
972 c.save();
973 //HACK!
974 if (clist !is null) clist.buildAccount(this);
975 } else if (c.info.nick != nick) {
976 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
977 if (nick != "<unknown>" && (c.info.nick == "<unknown>" || c.info.nick.length == 0)) {
978 c.info.nick = nick.idup;
979 c.save();
981 } else {
982 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
984 return false; // don't stop
988 private:
989 // connection established
990 void toxConnectionDropped () {
991 alias timp = this;
992 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
993 mIAmConnecting = false;
994 mIAmOnline = false;
995 if (forceOnline) {
996 status = mStatus;
997 mIAmConnecting = true;
999 foreach (Contact ct; contacts.byValue) ct.status = ContactStatus.Offline;
1000 glconPostScreenRepaint();
1003 // connection established
1004 void toxConnectionEstablished () {
1005 alias timp = this;
1006 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
1007 mIAmConnecting = false;
1008 mIAmOnline = true;
1009 toxCoreSetStatusMessage(toxpk, "Come taste the gasoline! [BioAcid]");
1010 toxLoadKnownContacts();
1011 glconPostScreenRepaint();
1014 void toxFriendOffline (in ref PubKey fpk) {
1015 if (auto ct = fpk in contacts) {
1016 if (ct.status != ContactStatus.Offline) {
1017 conwriteln("friend <", ct.info.nick, "> gone offline");
1018 ct.status = ContactStatus.Offline;
1019 glconPostScreenRepaint();
1024 void toxSelfStatus (ContactStatus cst) {
1025 if (mStatus != cst) {
1026 mStatus = cst;
1027 glconPostScreenRepaint();
1031 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
1032 if (auto ct = fpk in contacts) {
1033 if (ct.status != cst) {
1034 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
1035 ct.status = cst;
1036 //if (ct.status != ContactStatus.Offline && ct.status != ContactStatus.Connecting) ct.processResendQueue();
1037 glconPostScreenRepaint();
1042 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
1043 if (auto ct = fpk in contacts) {
1044 if (ct.info.statusmsg != msg) {
1045 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
1046 ct.info.statusmsg = msg;
1047 ct.save();
1048 glconPostScreenRepaint();
1053 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
1054 alias timp = this;
1055 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
1058 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
1059 if (auto ct = fpk in contacts) {
1060 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
1061 ct.appendToLog(kind, msg, action, time);
1062 if (*ct is activeContact) {
1063 // if inactive or invisible, add divider line and increase unread count
1064 if (!mainWindowVisible || !mainWindowActive) {
1065 if (ct.unreadCount == 0) addDividerLine();
1066 ct.unreadCount += 1;
1067 ct.saveUnreadCount();
1069 addTextToLog(this, *ct, kind, action, msg, time);
1070 } else {
1071 ct.unreadCount += 1;
1072 ct.saveUnreadCount();
1077 // ack for sent message
1078 void toxMessageAck (in ref PubKey fpk, long msgid) {
1079 alias timp = this;
1080 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
1081 if (auto ct = fpk in contacts) {
1082 if (*ct is activeContact) ackLogMessage(msgid);
1083 ct.ackReceived(msgid);
1087 private:
1088 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
1090 public:
1091 this (string aBaseDir) {
1092 import std.algorithm : sort;
1093 import std.file : DirEntry, SpanMode, dirEntries;
1094 import std.path : absolutePath, baseName;
1096 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1097 aBaseDir = "./";
1098 } else if (aBaseDir == "/") {
1099 assert(0, "wtf?!");
1100 } else {
1101 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1104 basePath = aBaseDir.absolutePath;
1105 toxDataDiskName = basePath~"toxdata.tox";
1106 protoOpts.txtunser(VFile(basePath~"proto.rc"));
1107 info.txtunser(VFile(basePath~"config.rc"));
1109 // load groups
1110 GroupOptions[] glist;
1111 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
1112 bool hasDefaultGroup = false;
1113 bool hasMoronsGroup = false;
1114 foreach (ref GroupOptions gi; glist[]) {
1115 auto g = new Group(this);
1116 g.info = gi;
1117 bool found = false;
1118 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
1119 if (!found) groups ~= g;
1120 if (g.gid == 0) hasDefaultGroup = true;
1121 if (g.gid == g.gid.max) hasMoronsGroup = true;
1124 // create default group if necessary
1125 if (!hasDefaultGroup) {
1126 GroupOptions gi;
1127 gi.gid = 0;
1128 gi.name = "default";
1129 gi.note = "default group for new contacts";
1130 gi.opened = true;
1131 auto g = new Group(this);
1132 g.info = gi;
1133 groups ~= g;
1136 // create morons group if necessary
1137 if (!hasMoronsGroup) {
1138 GroupOptions gi;
1139 gi.gid = gi.gid.max;
1140 gi.name = "<morons>";
1141 gi.note = "group for completely ignored dumbfucks";
1142 gi.opened = false;
1143 gi.showOffline = TriOption.No;
1144 gi.showPopup = TriOption.No;
1145 gi.blinkActivity = TriOption.No;
1146 gi.skipUnread = TriOption.Yes;
1147 gi.hideIfNoVisibleMembers = TriOption.Yes;
1148 gi.ftranAllowed = TriOption.No;
1149 gi.resendRotDays = 0;
1150 gi.hmcOnOpen = 0;
1151 auto g = new Group(this);
1152 g.info = gi;
1153 groups ~= g;
1154 saveGroups();
1157 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1159 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1161 // load contacts
1162 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1163 if (de.name.baseName == "." || de.name.baseName == "..") continue;
1164 try {
1165 import std.file : exists;
1166 if (!de.isDir) continue;
1167 string cfgfn = de.name~"/config.rc";
1168 if (!cfgfn.exists) continue;
1169 ContactInfo ci;
1170 ci.txtunser(VFile(cfgfn));
1171 auto c = new Contact(this);
1172 c.diskFileName = cfgfn;
1173 c.info = ci;
1174 contacts[c.info.pubkey] = c;
1175 c.loadResendQueue();
1176 c.loadUnreadCount();
1177 // fix contact group
1178 if (groupById!false(c.gid) is null) {
1179 c.info.gid = 0; // move to default group
1180 c.save();
1182 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1183 } catch (Exception e) {
1184 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1188 toxCreate();
1189 assert(toxpk.isValidKey, "something is VERY wrong here");
1190 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1193 ~this () {
1194 if (toxpk.isValidKey) {
1195 toxCoreCloseAccount(toxpk);
1196 toxpk[] = toxCoreEmptyKey[];
1200 // will not write contact to disk
1201 Contact createEmptyContact () {
1202 auto c = new Contact(this);
1203 c.info.gid = 0;
1204 c.info.nick = "test contact";
1205 c.info.pubkey[] = 0;
1206 return c;
1209 // returns `null` if there is no such group, and `dofail` is `true`
1210 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1211 foreach (const Group g; groups) if (g.gid == agid) return cast(typeof(return))g;
1212 static if (dofail) assert(0, "group not found"); else return null;
1215 int opApply () (scope int delegate (ref Contact ct) dg) {
1216 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1217 return 0;
1220 public:
1221 static Account CreateNew (string aBaseDir, string aAccName) {
1222 import std.file : mkdirRecurse;
1223 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1224 aBaseDir = "./";
1225 } else if (aBaseDir == "/") {
1226 assert(0, "wtf?!");
1227 } else {
1228 mkdirRecurse(aBaseDir);
1229 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1231 mkdirRecurse(aBaseDir~"/contacts");
1232 // write protocol options
1234 ProtoOptions popt;
1235 popt.txtser(VFile(aBaseDir~"proto.rc", "w"), skipstname:true);
1237 // account options
1239 AccountConfig acc;
1240 acc.nick = aAccName;
1241 acc.showPopup = true;
1242 acc.blinkActivity = true;
1243 acc.hideEmptyGroups = false;
1244 acc.ftranAllowed = true;
1245 acc.resendRotDays = 4;
1246 acc.hmcOnOpen = 10;
1247 acc.txtser(VFile(aBaseDir~"config.rc", "w"), skipstname:true);
1249 // create default group
1251 GroupOptions[1] grp;
1252 grp[0].gid = 0;
1253 grp[0].name = "default";
1254 grp[0].opened = true;
1255 //grp[0].hideIfNoVisible = TriOption.Yes;
1256 grp[].txtser(VFile(aBaseDir~"contacts/groups.rc", "w"), skipstname:true);
1258 // now load it
1259 return new Account(aBaseDir);
1264 // ////////////////////////////////////////////////////////////////////////// //
1265 class ListItemBase {
1266 private:
1267 this () {}
1269 protected:
1270 bool mVisible = true;
1272 public:
1273 void setupFont () => nvg.fontFace = "ui"; // setup font face for this item
1274 @property int height () => cast(int)nvg.textFontHeight;
1275 @property bool visible () => mVisible;
1276 bool onMouse (MouseEvent event) => false; // true: eaten
1277 bool onKey (KeyEvent event) => false; // true: eaten
1278 void drawAt (int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
1280 @property Account ownerAcc () => null;
1284 class ListItemAccount : ListItemBase {
1285 public:
1286 Account acc;
1288 public:
1289 this (Account aAcc) { assert(aAcc !is null); acc = aAcc; }
1291 override @property Account ownerAcc () => acc;
1293 override void setupFont () => nvg.fontFace = "uib"; // bold
1295 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1296 int hgt = height;
1298 nvg.newPath();
1299 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1300 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
1301 nvg.fill();
1303 final switch (acc.status) {
1304 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
1305 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
1306 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
1307 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
1308 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
1310 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
1311 nvg.textAlign = NVGTextAlign.V.Baseline;
1312 nvg.textAlign = NVGTextAlign.H.Center;
1313 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
1318 class ListItemGroup : ListItemBase {
1319 public:
1320 Group group;
1322 public:
1323 this (Group aGroup) { assert(aGroup !is null); group = aGroup; }
1325 override @property Account ownerAcc () => group.acc;
1327 override @property bool visible () => group.visible;
1329 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1330 int hgt = height;
1332 nvg.newPath();
1333 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1334 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
1335 nvg.fill();
1337 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
1338 nvg.textAlign = NVGTextAlign.V.Baseline;
1339 nvg.textAlign = NVGTextAlign.H.Center;
1340 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
1345 class ListItemContact : ListItemBase {
1346 public:
1347 Contact ct;
1349 public:
1350 this (Contact aCt) { assert(aCt !is null); ct = aCt; }
1352 override @property Account ownerAcc () => ct.acc;
1354 override @property bool visible () => ct.visible;
1356 override @property int height () { import std.algorithm : max; return max(cast(int)nvg.textFontHeight, 16); } //FIXME: 16 is image height
1358 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1359 int hgt = height;
1361 if (selected) {
1362 nvg.newPath();
1363 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1364 nvg.fillColor(NVGColor("#9600"));
1365 nvg.fill();
1368 // draw icon
1369 nvg.newPath();
1370 int iw, ih;
1371 NVGImage icon;
1372 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
1373 nvg.imageSize(icon, iw, ih);
1374 //conwriteln("image: (", iw, "x", ih, ")");
1375 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
1376 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
1377 nvg.fill();
1379 nvg.fillColor(NVGColor("#f70"));
1380 nvg.textAlign = NVGTextAlign.V.Baseline;
1381 nvg.textAlign = NVGTextAlign.H.Left;
1382 //conwriteln(nvg.textFontDescender);
1383 nvg.text(x0+4+iw+4, y0+hgt+cast(int)nvg.textFontDescender, ct.displayNick);
1388 // ////////////////////////////////////////////////////////////////////////// //
1389 // visible contact list
1390 final class CList {
1391 private import core.time;
1393 private:
1394 int mActiveItem = -1; // active item (may be different from selected with cursor)
1395 int mTopY;
1396 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
1397 //MonoTime mLastClick = MonoTime.zero;
1398 //int mLastClickItem = -1;
1400 public:
1401 ListItemBase[] items;
1403 public:
1404 this () {}
1406 // the first one is "main"; can return `null`
1407 Account mainAccount () {
1408 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
1409 return null;
1412 // the first one is "main"; can return `null`
1413 Account accountByPK (in ref PubKey pk) {
1414 foreach (ListItemBase li; items) {
1415 if (auto lc = cast(ListItemAccount)li) {
1416 if (lc.acc.toxpk[] == pk[]) return lc.acc;
1419 return null;
1422 void forEachAccount (scope void delegate (Account acc) dg) {
1423 if (dg is null) return;
1424 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
1427 void opOpAssign(string op:"~") (ListItemBase li) {
1428 if (li is null) return;
1429 items ~= li;
1432 void setFont () {
1433 //nvg.fontFace = "ui";
1434 nvg.fontSize = 20;
1435 nvg.fontBlur = 0;
1436 nvg.textAlign = NVGTextAlign.H.Left;
1437 nvg.textAlign = NVGTextAlign.V.Baseline;
1440 void removeAccount (Account acc) {
1441 if (acc is null) return;
1442 usize pos = 0, dest = 0;
1443 while (pos < items.length) {
1444 if (items[pos].ownerAcc !is acc) {
1445 if (pos != dest) items[dest++] = items[pos];
1447 ++pos;
1449 items.length = dest;
1452 void buildAccount (Account acc) {
1453 if (acc is null) return;
1454 removeAccount(acc);
1455 items ~= new ListItemAccount(acc);
1456 Contact[] css;
1457 scope(exit) delete css;
1458 foreach (Group g; acc.groups) {
1459 items ~= new ListItemGroup(g);
1460 css.length = 0;
1461 css.assumeSafeAppend;
1462 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
1463 import std.algorithm : sort;
1464 css.sort!((a, b) {
1465 string s0 = a.displayNick;
1466 string s1 = b.displayNick;
1467 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
1468 foreach (immutable idx, char c0; s0[0..xlen]) {
1469 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
1470 char c1 = s1[idx];
1471 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
1472 if (auto d = c0-c1) return (d < 0);
1474 return (s0.length < s1.length);
1476 foreach (Contact c; css) items ~= new ListItemContact(c);
1480 // -1: oops
1481 // should be called after clist was drawn at least once
1482 int itemAtY (int aty) {
1483 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
1484 nvg.save();
1485 scope(exit) nvg.restore();
1486 setFont();
1487 foreach (immutable iidx, ListItemBase li; items) {
1488 if (iidx != mActiveItem && !li.visible) continue;
1489 li.setupFont();
1490 int lh = li.height;
1491 if (lh < 1) continue;
1492 if (aty < lh) return cast(int)iidx;
1493 aty -= lh;
1495 return -1;
1498 // if called with `null` ct, deactivate
1499 void delegate (Contact ct) onActivateContactCB;
1501 // true: eaten
1502 bool onMouse (MouseEvent event) {
1503 if (mLastWidth < 1 || mLastHeight < 1) return false;
1504 int mx = event.x-mLastX;
1505 int my = event.y-mLastY;
1506 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
1507 if (event == "LMB-Down") {
1508 int it = itemAtY(my);
1509 if (it >= 0 && it != mActiveItem) {
1510 if (auto ci = cast(ListItemContact)items[it]) {
1511 mActiveItem = it;
1512 if (onActivateContactCB !is null) onActivateContactCB(ci.ct);
1513 glconPostScreenRepaint();
1517 return true;
1520 // true: eaten
1521 bool onKey (KeyEvent event) {
1522 return false;
1525 // real rect is scissored
1526 void drawAt (int x0, int y0, int wdt, int hgt) {
1527 mLastX = x0;
1528 mLastY = y0;
1529 mLastHeight = hgt;
1530 mLastWidth = wdt;
1531 nvg.save();
1532 scope(exit) nvg.restore();
1533 setFont();
1534 int y = 0;
1535 foreach (immutable iidx, ListItemBase li; items) {
1536 if (iidx != mActiveItem && !li.visible) continue;
1537 li.setupFont();
1538 int lh = li.height;
1539 if (lh < 1) continue;
1540 nvg.save();
1541 scope(exit) nvg.restore();
1542 version(all) {
1543 nvg.intersectScissor(x0, y0+y, wdt, lh);
1544 li.drawAt(x0, y0+y, wdt, (iidx == mActiveItem));
1545 } else {
1546 nvg.translate(x0+0.5f, y0+y+0.5f);
1547 nvg.intersectScissor(0, 0, wdt, lh);
1548 li.drawAt(0, 0, wdt);
1550 y += lh;
1551 if (y >= hgt) break;
1557 // ////////////////////////////////////////////////////////////////////////// //
1558 __gshared CList clist;
1559 __gshared Contact activeContact;
1562 void loadAccount (string nick) {
1563 Account acc;
1565 try {
1566 acc = new Account(nick);
1567 } catch (Exception e) {
1568 conwriteln("creating account...");
1569 assert(0, "not yet");
1570 acc = Account.CreateNew("_fakeacc", "ketmar");
1572 // create fake contact
1573 if (acc.contacts.length == 0) {
1574 conwriteln("creating fake contact...");
1575 auto c = acc.createEmptyContact();
1576 c.info.nick = "test contact";
1577 c.info.pubkey[] = 0x55;
1578 c.save();
1581 clist.buildAccount(acc);
1585 // ////////////////////////////////////////////////////////////////////////// //
1586 // null: deactivate
1587 void doActivateContact (Contact ct) {
1588 if (activeContact is ct) return;
1590 activeContact = ct;
1591 wipeLog();
1592 if (ct is null) {
1593 //conwriteln("clear log");
1594 if (clist !is null) clist.mActiveItem = -1;
1595 glconPostScreenRepaint();
1596 } else {
1597 //conwriteln("activated contact <", ct.info.nick, ">: [", tox_hex(ct.info.pubkey), "]");
1598 LogFile log;
1599 ct.loadLogInto(log);
1600 auto mcount = cast(int)log.messages.length;
1601 int left = ct.hmcOnOpen;
1602 if (left < ct.unreadCount) left = ct.unreadCount;
1603 if (left > mcount) left = mcount;
1604 if (mcount > left) mcount = left;
1605 if (mcount > 0) {
1606 foreach (const ref msg; log.messages[$-mcount..$]) {
1607 if (left == ct.unreadCount) addDividerLine();
1608 addTextToLog(ct.acc, ct, msg);
1609 --left;
1612 if (ct.unreadCount != 0) { ct.unreadCount = 0; ct.saveUnreadCount(); }
1615 fixTrayIcon();
1619 //FIXME: scan all accounts
1620 void fixTrayIcon () {
1621 if (clist is null) return;
1622 auto acc = clist.mainAccount;
1623 if (acc is null) return;
1624 int unc = 0;
1625 foreach (Contact ct; acc) unc += ct.unreadCount;
1626 if (unc) {
1627 import std.format : format;
1628 setTrayUnread();
1629 setHint("unread: %d".format(unc));
1630 } else {
1631 setTrayStatus(acc.status);
1632 final switch (acc.status) {
1633 case ContactStatus.Connecting: setHint("connecting..."); break;
1634 case ContactStatus.Offline: setHint("offline"); break;
1635 case ContactStatus.Online: setHint("online"); break;
1636 case ContactStatus.Away: setHint("away"); break;
1637 case ContactStatus.Busy: setHint("busy"); break;
1643 void fixUnreadIndicators () {
1644 if (!mainWindowVisible || !mainWindowActive) return; // nothing to do
1645 if (activeContact is null || activeContact.unreadCount == 0) return; // nothing to do
1646 activeContact.unreadCount = 0;
1647 activeContact.unreadCount = 0;
1648 activeContact.saveUnreadCount();
1649 fixTrayIcon();
1653 // ////////////////////////////////////////////////////////////////////////// //
1654 //__gshared string accountNameToLoad = "_fakeacc";
1655 __gshared string accountNameToLoad = "";
1656 __gshared string globalHotkey = "M-H-F";
1659 void main (string[] args) {
1660 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
1662 conRegVar!accountNameToLoad("starting_account", "account to load");
1664 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
1666 glconShowKey = "M-Grave";
1667 glconSetAndSealFPS(0); // draw-on-demand
1669 conProcessQueue(256*1024); // load config
1670 conProcessArgs!true(args);
1671 conProcessQueue(256*1024);
1673 if (accountNameToLoad.length == 0) assert(0, "no account to load");
1675 //setOpenGLContextVersion(3, 2); // up to GLSL 150
1676 setOpenGLContextVersion(2, 0); // it's enough
1678 loadAllFonts();
1681 NVGPathSet svp = null;
1683 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
1684 sdpyWindowClass = "BIOACID";
1685 auto sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
1686 glconCtlWindow = sdmain;
1688 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
1689 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
1691 try {
1692 if (globalHotkey.length > 0) {
1693 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
1695 } catch (Exception e) {
1696 conwriteln("ERROR registering hotkey!");
1699 conRegFunc!(() {
1700 if (sdmain !is null) sdmain.close();
1701 })("quit", "quit BioAcid");
1703 conRegFunc!(() {
1704 if (sdmain !is null && !sdmain.closed) {
1705 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
1706 if (!mainWindowVisible) {
1707 // this strange code brings window to the current desktop it if was on a different one
1708 sdmain.hide();
1709 sdmain.show();
1710 } else if (sdmain.visible) {
1711 sdmain.hide();
1712 } else {
1713 sdmain.show();
1715 flushGui();
1717 })("win_toggle", "show/hide main window");
1719 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
1720 if (sdmain.closed) return;
1721 if (isQuitRequested) { sdmain.close(); return; }
1722 sdmain.redrawOpenGlSceneNow();
1725 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
1726 glconProcessEventMessage();
1729 sdmain.addEventListener((PopupCheckerEvent evt) {
1730 popupCheckExpirations();
1734 // ////////////////////////////////////////////////////////////////////// //
1735 // tox core events
1736 sdmain.addEventListener((ToxEventBase evt) {
1737 auto acc = clist.accountByPK(evt.self);
1739 if (acc is null) return;
1741 bool fixTray = false;
1742 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1744 // connection?
1745 if (auto e = cast(ToxEventConnection)evt) {
1746 if (e.who[] == acc.toxpk[]) {
1747 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1748 fixTray = true;
1749 } else {
1750 if (!e.connected) acc.toxFriendOffline(e.who);
1752 return;
1754 // status?
1755 if (auto e = cast(ToxEventStatus)evt) {
1756 if (e.who[] == acc.toxpk[]) {
1757 acc.toxSelfStatus(e.status);
1758 fixTray = true;
1759 } else {
1760 acc.toxFriendStatus(e.who, e.status);
1762 return;
1764 // status message?
1765 if (auto e = cast(ToxEventStatusMsg)evt) {
1766 if (e.who[] != acc.toxpk[]) {
1767 acc.toxFriendStatusMessage(e.who, e.message);
1769 return;
1771 // incoming text message?
1772 if (auto e = cast(ToxEventMessage)evt) {
1773 if (e.who[] != acc.toxpk[]) {
1774 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1775 fixTray = true;
1777 return;
1779 // ack outgoing text message?
1780 if (auto e = cast(ToxEventMessageAck)evt) {
1781 if (e.who[] != acc.toxpk[]) {
1782 acc.toxMessageAck(e.who, e.msgid);
1784 return;
1787 //glconProcessEventMessage();
1791 // ////////////////////////////////////////////////////////////////////// //
1792 sdmain.onClosing = delegate () {
1793 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
1794 popupKillAll();
1795 if (nvg !is null) {
1796 sdmain.setAsCurrentOpenGlContext();
1797 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
1798 svp.kill();
1799 nvg.kill();
1801 assert(nvg is null);
1802 if (sdhint !is null) sdhint.close();
1803 if (trayicon !is null) trayicon.close();
1806 sdmain.closeQuery = delegate () {
1807 concmd("quit");
1808 glconPostDoConCommands!true();
1811 // first time setup
1812 sdmain.visibleForTheFirstTime = delegate () {
1813 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
1814 sdmain.setAsCurrentOpenGlContext(); // make this window active
1815 scope(exit) sdmain.releaseCurrentOpenGlContext();
1816 sdmain.vsync = false;
1818 glconInit(sdmain.width, sdmain.height);
1820 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
1821 if (nvg is null) assert(0, "cannot initialize NanoVG");
1822 loadFonts(nvg);
1824 try {
1825 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
1826 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
1827 auto xi = loadImageFromMemory(skullsPng[]);
1828 scope(exit) delete xi;
1829 //{ import core.stdc.stdio; printf("creating background image...\n"); }
1830 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
1831 //{ import core.stdc.stdio; printf("background image created\n"); }
1832 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
1833 } catch (Exception e) {
1834 assert(0, "cannot load background image");
1836 buildStatusImages();
1838 prepareTrayIcon();
1840 lay = new LayTextClass(laf, sdmain.width/2);
1841 lay.fontStyle.fontsize = 16;
1842 lay.fontStyle.color = NVGColor.darkorange.asUint;
1843 lay.fontStyle.monospace = true;
1844 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1845 lay.put("ketmar (бля)");
1846 lay.fontStyle.monospace = false;
1847 lay.putHardSpace(64);
1848 lay.putExpander();
1849 lay.put("!");
1850 lay.putExpander();
1851 lay.put("2018/02/12");
1852 lay.putNBSP();
1853 lay.fontStyle.color = NVGColor("#fff").asUint;
1854 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1855 lay.put("11:09:03");
1856 lay.endPara();
1857 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1858 lay.fontStyle.color = NVGColor.darkorange.asUint;
1859 lay.fontStyle.fontsize = 20;
1860 lay.put("this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1861 lay.endPara();
1862 lay.finalize();
1863 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
1864 lastWindowWidth = sdmain.width;
1866 clist = new CList();
1867 loadAccount(accountNameToLoad);
1868 //clist.buildAccount(acc);
1869 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
1871 sdmain.setMinSize(640, 480);
1873 fixTrayIcon();
1874 //sdmain.redrawOpenGlSceneNow();
1877 sdmain.windowResized = delegate (int wdt, int hgt) {
1878 if (sdmain.closed) return;
1879 glconResize(wdt, hgt);
1880 glconPostScreenRepaint/*Delayed*/();
1881 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
1882 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
1883 immutable double frc = lastWindowWidth/optCListWidth;
1884 optCListWidth = cast(int)(wdt/frc);
1885 if (optCListWidth < 64) optCListWidth = 64;
1886 lastWindowWidth = wdt;
1888 lay.relayout(wdt);
1892 int mouseX = -666, mouseY = -666;
1895 sdmain.handleKeyEvent = delegate (KeyEvent event) {
1896 if (sdmain.closed) return;
1897 scope(exit) glconPostDoConCommands!true();
1898 if (glconKeyEvent(event)) return;
1900 auto acc = clist.mainAccount;
1902 if (event == "D-Escape") {
1903 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
1904 sdmain.hide();
1905 flushGui();
1907 return;
1910 if (event == "D-C-Q") { concmd("quit"); return; }
1911 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
1912 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
1913 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
1915 if (event == "D-C-W") { doActivateContact(null); return; }
1917 if (clist !is null && clist.onKey(event)) return;
1919 if (event == "D-C-S-Enter") {
1920 static PopupWindow.Kind kind;
1921 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1922 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1923 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
1924 return;
1927 if (activeContact !is null) {
1928 if (event == "D-Enter") {
1929 auto text = activeContact.edit.text;
1930 activeContact.edit.clear();
1931 activeContact.send(text);
1932 glconPostScreenRepaint();
1933 return;
1935 if (activeContact.edit.onKey(event)) return;
1939 if (event == "D-Space") {
1940 // add random text
1941 lay.fontStyle.fontsize = 16;
1942 lay.fontStyle.color = NVGColor.k8orange.asUint;
1943 lay.fontStyle.monospace = true;
1944 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1945 lay.put("ketmar");
1946 lay.fontStyle.monospace = false;
1947 lay.putHardSpace(64);
1948 lay.putExpander();
1949 lay.put("!");
1950 lay.putExpander();
1951 lay.put("2018/02/12");
1952 lay.putNBSP();
1953 lay.fontStyle.color = NVGColor("#fff").asUint;
1954 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1955 lay.put("11:09:03");
1956 lay.endPara();
1957 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1958 lay.fontStyle.color = NVGColor.k8orange.asUint;
1959 lay.fontStyle.fontsize = 20;
1960 import std.random;
1961 // words
1962 version(none) {
1963 foreach (immutable widx; 0..uniform!"[]"(2, 28)) {
1964 // one word
1965 if (widx != 0) lay.put(" ");
1966 foreach (; 0..uniform!"[]"(1, 9)) {
1967 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1968 lay.put(ch);
1969 //lay.putSoftHypen();
1972 } else {
1973 // long word, for hyphenation test
1974 foreach (immutable idx; 0..228) {
1975 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1976 lay.put(ch);
1977 if (idx%24 == 23) lay.putSoftHypen();
1980 lay.endPara();
1981 lay.finalize();
1983 setHint("boo!");
1984 // redraw
1985 glconPostScreenRepaint();
1986 return;
1991 int msLastPressX = -666, msLastPressY = -666;
1992 bool msDoLogButton = false;
1994 sdmain.handleMouseEvent = delegate (MouseEvent event) {
1995 if (sdmain.closed) return;
1996 scope(exit) glconPostDoConCommands!true();
1997 if (isConsoleVisible) return;
1998 mouseX = event.x;
1999 mouseY = event.y;
2001 // check for href
2002 //FIXME: process it here, not in renderer
2003 if (event == "LMB-Down") {
2004 msLastPressX = mouseX;
2005 msLastPressY = mouseY;
2006 msDoLogButton = true;
2007 glconPostScreenRepaint();
2010 if (clist !is null) {
2011 sdmain.setAsCurrentOpenGlContext(); // make this window active
2012 scope(exit) sdmain.releaseCurrentOpenGlContext();
2014 bool inFrame = nvg.inFrame;
2015 if (!inFrame) nvg.beginFrame(sdmain.width, sdmain.height);
2016 scope(exit) if (!inFrame) nvg.endFrame();
2018 if (clist.onMouse(event)) { /*glconPostScreenRepaint();*/ return; }
2020 /*if (svp is null)*/ { glconPostScreenRepaint(); return; } // mouse motion
2023 sdmain.handleCharEvent = delegate (dchar ch) {
2024 if (sdmain.closed) return;
2025 scope(exit) glconPostDoConCommands!true();
2026 if (glconCharEvent(ch)) return;
2028 if (activeContact !is null) {
2029 if (activeContact.edit.onChar(ch)) return;
2033 // draw main screen
2034 sdmain.redrawOpenGlScene = delegate () {
2035 glconPostDoConCommands!true();
2036 if (sdmain.closed) return;
2037 // draw main screen
2038 scope(exit) glconDraw();
2040 if (nvg is null) return;
2041 sdmain.setAsCurrentOpenGlContext(); // make this window active
2042 scope(exit) sdmain.releaseCurrentOpenGlContext();
2044 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
2045 glViewport(0, 0, sdmain.width, sdmain.height);
2046 glMatrixMode(GL_MODELVIEW);
2047 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
2049 glClearColor(0, 0, 0, 0);
2050 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
2053 nvg.beginFrame(sdmain.width, sdmain.height);
2054 scope(exit) nvg.endFrame();
2056 if (clist !is null && optCListWidth > 0) {
2057 // draw contact list
2058 int cx = 1;
2059 int cy = 1;
2060 int wdt = optCListWidth;
2061 int hgt = nvg.height-cy*2;
2063 nvg.shapeAntiAlias = true;
2064 nvg.nonZeroFill;
2065 nvg.strokeWidth = 1;
2068 nvg.save();
2069 scope(exit) nvg.restore();
2071 nvg.newPath();
2072 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
2074 int w, h;
2075 nvg.imageSize(nvgSkullsImg, w, h);
2076 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
2078 nvg.strokeColor(NVGColor("#f70"));
2079 nvg.fill();
2080 nvg.stroke();
2082 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
2084 clist.drawAt(cx+3, cy+3, wdt-3*2, hgt-3*2);
2088 nvg.save();
2089 scope(exit) nvg.restore();
2091 //nvg.transform(NVGMatrix.init);
2093 auto xf = nvg.currTransform;
2094 nvg.currTransform = xf;
2097 // draw chat log
2098 cx += wdt+2;
2099 wdt = nvg.width-cx-1;
2100 auto baphHgt = hgt;
2102 // calculate editor dimensions and draw editor
2103 if (activeContact !is null) {
2104 nvg.save();
2105 scope(exit) nvg.restore();
2107 auto edinfo = activeContact.edit.calcHeight(nvg, wdt-3*2);
2108 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
2110 int edy = cy+hgt-cast(int)edinfo.height-3*2;
2112 nvg.newPath();
2113 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edinfo.height+3*2, 6);
2114 nvg.fillColor(NVGColor.black);
2115 nvg.strokeColor(NVGColor("#f70"));
2116 nvg.fill();
2117 nvg.stroke();
2119 nvg.intersectScissor(cx+3.5f, edy+3.5f, wdt-3*2, edinfo.height);
2120 activeContact.edit.draw(nvg, cx+3, edy+3, wdt-3*2, cast(int)edinfo.height);
2122 hgt -= cast(int)edinfo.height+3*2;
2125 nvg.newPath();
2126 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
2127 nvg.fillColor(NVGColor.black);
2128 nvg.strokeColor(NVGColor("#f70"));
2129 nvg.fill();
2130 nvg.stroke();
2132 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
2133 version(all) {
2134 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
2135 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
2136 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
2137 immutable float sz = BaphometDims*scale;
2138 nvg.strokeColor(NVGColor("#400"));
2139 nvg.fillColor(NVGColor("#400"));
2140 nvg.renderBaphomet(cx+10.5f+(wdt-3*2-10*2)/2-sz/2, cy+10.5f+(baphHgt-3*2-10*2)/2-sz/2, scale, scale);
2143 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
2144 immutable float sby = cy+3.5f;
2145 wdt -= BND_SCROLLBAR_WIDTH+1;
2146 wdt -= 3*2;
2147 hgt -= 3*2;
2149 lay.relayout(wdt); // this is harmess if width wasn't changed
2150 int ty = lay.textHeight-hgt+1;
2151 if (ty < 0) ty = 0;
2153 nvg.bndScrollSlider(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 0.0f, (lay.textHeight > 0 ? ty+hgt/cast(float)lay.textHeight : 1.0f));
2155 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt, hgt);
2156 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
2158 if (msDoLogButton) {
2159 msDoLogButton = false;
2160 auto widx = lay.wordAtXY(msLastPressX-(cx+3), ty+(msLastPressY-(cy+3)));
2161 if (widx >= 0) {
2162 if (auto hr = cast(uint)widx in layUrlList) {
2163 conwriteln("URL CLICK: <", hr.url);
2164 openUrl(hr.url);
2173 flushGui();
2174 sdmain.eventLoop(15000,
2175 // pulser: process resend queues here
2176 delegate () {
2177 if (sdmain.closed || clist is null) return;
2178 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
2181 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
2182 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
2183 toxCoreShutdownAll();
2184 popupKillAll();
2185 flushGui();
2186 conProcessQueue(int.max/4);