fixed lastseen text (cosmetix)
[bioacid.git] / bioacid.d
blob4de37b1aa683c4cb4ad08f9bb7a3fc15af941bf9
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.sdpyutil;
35 import iv.unarray;
36 import iv.utfutil;
37 import iv.vfs.io;
39 version(sfnt_test) import iv.nanovega.simplefont;
41 import accdb;
42 import accobj;
43 import fonts;
44 import popups;
45 import icondata;
46 import notifyicon;
47 import toxproto;
49 import tkmain;
50 import tklog;
51 import tkclist;
52 import tkminiedit;
55 // ////////////////////////////////////////////////////////////////////////// //
56 //__gshared string accountNameToLoad = "_fakeacc";
57 __gshared string accountNameToLoad = "";
58 __gshared string globalHotkey = "M-H-F";
59 __gshared bool optAccountAllowCreate = false;
62 void main (string[] args) {
63 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
65 conRegVar!accountNameToLoad("starting_account", "account to load");
66 conRegVar!optAccountAllowCreate("account_allow_creating", "create new account if BioAcid can't load it");
67 conRegVar!optShowOffline("show_offline", "always show offline contacts?");
68 conRegVar!optHRLastSeen("hr_lastseen", "human-readable lastseen?");
70 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
72 //glconShowKey = "M-Grave";
73 glconSetAndSealFPS(0); // draw-on-demand
75 conProcessQueue(256*1024); // load config
76 conProcessArgs!true(args);
77 conProcessQueue(256*1024);
79 if (accountNameToLoad.length == 0) assert(0, "no account to load");
81 //setOpenGLContextVersion(3, 2); // up to GLSL 150
82 setOpenGLContextVersion(2, 0); // it's enough
84 loadAllFonts();
87 NVGPathSet svp = null;
89 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
90 sdpyWindowClass = "BIOACID";
91 sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
92 glconCtlWindow = sdmain;
94 setupToxCoreSender();
95 setupToxEventListener(sdmain);
97 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
98 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
100 MiniEdit sysedit = new MiniEdit();
102 MiniEdit currEdit () { return (activeContact !is null ? activeContact.edit : sysedit); }
104 try {
105 if (globalHotkey.length > 0) {
106 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
108 } catch (Exception e) {
109 conwriteln("ERROR registering hotkey!");
112 conRegFunc!(() {
113 if (sdmain !is null) sdmain.close();
114 })("quit", "quit BioAcid");
116 conRegFunc!(() {
117 if (sdmain !is null && !sdmain.closed) {
118 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
119 if (!mainWindowVisible) {
120 // this strange code brings window to the current desktop it if was on a different one
121 sdmain.hide();
122 sdmain.show();
123 } else if (sdmain.visible) {
124 sdmain.hide();
125 } else {
126 sdmain.show();
128 flushGui();
130 })("win_toggle", "show/hide main window");
132 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
133 if (sdmain.closed) return;
134 if (isQuitRequested) { sdmain.close(); return; }
135 sdmain.redrawOpenGlSceneNow();
138 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
139 glconProcessEventMessage();
142 sdmain.addEventListener((PopupCheckerEvent evt) {
143 popupCheckExpirations();
147 // ////////////////////////////////////////////////////////////////////// //
148 sdmain.onClosing = delegate () {
149 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
150 popupKillAll();
151 if (nvg !is null) {
152 sdmain.setAsCurrentOpenGlContext();
153 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
154 svp.kill();
155 nvg.kill();
157 assert(nvg is null);
158 if (sdhint !is null) sdhint.close();
159 if (trayicon !is null) trayicon.close();
162 sdmain.closeQuery = delegate () {
163 concmd("quit");
164 glconPostDoConCommands!true();
167 // first time setup
168 sdmain.visibleForTheFirstTime = delegate () {
169 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
170 sdmain.setAsCurrentOpenGlContext(); // make this window active
171 scope(exit) sdmain.releaseCurrentOpenGlContext();
172 sdmain.vsync = false;
174 glconInit(sdmain.width, sdmain.height);
176 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
177 if (nvg is null) assert(0, "cannot initialize NanoVG");
178 loadFonts();
180 try {
181 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
182 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
183 auto xi = loadImageFromMemory(skullsPng[]);
184 scope(exit) delete xi;
185 //{ import core.stdc.stdio; printf("creating background image...\n"); }
186 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
187 //{ import core.stdc.stdio; printf("background image created\n"); }
188 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
189 } catch (Exception e) {
190 assert(0, "cannot load background image");
192 buildStatusImages();
194 prepareTrayIcon();
196 lay = new LayTextClass(laf, sdmain.width/2);
197 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
198 lastWindowWidth = sdmain.width;
200 clist = new CList();
201 loadAccount(accountNameToLoad, optAccountAllowCreate);
202 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
203 addContactCommands();
205 sdmain.setMinSize(640, 480);
207 fixTrayIcon();
208 //sdmain.redrawOpenGlSceneNow();
211 sdmain.windowResized = delegate (int wdt, int hgt) {
212 if (sdmain.closed) return;
213 glconResize(wdt, hgt);
214 glconPostScreenRepaint/*Delayed*/();
215 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
216 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
217 immutable double frc = lastWindowWidth/optCListWidth;
218 optCListWidth = cast(int)(wdt/frc);
219 if (optCListWidth < 64) optCListWidth = 64;
220 lastWindowWidth = wdt;
222 lay.relayout(wdt);
226 int mouseX = -666, mouseY = -666;
229 sdmain.handleKeyEvent = delegate (KeyEvent event) {
230 if (sdmain.closed) return;
231 scope(exit) glconPostDoConCommands!true();
232 if (glconKeyEvent(event)) return;
234 auto acc = clist.mainAccount;
236 if (event == "D-Escape") {
237 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
238 sdmain.hide();
239 flushGui();
241 return;
244 scope(exit) { if (event.pressed) glconPostScreenRepaint(); }
246 if (event == "D-C-Q") { concmd("quit"); return; }
247 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
248 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
249 if (event == "D-C-3") { acc.status = ContactStatus.Busy; return; }
250 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
252 if (event == "D-C-W") { doActivateContact(null); return; }
254 if (clist !is null) {
255 if (clist.onKey(event)) return;
259 if (event == "D-C-S-Enter") {
260 static PopupWindow.Kind kind;
261 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
262 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
263 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
264 return;
268 if (event == "D-Enter") {
269 auto text = currEdit.text.xstrip;
270 currEdit.clear();
271 if (text.length == 0) return;
273 // `null`: eol
274 string getWord () {
275 while (text.length && text[0] <= ' ') text = text[1..$];
276 if (text.length == 0) return null;
277 if (text[0] == '"' || text[0] == '\'') {
278 string res;
279 char ech = text[0];
280 text = text[1..$];
281 while (text.length && text[0] != ech) {
282 if (text[0] == '\\') {
283 text = text[1..$];
284 if (text.length == 0) break;
285 char ch = text[0];
286 switch (ch) {
287 case '\r': res ~= '\r'; break;
288 case '\n': res ~= '\n'; break;
289 case '\t': res ~= '\t'; break;
290 default: res ~= ch; break;
292 } else {
293 res ~= text[0];
295 text = text[1..$];
297 if (text.length) { assert(text[0] == ech); text = text[1..$]; }
298 return res;
299 } else {
300 auto ep = 0;
301 while (ep < text.length && text[ep] > ' ') ++ep;
302 auto res = text[0..ep];
303 text = text[ep..$];
304 return res;
308 if (activeContact !is null) {
309 auto otext = text;
310 if (text[0] == '/' && !text.startsWith("/me ") && !text.startsWith("/me\t")) {
311 text = text[1..$];
312 auto cmd = getWord();
313 if (cmd == "help") {
314 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false,
315 "/accept -- accept friend request\n"~
316 "/kfd -- remove friend, block any further requests\n"~
317 "/status -- set account status"~
318 "/pubkey -- get contact public key"~
319 "", systimeNow);
320 } else if (cmd == "accept") {
321 if (toxCoreAddFriend(activeContact.acc.toxpk, activeContact.info.pubkey)) {
322 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "accepted!", systimeNow);
323 activeContact.kind = ContactInfo.Kind.Friend;
324 activeContact.save();
325 } else {
326 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "ERROR accepting!", systimeNow);
328 } else if (cmd == "kfd") {
329 toxCoreRemoveFriend(activeContact.acc.toxpk, activeContact.info.pubkey);
330 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "KILL! FUCK! DIE!", systimeNow);
331 activeContact.kind = ContactInfo.Kind.KillFuckDie;
332 activeContact.save();
333 } else if (cmd == "remove") {
334 acc.removeContact(activeContact);
335 } else if (cmd == "friend") {
336 if (!isValidAddr(activeContact.info.fraddr)) { conwriteln("invalid address"); return; }
337 string msg = getWord();
338 if (msg.length == 0) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; please, specify message!"); return; }
339 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; message too long"); return; }
340 if (!acc.sendFriendRequest(activeContact.info.fraddr, msg)) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; error sending friend request"); return; }
341 } else if (cmd == "status") {
342 string msg = getWord();
343 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
344 if (!toxCoreSetStatusMessage(activeContact.acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
345 activeContact.acc.info.statusmsg = msg;
346 activeContact.acc.save();
347 try { activeContact.acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
348 } else if (cmd == "pubkey") {
349 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, tox_hex(activeContact.info.pubkey), systimeNow);
350 } else {
351 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "wut?!", systimeNow);
353 } else {
354 activeContact.send(text);
356 } else {
357 // sysedit
358 if (acc !is null && text.length && text[0] == '/') {
359 text = text[1..$];
360 // get command
361 auto cmd = getWord();
362 switch (cmd) {
363 case "friend": // add new friend
364 string addr = getWord();
365 ToxAddr fraddr = decodeAddrStr(addr);
366 if (!isValidAddr(fraddr)) { conwriteln("invalid address: '", addr, "'"); return; }
367 string msg = getWord();
368 if (msg.length == 0) { conwriteln("address: '", tox_hex(fraddr), "'; please, specify message!"); return; }
369 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", addr, "'; message too long"); return; }
370 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
371 if (!acc.sendFriendRequest(fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request"); return; }
372 break;
373 case "status":
374 string msg = getWord();
375 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
376 if (!toxCoreSetStatusMessage(acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
377 acc.info.statusmsg = msg;
378 activeContact.acc.save();
379 try { acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
380 break;
381 case "myaddr":
382 currEdit.text = acc.getAddress();
383 break;
384 case "help":
385 addTextToLog(null, null, LogFile.Msg.Kind.Notification, false,
386 "/friend addr msg -- send friend request\n"~
387 "/status -- set account status\n"~
388 "/myaddr -- put my address into the editor"~
389 "", systimeNow);
390 break;
391 default:
392 conwriteln("unknown command: '", cmd, "'");
393 break;
395 } else {
396 if (text.length) conwriteln("NOT A COMMAND: ", text);
399 return;
402 if (currEdit.onKey(event)) return;
405 int msLastPressX = -666, msLastPressY = -666;
406 uint msDoLogButton = 0; // =0: none; 1: left; 2: middle; 3: right
408 sdmain.handleMouseEvent = delegate (MouseEvent event) {
409 if (sdmain.closed) return;
410 scope(exit) glconPostDoConCommands!true();
411 if (isConsoleVisible) return;
412 mouseX = event.x;
413 mouseY = event.y;
415 // check for click in log
416 //FIXME: process it here, not in renderer
417 if (event == "LMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 1; glconPostScreenRepaint(); }
418 if (event == "MMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 2; glconPostScreenRepaint(); }
419 if (event == "RMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 3; glconPostScreenRepaint(); }
421 if (clist !is null) {
422 if (clist.onMouse(event)) return;
425 // log scroll
426 if (layWinHeight > 0) {
427 enum ScrollHeight = 32;
428 if (event == "WheelUp") {
429 layOffset += ScrollHeight;
430 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
431 } else if (event == "WheelDown") {
432 layOffset -= ScrollHeight;
434 if (layOffset < 0) layOffset = 0;
437 // don't spam with repaint events
438 if (event.type != MouseEventType.motion) glconPostScreenRepaint();
441 sdmain.handleCharEvent = delegate (dchar ch) {
442 if (sdmain.closed) return;
443 scope(exit) glconPostDoConCommands!true();
444 if (glconCharEvent(ch)) return;
446 scope(exit) glconPostScreenRepaint();
448 if (currEdit.onChar(ch)) return;
451 // draw main screen
452 sdmain.redrawOpenGlScene = delegate () {
453 glconPostDoConCommands!true();
454 if (sdmain.closed) return;
455 sdmain.setAsCurrentOpenGlContext(); // make this window active
456 scope(exit) sdmain.releaseCurrentOpenGlContext();
458 // draw main screen
459 scope(exit) glconDraw();
461 if (nvg is null) return;
463 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
464 glViewport(0, 0, sdmain.width, sdmain.height);
465 glMatrixMode(GL_MODELVIEW);
466 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
468 glClearColor(0, 0, 0, 0);
469 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
472 nvg.beginFrame(sdmain.width, sdmain.height);
473 scope(exit) nvg.endFrame();
475 if (clist !is null && optCListWidth > 0) {
476 // draw contact list
477 int cx = 1;
478 int cy = 1;
479 int wdt = optCListWidth;
480 int hgt = nvg.height-cy*2;
482 nvg.shapeAntiAlias = true;
483 nvg.nonZeroFill;
484 nvg.strokeWidth = 1;
487 nvg.save();
488 scope(exit) nvg.restore();
490 nvg.newPath();
491 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
493 int w, h;
494 nvg.imageSize(nvgSkullsImg, w, h);
495 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
497 nvg.strokeColor(NVGColor("#f70"));
498 nvg.fill();
499 nvg.stroke();
501 // draw contact list
503 nvg.save();
504 scope(exit) nvg.restore();
505 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
506 clist.drawAt(nvg, cx+3, cy+3, wdt-3*2, hgt-3*2);
508 // draw scrollbar
509 if (clist.sbSize > 0) {
510 nvg.bndScrollBar(cx+3+(wdt-3*2)+3-BND_SCROLLBAR_WIDTH+0.5f, cy+1+0.5f, BND_SCROLLBAR_WIDTH, hgt-2, BND_DEFAULT, clist.sbPosition, clist.sbSize);
515 nvg.save();
516 scope(exit) nvg.restore();
518 // draw chat log
519 cx += wdt+2;
520 wdt = nvg.width-cx-1;
521 auto baphHgt = hgt;
523 // calculate editor dimensions and draw editor
525 nvg.save();
526 scope(exit) nvg.restore();
528 currEdit.setWidth(wdt-3*2);
530 auto edheight = currEdit.calcHeight();
532 int edy = cy+hgt-edheight-3*2;
534 nvg.newPath();
535 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edheight+3*2, 6);
536 nvg.fillColor(NVGColor.black);
537 nvg.strokeColor(NVGColor("#f70"));
538 nvg.fill();
539 nvg.stroke();
541 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edheight+2);
542 currEdit.draw(nvg, cx+3, edy+3);
544 hgt -= edheight+3*2;
547 nvg.newPath();
548 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
549 nvg.fillColor(NVGColor.black);
550 nvg.strokeColor(NVGColor("#f70"));
551 nvg.fill();
552 nvg.stroke();
554 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
555 version(all) {
556 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
557 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
558 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
559 immutable float sz = BaphometDims*scale;
560 nvg.strokeColor(NVGColor("#400"));
561 nvg.fillColor(NVGColor("#400"));
562 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);
565 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
566 immutable float sby = cy+3.5f;
567 wdt -= BND_SCROLLBAR_WIDTH+1;
568 wdt -= 3*2;
569 hgt -= 3*2;
571 lay.relayout(wdt); // this is harmess if width wasn't changed
572 int ty = lay.textHeight-hgt+1-layOffset;
573 if (ty < 0) ty = 0;
574 layWinHeight = cast(int)hgt;
576 if (lay.textHeight > hgt) {
577 float h = lay.textHeight-hgt;
578 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
579 } else {
580 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
583 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
584 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
586 if (msDoLogButton) {
587 uint btn = msDoLogButton;
588 msDoLogButton = 0;
589 msLastPressX -= cx+3;
590 msLastPressY -= cy+3;
591 if (msLastPressX >= 0 && msLastPressY >= 0 && msLastPressX < wdt && msLastPressY < hgt) {
592 auto widx = lay.wordAtXY(msLastPressX, ty+msLastPressY);
593 if (widx >= 0) {
594 auto w = lay.wordByIndex(widx);
595 // lmb
596 if (btn == 1) {
597 if (auto hr = cast(uint)widx in layUrlList) {
598 //conwriteln("URL CLICK: <", hr.url, ">");
599 openUrl(hr.url);
602 // rmb
603 if (btn == 3 && w.objectIdx >= 0) {
604 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(w.objectIdx)) {
605 assert(activeContact !is null);
606 activeContact.removeFromResendQueue(maw.msgid, maw.digest, maw.time);
607 maw.msgid = -1;
608 // yeah, repaint
609 glconPostScreenRepaint();
620 flushGui();
621 sdmain.eventLoop(16000,
622 // pulser: process resend queues here
623 delegate () {
624 if (sdmain.closed || clist is null) return;
625 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
628 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
629 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
630 toxCoreShutdownAll();
631 popupKillAll();
632 flushGui();
633 conProcessQueue(int.max/4);