"C-Period" to find my messages; "C-Insert" to copy post URL to clipboard
[knntp.git] / group.d
blob7255c8f75f3bf080f74cc002c501babe48e73b58
1 /* DigitalMars NNTP reader
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module group is aliced;
20 import core.time;
22 import iv.cmdcon;
23 import iv.strex;
24 import iv.vfs;
26 import nntp;
27 import twitlist;
28 import egfx;
29 import egui;
32 // ////////////////////////////////////////////////////////////////////////// //
33 public class UpdatingGroupEvent { uint gidx; this (uint aidx) { gidx = aidx; } }
34 public class UpdatingGroupCompleteEvent { uint gidx; this (uint aidx) { gidx = aidx; } }
35 public class UpdatingCompleteEvent {}
36 public class EvGroupAdded { Group g; this (Group ag) { g = ag; } }
39 // ////////////////////////////////////////////////////////////////////////// //
40 public final class Group {
41 private:
42 private ArticleBase mbase;
44 // add various cached things here
45 bool mActive;
46 uint[] mList; // visual list
47 string[] mTwited;
48 uint mCuridx = uint.max; // current message index (in list)
49 uint mMsgtop; // top visible message (index in list)
50 int mYtopofs; // number of pixels to skip at top item
52 uint mUnreadCount; // calculated, cached
54 //bool updating; // currently updating?
55 MonoTime lastUpdateTime = MonoTime.zero;
57 public:
58 bool uiFlagUpdating;
60 public:
61 @property bool active () const pure nothrow @safe @nogc { return mActive; }
62 @property void active (bool v) nothrow {
63 if (v != mActive) {
64 if (mActive && mCuridx < mList.length) mbase[mList[mCuridx]].clearContent();
65 mActive = v;
69 @property uint curidx () const pure nothrow @safe @nogc { return mCuridx; }
70 @property void curidx (uint v) nothrow {
71 if (v < mList.length && v != mCuridx) {
72 if (mCuridx < mList.length) mbase[mList[mCuridx]].clearContent();
73 mCuridx = v;
76 @property uint msgtop () const pure nothrow @safe @nogc { return mMsgtop; }
77 @property int ytopofs () const pure nothrow @safe @nogc { return mYtopofs; }
78 @property uint unreadCount () const pure nothrow @safe @nogc { return mUnreadCount; }
80 @property bool curidxValid () const pure nothrow @safe @nogc { return (mCuridx < mList.length); }
82 @property uint baseidx (int listidx) const pure nothrow @safe @nogc { return (listidx >= 0 && listidx < mList.length ? mList[listidx] : uint.max); }
83 @property uint length () const pure nothrow @safe @nogc { return cast(uint)mList.length; }
85 @property string groupname () const pure nothrow @safe @nogc { return mbase.groupname; }
87 @property string twited (int listidx) const pure nothrow @safe @nogc { return (listidx >= 0 && listidx < mTwited.length ? mTwited[listidx] : null); }
89 public:
90 void moveToNextThread () {
91 synchronized(mbase) {
92 if (mCuridx >= mList.length) {
93 mCuridx = (mList.length ? cast(uint)(mList.length-1) : 0);
94 } else {
95 if (mbase[mList[mCuridx]].depth == 0) ++mCuridx;
96 while (mCuridx < mList.length && mbase[mList[mCuridx]].depth > 0) ++mCuridx;
97 if (mCuridx >= mList.length) mCuridx = cast(uint)(mList.length-1);
99 makeCurrentVisible(true);
103 void forceUpdating () {
104 lastUpdateTime = MonoTime.zero;
107 void forceUpdated () {
108 lastUpdateTime = MonoTime.currTime;
111 bool needUpdate () {
112 auto ctt = MonoTime.currTime;
113 if ((ctt-lastUpdateTime).total!"minutes" < 9) return false;
114 return true;
117 bool doUpdate () {
118 if (!needUpdate) return false;
119 lastUpdateTime = MonoTime.currTime;
121 //static import iv.vfs.io;
123 conwriteln("updating '", mbase.groupname, "'");
124 SocketNNTP sk;
125 try {
126 sk = new SocketNNTP("news.digitalmars.com");
127 } catch (Exception e) {
128 conwriteln("connection error: ", e.msg);
129 return false;
131 scope(exit) {
132 if (sk.active) sk.quit();
133 sk.close();
136 sk.selectGroup(mbase.groupname);
138 if (sk.emptyGroup) return false;
140 uint stnum = mbase.maxnum+1;
142 if (mbase.length == 0) stnum = (sk.hiwater > 1023 ? sk.hiwater-1023 : 0);
143 if (stnum > sk.hiwater) { conwriteln("no new articles"); return false; }
145 conwriteln(sk.hiwater+1-stnum, " new articles");
147 // download new articles
148 //bool wantNewLine = false;
149 foreach (immutable uint anum; stnum..sk.hiwater+1) {
150 import std.conv : to;
151 //iv.vfs.io.write("\r[", anum, "/", sk.hiwater, "] ... \x1b[K");
152 //wantNewLine = true;
153 auto art = sk.getArticle(anum);
154 if (!art.valid) {
155 //iv.vfs.io.writeln("SKIP");
156 //wantNewLine = false;
157 conwriteln(" SKIP: ", anum);
158 continue;
160 synchronized(mbase) {
161 if (twits.check(art) is null) {
162 //iv.vfs.io.write("OK");
163 art.flags |= Article.Flag.Unread;
164 //wantNewLine = true;
165 } else {
166 //iv.vfs.io.write("IGNORE");
167 //iv.vfs.io.writeln(" ", anum, ": ", art.from, " -- ", art.subj);
168 //wantNewLine = false;
169 conwriteln(" IGNORE: ", anum, ": ", art.fromname, " <", art.frommail, "> -- ", art.subj);
171 uint nidx = mbase.insert(art);
172 if (nidx != 0) {
173 uint ridx = nidx;
174 while (mbase[ridx].parent != 0) ridx = mbase[ridx].parent;
175 if (auto a0 = mbase[ridx]) {
176 if (threadtwits.check(*a0) != ThreadTwitList.Action.None) {
177 if (auto a1 = mbase[nidx]) {
178 a1.unread = false;
179 a1.updated = true;
184 //abase.selfCheck();
187 //if (wantNewLine) iv.vfs.io.writeln;
189 synchronized(mbase) {
190 mbase.selfCheck();
191 mbase.writeUpdates();
193 //buildVisibleList();
195 return true;
198 void withBase (scope void delegate () dg) {
199 synchronized(mbase) dg();
202 void withBase (scope void delegate (ArticleBase abase) dg) {
203 synchronized(mbase) dg(mbase);
206 void deactivate () {
207 releaseContent();
208 mActive = false;
211 void activate () {
212 mActive = true;
215 void releaseContent () {
216 synchronized(mbase) {
217 if (mCuridx < mList.length) mbase[mList[mCuridx]].clearContent();
221 void buildVisibleList () {
222 import core.time;
223 import std.datetime;
224 synchronized(mbase) {
225 uint curi = (mCuridx < mList.length ? mbase[mList[mCuridx]].number : uint.max);
226 uint topi = (mMsgtop < mList.length ? mbase[mList[mMsgtop]].number : uint.max);
228 mMsgtop = 0;
229 mCuridx = uint.max;
230 mUnreadCount = 0;
232 mList.length = 0;
233 mList.assumeSafeAppend;
234 mList.reserve(mbase.length);
236 mTwited.length = 0;
237 mTwited.assumeSafeAppend;
238 mTwited.reserve(mList.length);
240 long oldtime = (Clock.currTime-(3*30).days).toUnixTime; // don't show threads more than ~3 month old
242 bool hasUntwited (uint idx) {
243 while (idx != 0) {
244 auto art = mbase[idx];
245 if (twits.check(*art) is null) return true;
246 if (hasUntwited(art.firstchild)) return true;
247 idx = art.nextsib;
249 return false;
252 bool veryOldThread (uint idx) {
253 while (idx != 0) {
254 auto art = mbase[idx];
255 if (art.time >= oldtime) return false;
256 if (!veryOldThread(art.firstchild)) return false;
257 idx = art.nextsib;
259 return true;
262 // index in base.alist
263 void addArticle (uint idx) {
264 while (idx != 0) {
265 auto art = mbase[idx];
266 art.clearContent();
267 if (art.parent == 0) {
268 if (threadtwits.check(*art) == ThreadTwitList.Action.Hide) {
269 idx = art.nextsib;
270 continue;
272 // don't show too old threads
273 if (art.time < oldtime && veryOldThread(art.firstchild)) {
274 idx = art.nextsib;
275 continue;
278 if (auto tw = twits.check(*art)) {
279 if (tw.hardIgnoreThread && art.parent == 0) {
280 //conwriteln("HARD IGNORE: ", art.from, " -- ", art.subj, " [", tw.toString, "]");
281 // ignore whole thread
282 idx = art.nextsib;
283 continue;
285 // if thread contains only twitted idiots, don't show it
286 if (!hasUntwited(art.firstchild)) {
287 //conwriteln("IGNORE IDIOTS: ", art.from, " -- ", art.subj, " [", tw.toString, "]");
288 idx = art.nextsib;
289 continue;
291 mTwited ~= (tw.title.length ? tw.title : "idiot");
293 if (art.number == topi) mMsgtop = cast(uint)mList.length;
294 if (art.number == curi) mCuridx = cast(uint)mList.length;
295 if (art.unread) ++mUnreadCount;
296 mList ~= idx;
297 if (mTwited.length != mList.length) mTwited ~= null;
298 addArticle(art.firstchild);
299 idx = art.nextsib;
303 addArticle(mbase.first);
305 if (mCuridx == uint.max && mList.length && !mbase[mList[0]].unread) mCuridx = 0;
306 makeCurrentVisible();
308 conwriteln(mbase.groupname, ": ", mbase.length, " in base; ", mList.length, " visible; ", mUnreadCount, " unread");
312 void makeCurrentVisible (bool center=false) {
313 if (mList.length == 0) {
314 mMsgtop = 0;
315 mCuridx = uint.max;
316 mYtopofs = 0;
317 return;
320 if (mCuridx == uint.max) return; // nothing to do
322 // below?
323 int fvis = mMsgtop+(mYtopofs ? 1 : 0);
324 int lvis = fvis+guiThreadListHeight/gxTextHeightUtf-1+(mYtopofs ? 1 : 0);
325 if (lvis < fvis) lvis = fvis;
326 if (mCuridx < fvis) {
327 mMsgtop = mCuridx;
328 mYtopofs = 0;
329 } else if (mCuridx > lvis || (center && mCuridx > lvis-3)) {
330 lvis -= fvis;
331 if (center) lvis /= 2;
332 if (lvis < 1) lvis = 1;
333 mMsgtop = (mCuridx > lvis ? mCuridx-lvis : 0);
334 mYtopofs = 0;
338 // true: something was done
339 bool moveUp () {
340 if (mList.length == 0) return false;
341 if (mCuridx == uint.max) {
342 mCuridx = 0;
343 } else {
344 if (mList.length == 1) {
345 if (mCuridx == 0) return false;
346 releaseContent();
347 mCuridx = 0;
348 } else {
349 if (mCuridx == 0) return false;
350 releaseContent();
351 --mCuridx;
354 makeCurrentVisible();
355 return true;
358 // true: something was done
359 bool moveDown () {
360 if (mList.length == 0) return false;
361 if (mCuridx == uint.max) {
362 mCuridx = 0;
363 } else {
364 if (mList.length == 1) {
365 if (mCuridx == 0) return false;
366 releaseContent();
367 mCuridx = 0;
368 } else {
369 if (mList.length-mCuridx <= 1) return false;
370 releaseContent();
371 ++mCuridx;
374 makeCurrentVisible();
375 return true;
378 // true: something was done
379 bool movePageUp () {
380 makeCurrentVisible();
381 if (mList.length == 0) return false;
382 if (mCuridx == uint.max) {
383 mCuridx = 0;
384 } else {
385 if (mCuridx > mMsgtop) {
386 mCuridx = mMsgtop;
387 mYtopofs = 0;
388 } else {
389 int pgsize = guiThreadListHeight/gxTextHeightUtf-1;
390 if (pgsize < 1) pgsize = 1;
391 if (mCuridx > pgsize) mCuridx -= pgsize; else mCuridx = 0;
394 makeCurrentVisible();
395 return true;
398 // true: something was done
399 bool movePageDown () {
400 makeCurrentVisible();
401 if (mList.length == 0) return false;
402 if (mCuridx == uint.max) {
403 mCuridx = 0;
404 } else {
405 int pgsize = guiThreadListHeight/gxTextHeightUtf-1;
406 if (mCuridx < mMsgtop+pgsize) {
407 mCuridx = mMsgtop+pgsize;
408 } else {
409 mCuridx += pgsize;
411 if (mCuridx >= mList.length) mCuridx = cast(uint)(mList.length-1);
413 makeCurrentVisible();
414 return true;
417 // true: something was done
418 bool scrollUp () {
419 if (mList.length == 0) return false;
420 if (mMsgtop == 0) return false;
421 --mMsgtop;
422 return true;
425 // true: something was done
426 bool scrollDown () {
427 if (mList.length == 0) return false;
428 if (mList.length-mMsgtop < 2) return false;
429 ++mMsgtop;
430 return true;
433 // true: something was done
434 bool moveToFirst () {
435 if (mList.length == 0) return false;
436 if (mCuridx == uint.max) {
437 mCuridx = 0;
438 } else {
439 if (mCuridx == 0) return false;
440 releaseContent();
441 mCuridx = 0;
443 makeCurrentVisible();
444 return true;
447 // true: something was done
448 bool moveToLast () {
449 if (mList.length == 0) return false;
450 if (mCuridx == uint.max) {
451 mCuridx = 0;
452 } else {
453 if (mCuridx == mList.length-1) return false;
454 releaseContent();
455 mCuridx = cast(uint)mList.length-1;
457 makeCurrentVisible();
458 return true;
461 // true: something was done
462 bool moveToNextUnread () {
463 synchronized(mbase) {
464 if (mCuridx >= mList.length || !mbase.unread(mList[mCuridx])) {
465 uint idx = (mCuridx == uint.max ? 0 : mCuridx);
466 while (idx < mList.length) {
467 if (mbase.unread(mList[idx])) break;
468 ++idx;
470 if (idx >= mList.length && mCuridx != uint.max) {
471 idx = 0;
472 while (idx < mCuridx) {
473 if (mbase.unread(mList[idx])) break;
474 ++idx;
476 if (idx == mCuridx) return false;
478 releaseContent();
479 mCuridx = idx;
481 if (mCuridx < mList.length) {
482 auto art = mbase[mList[mCuridx]];
483 if (art.unread) {
484 --mUnreadCount;
485 art.unread = false;
486 art.updated = true;
487 mbase.writeUpdates();
490 makeCurrentVisible(true);
491 return true;
495 // true: something was done
496 bool markAsUnread () {
497 synchronized(mbase) {
498 if (mCuridx < mList.length && !mbase.unread(mList[mCuridx])) {
499 auto art = mbase[mList[mCuridx]];
500 if (!art.unread) {
501 ++mUnreadCount;
502 art.unread = true;
503 art.updated = true;
504 mbase.writeUpdates();
505 return true;
508 return false;
512 // true: something was done
513 bool markAsRead () {
514 synchronized(mbase) {
515 if (mCuridx < mList.length && mbase.unread(mList[mCuridx])) {
516 auto art = mbase[mList[mCuridx]];
517 if (art.unread) {
518 ++mUnreadCount;
519 art.unread = false;
520 art.updated = true;
521 mbase.writeUpdates();
522 return true;
525 return false;
531 // ////////////////////////////////////////////////////////////////////////// //
532 public __gshared Group[] groups;
534 // can return null
535 public Group getActiveGroup () nothrow @trusted @nogc {
536 foreach (Group g; groups) if (g.mActive) return g;
537 return null;
540 public int getActiveGroupIndex () nothrow @trusted @nogc {
541 foreach (immutable idx, Group g; groups) if (g.mActive) return cast(int)idx;
542 return -1;
546 // ////////////////////////////////////////////////////////////////////////// //
547 public struct ArticleId {
548 string group;
549 uint number = uint.max;
551 this() (in Group g, in auto ref Article art) pure nothrow @safe @nogc { set(g, art); }
553 void set() (in Group g, in auto ref Article art) pure nothrow @safe @nogc {
554 if (g is null || !art.valid) {
555 group = null;
556 number = uint.max;
557 } else {
558 group = g.mbase.groupname;
559 number = art.number;
563 @property bool valid () const pure nothrow @safe @nogc { return (number != uint.max && group.length != 0); }
564 @property bool opEquals() (in auto ref ArticleId aid) const pure nothrow @safe @nogc { return (aid.number == number && aid.group == group); }
566 bool equal() (in Group g, in auto ref Article art) const pure nothrow @safe @nogc {
567 if (g is null || !art.valid) {
568 return (number == uint.max || group.length == 0);
569 } else {
570 return (art.number == number && g.mbase.groupname == group);
574 void clear () pure nothrow @safe @nogc { number = uint.max; group = null; }
578 // ////////////////////////////////////////////////////////////////////////// //
579 public __gshared ArticleId lastArticleText;
580 public __gshared int articleTextTopLine;
583 // ////////////////////////////////////////////////////////////////////////// //
584 shared static this () {
585 conRegFunc!((ConString name) {
586 if (name.length == 0) { conwriteln("empty group name?"); return; }
587 foreach (Group g; groups) if (g.groupname == name) { conwriteln("duplicate group: '", name, "'"); return; }
588 try {
589 import std.file : exists;
590 auto b = new ArticleBase(name.idup);
591 if (b.idxfname.exists) b.load();
592 auto g = new Group();
593 g.mbase = b;
594 if (groups.length == 0) g.active = true;
595 groups ~= g;
596 g.buildVisibleList();
597 if (vbwin !is null) vbwin.postEvent(new EvGroupAdded(g));
598 } catch (Exception e) {
599 conwriteln("ERROR adding group '", name, "': ", e.msg);
601 })("group_add", "add newsgroup");