From 0034da77393af203c6dd81ad2b40ff92117ad290 Mon Sep 17 00:00:00 2001
From: rswindell <>
Date: Mon, 5 Dec 2016 12:12:25 +0000
Subject: [PATCH] Introduce a Thread View Mode while reading messages. Use '*'
 to toggle. Any alpha-char command or entering a message number will exit
 thread view mode. If the entire thread fits on the screen, then the arrow
 keys can be used to move about the thread (up, down, left, and right). The
 old thread-ID keys ( and ) still work, but a little differently. And while in
 Thread View  Mode, ENTER  and - keys now move forward and backward by thread.

I meant to do this a long time ago and really should have (all the underlying
thread support was there). It really highlights all the network nodes that
don't support REPLY-IDs. :-(
---
 src/sbbs3/readmsgs.cpp | 192 +++++++++++++++++++++++++++++++++++++----
 src/sbbs3/sbbs.h       |   1 +
 text/menu/msgscan.asc  |  22 ++---
 3 files changed, 189 insertions(+), 26 deletions(-)

diff --git a/src/sbbs3/readmsgs.cpp b/src/sbbs3/readmsgs.cpp
index 1032563717..2e88adc744 100644
--- a/src/sbbs3/readmsgs.cpp
+++ b/src/sbbs3/readmsgs.cpp
@@ -149,9 +149,9 @@ void sbbs_t::msghdr(smbmsg_t* msg)
 	bprintf("%-16.16s %04Xh\r\n","attr"				,msg->hdr.attr);
 	bprintf("%-16.16s %08lXh\r\n","auxattr"			,msg->hdr.auxattr);
 	bprintf("%-16.16s %08lXh\r\n","netattr"			,msg->hdr.netattr);
-	bprintf("%-16.16s %ld\r\n"	 ,"number"			,msg->hdr.number);
 	bprintf("%-16.16s %06lXh\r\n","header offset"	,msg->idx.offset);
 	bprintf("%-16.16s %u\r\n"	 ,"header length"	,msg->hdr.length);
+	bprintf("%-16.16s %ld\r\n"	 ,"number"			,msg->hdr.number);
 
 	/* optional fixed fields */
 	if(msg->hdr.thread_id)
@@ -424,6 +424,68 @@ static int rank_post(const void* a1, const void* a2)
 	return diff;
 }
 
+static int find_post(smb_t* smb, uint32_t msgnum, post_t* post)
+{
+	uint32_t i;
+
+	/* ToDo: optimize search */
+	for(i=0; i<smb->msgs; i++)
+		if(post[i].idx.number == msgnum)
+			return i;
+	return -1;
+}
+
+void sbbs_t::show_thread(uint32_t msgnum, post_t* post, unsigned curmsg, int thread_depth, uint64_t reply_mask)
+{
+	char date[32];
+	smbmsg_t msg;
+
+	int i = find_post(&smb, msgnum, post);
+	if(i < 0)
+		return;
+
+	memset(&msg, 0, sizeof(msg));
+	msg.idx = post[i].idx;
+	if(smb_getmsghdr(&smb, &msg) != SMB_SUCCESS)
+		return;
+	attr(LIGHTGRAY);
+	if(thread_depth) {
+		for(int j=0; j < thread_depth-1; j++)
+			bprintf("  %c", (reply_mask&(1LL<<j)) ? 179 : ' ');
+		if(msg.hdr.thread_next)
+			bprintf("  %c", 195);
+		else
+			bprintf("  %c", 192);
+	}
+	if(i == curmsg)
+		attr(WHITE);
+	bprintf("%u%c "
+		,post[i].num
+//		,msg.hdr.number
+		,i == curmsg ? '>':':');
+	bprintf("%-*.*s %c %s\r\n"
+		,cols-column-13
+		,cols-column-13
+		,msg.hdr.attr&MSG_ANONYMOUS && !sub_op(smb.subnum)
+			? text[Anonymous] : msg.from
+		,msg_listing_flag(smb.subnum, &msg, &post[i])
+		,unixtodstr(&cfg, msg.hdr.when_written.time, date));
+
+	if(thread_depth) {
+		if(msg.hdr.thread_first)
+			reply_mask |= (1LL<<(thread_depth-1));
+		else
+			reply_mask &= ~(1LL<<(thread_depth-1));
+	}
+	if(thread_depth && !msg.hdr.thread_next)
+		reply_mask &= ~(1LL<<(thread_depth-1));
+	if(msg.hdr.thread_first)
+		show_thread(msg.hdr.thread_first, post, curmsg, thread_depth+1, reply_mask);
+	if(msg.hdr.thread_next)
+		show_thread(msg.hdr.thread_next, post, curmsg, thread_depth, reply_mask);
+	smb_freemsgmem(&msg);
+}
+
 /****************************************************************************/
 /* Reads posts on subboard sub. 'mode' determines new-posts only, browse,   */
 /* or continuous read.                                                      */
@@ -448,6 +510,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 	uint32_t u;
 	post_t	*post;
 	smbmsg_t	msg;
+	bool	thread_mode = false;
 
 	cursubnum=subnum;	/* for ARS */
 	if(cfg.scanposts_mod[0] && !scanposts_inside) {
@@ -667,7 +730,17 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 
 		mismatches=0;
 
-		if(domsg && !(sys_status&SS_ABORT)) {
+		if(thread_mode) {
+			long first = smb_first_in_thread(&smb, &msg);
+			if(first < 0) {
+				bputs(text[NoMessagesFound]);
+				break;
+			}
+			bprintf("\1n\1l\1h\1bThread: \1c%.70s\r\n", msg.subj);
+			show_thread(first, post, smb.curmsg);
+			subscan[subnum].last = post[smb.curmsg].idx.number;
+		}
+		else if(domsg && !(sys_status&SS_ABORT)) {
 
 			if(do_find && mode&SCAN_FIND) { 			/* Find text in messages */
 				buf=smb_getmsgtxt(&smb,&msg,GETMSGTXT_ALL);
@@ -801,7 +874,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 			bprintf(text[UnvalidatedWarning],unvalidated+1);
 		bprintf(text[ReadingSub],ugrp,cfg.grp[cfg.sub[subnum]->grp]->sname
 			,usub,cfg.sub[subnum]->sname,smb.curmsg+1,smb.msgs);
-		sprintf(str,"ABCDEFHILMNPQRTUVY?<>[]{}-+()");
+		sprintf(str,"ABCDEFHILMNPQRTUVY?*<>[]{}-+()\x0a\x1d\x1e\06");
 		if(sub_op(subnum))
 			strcat(str,"O");
 		do_find=true;
@@ -812,10 +885,21 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 				break; 
 			}
 			smb.curmsg=(l&~0x80000000L)-1;
-			do_find=false;
+			do_find = false;
+			thread_mode = false;
+			domsg = true;
 			continue; 
 		}
+		if(thread_mode && isalpha(l)) {
+			thread_mode = false;
+			domsg = true;
+		}
 		switch(l) {
+			case '*':
+				thread_mode = !thread_mode;
+				if(!thread_mode)
+					domsg = true;
+				continue;
 			case 'A':   
 			case 'R':   
 				if((char)l==(cfg.sys_misc&SM_RA_EMU ? 'A' : 'R')) { 
@@ -1165,6 +1249,21 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 				break;
 			}
 			case '-':
+				if(thread_mode && msg.hdr.thread_id) {
+					for(i=smb.curmsg-1; i >= 0; i--) {
+						smbmsg_t nextmsg;
+						memset(&nextmsg, 0, sizeof(nextmsg));
+						nextmsg.idx = post[i].idx;
+						if(smb_getmsghdr(&smb, &nextmsg) != SMB_SUCCESS)
+							continue;
+						smb_freemsgmem(&nextmsg);
+						if(nextmsg.hdr.thread_id < msg.hdr.thread_id)
+							break;
+					}
+					if(i >= 0)
+						smb.curmsg = i;
+					break;
+				}
 				if(smb.curmsg>0) smb.curmsg--;
 				do_find=false;
 				break;
@@ -1286,11 +1385,34 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 				}
 				break;
 			case ')':   /* Thread forward */
-				l=msg.hdr.thread_first;
-				if(!l) l=msg.hdr.thread_next;
-				if(!l) {
+			case LF:	/* down-arrow */
+                l=msg.hdr.thread_next;
+                if(!l) l=msg.hdr.thread_first;
+                if(!l) {
 					domsg=0;
-					bputs(text[NoMessagesFound]);
+					outchar('\a');
+					break; 
+				}
+				for(u=0;u<smb.msgs;u++)
+					if(l==post[u].idx.number)
+						break;
+				if(u<smb.msgs) {
+					smb.curmsg=u;
+				} else {
+					domsg=0;
+					if(thread_mode)
+						outchar('\a');
+					else
+						bputs(text[NoMessagesFound]);
+				}
+				do_find=false;
+				break;
+			case CTRL_F:	/* Right-arrow */
+                l=msg.hdr.thread_first;
+                if(!l) l=msg.hdr.thread_next;
+                if(!l) {
+					domsg=0;
+					outchar('\a');
 					break; 
 				}
 				for(u=0;u<smb.msgs;u++)
@@ -1300,27 +1422,51 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 					smb.curmsg=u;
 				else {
 					domsg=0;
-					bputs(text[NoMessagesFound]);
+					outchar('\a');
 				}
 				do_find=false;
 				break;
 			case '(':   /* Thread backwards */
+			case '\x1d':	/* left arrow */
 				if(!msg.hdr.thread_back) {
 					domsg=0;
-					bputs(text[NoMessagesFound]);
+					outchar('\a');
 					break; 
 				}
 				for(u=0;u<smb.msgs;u++)
 					if(msg.hdr.thread_back==post[u].idx.number)
 						break;
-				if(u<smb.msgs)
+				if(u<smb.msgs) {
 					smb.curmsg=u;
-				else {
+				} else {
 					domsg=0;
-					bputs(text[NoMessagesFound]);
+					if(thread_mode)
+						outchar('\a');
+					else
+						bputs(text[NoMessagesFound]);
 				}
 				do_find=false;
 				break;
+			case '\x1e':	/* up arrow */
+				if(!msg.hdr.thread_id) {
+					domsg=0;
+					bputs(text[NoMessagesFound]);
+					break; 
+				}
+				for(i=smb.curmsg-1; i >= 0; i--) {
+					smbmsg_t nextmsg;
+					memset(&nextmsg, 0, sizeof(nextmsg));
+					nextmsg.idx = post[i].idx;
+					if(smb_getmsghdr(&smb, &nextmsg) != SMB_SUCCESS)
+						continue;
+					smb_freemsgmem(&nextmsg);
+					if(nextmsg.hdr.thread_id == msg.hdr.thread_id)
+						break;
+				}
+				if(i<0)
+					break;
+				smb.curmsg = i;
+				break;
 			case '>':   /* Search Title forward */
 				for(u=smb.curmsg+1;u<smb.msgs;u++)
 					if(post[u].idx.subj==msg.idx.subj)
@@ -1397,8 +1543,24 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 				}
 				do_find=false;
 				break;
-			case 0: /* Carriage return - Next Message */
+			case 0: /* Carriage return - Next Message/Thread */
 			case '+':
+				if(thread_mode) {
+					for(u=smb.curmsg+1;u<smb.msgs;u++) {
+						smbmsg_t nextmsg;
+						memset(&nextmsg, 0, sizeof(nextmsg));
+						nextmsg.idx = post[u].idx;
+						if(smb_getmsghdr(&smb, &nextmsg) != SMB_SUCCESS)
+							continue;
+						smb_freemsgmem(&nextmsg);
+						if(nextmsg.hdr.thread_id > msg.hdr.thread_id)
+							break;
+					}
+					if(u >= smb.msgs)
+						done=1;
+					smb.curmsg = u;
+					break;
+				}
 				if(smb.curmsg<smb.msgs-1) smb.curmsg++;
 				else done=1;
 				break;
@@ -1406,7 +1568,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 				menu("msgscan");
 				domsg=0;
 				break;	
-		} 
+		}
 	}
 	if(msg.total_hfields)
 		smb_freemsgmem(&msg);
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index f350b67e0d..1d6528cb9b 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -756,6 +756,7 @@ public:
 	long	listmsgs(uint subnum, long mode, post_t* post, long start, long posts);
 	long	searchposts(uint subnum, post_t* post, long start, long msgs, const char* find);
 	long	showposts_toyou(uint subnum, post_t* post, ulong start, long posts, long mode=0);
+	void	show_thread(uint32_t msgnum, post_t* post, unsigned curmsg, int thread_depth = 0, uint64_t reply_mask = 0);
 	void	msghdr(smbmsg_t* msg);
 	uchar	msg_listing_flag(uint subnum, smbmsg_t*, post_t*);
 
diff --git a/text/menu/msgscan.asc b/text/menu/msgscan.asc
index d2e55eaeac..ac2523cf29 100644
--- a/text/menu/msgscan.asc
+++ b/text/menu/msgscan.asc
@@ -3,17 +3,17 @@
 c4�k������������������������0
 b������4 hwRead Messages nb��������4 hwPost/Reply Messages nb������4 hwMessage Threading nb����
 �������
-� hy<CR> ngNext message�b� hyP ngPost a message�b� hy< > ng-/+ by Subjects�b�
-�    hy- ngPrevious message   b� hyD ngDelete msg/close poll b� hy{ } ng-/+ by Author�b�
-�    hyC ngContiunous read    b��������������������������� hy[ ] ng-/+ by 'To User'    b�
-�    hy@MSGREREAD@ ngReread last msg    b� hy@MSGREPLY@ ngReply to last message b� hy( ) ng-/+ by Thread-ID    b�
-�    hy# ngGo to message #    b� hyM ngReply in mail to last b���
-�                         b� hyV ngVote on last msg/poll 4 hw      Other Commands nb�����
-������4 hwList Messagesnb�4      ��0                         �
-�                         40�������4 hwSearch/Find nb������� hyB ngBypass current sub    b�
-� hyL ngList all messages     b��� hyI ngInformation on sub    b�
-� hyT ngNext ten messages     b� hyY ngMessages to you�b� hyE ngEdit last message     b�
-� hyN ngNew messages           b� hyU ngYour un-read messages b���������������������������
+� hy<CR> ngNext msg/thread     b� hyP ngPost a message�b� hy< > ng-/+ by Subjects�b�
+�   hy- ngPrevious msg/thread b� hyD ngDelete msg/close poll b� hy{ } ng-/+ by Author�b�
+�   hyC ngContinuous read     b��������������������������� hy[ ] ng-/+ by 'To User'    b�
+�   hy@MSGREREAD@ ngReread last msg     b� hy@MSGREPLY@ ngReply to last message b� hy( ) ng-/+ by Thread-ID    b�
+�   hy# ngGo to message #     b� hyM ngReply in mail to last b�   hy*  ngToggle Thread View  b�
+�                         b� hyV ngVote on last msg/poll b�                          b�
+������4 hwList Messagesnb�4      �4 hw      Other Commands nb�����
+�                         40�������4 hwSearch/Find nb�������                          b�
+� hyL ngList all messages     b��� hyB ngBypass current sub    b�
+� hyT ngNext ten messages     b� hyY ngMessages to you�b� hyI ngInformation on sub    b�
+� hyN ngNew messages           b� hyU ngYour un-read messages b� hyE ngEdit last message     b�
 � hyH ngHighest ranked msgs   b� hyF ngFind text in messages b� hyQ ngQuit to Main menu     b�
 b�������
 4  hyAnytime: cCtrl-U nc4Who's online  hCtrl-P nc4Send private msg  hCtrl-C nc4Abort cmd/text 0
-- 
GitLab