Monday, November 08, 2010 at 12:04 AM.

system.verbs.builtins.mainResponder.discuss.readThread

on readThread (msgNum, flResponseForm=true, flShowThreadLinks=true, flShowAuthors=true, flShowPostTime=true, flShowThreadInfo=true, flShowReadCount=true, flShowResponseCount=true, flShowEnclosures=true, editPageUrl="", flShowCowSkullLink=false, pta=nil, adrMsgTable=nil, adrData=nil, adrGetBodyCallback=nil, adrGetTimeCallback=nil, fleditableSubject=false, cssPrefix="", threadAliveDays=5, threadActiveDays=1, bgColor="ivory", otherColor="beige", headerColor="black", headerTextColor="ivory", adrTemplate=nil, adrTopicTemplate=nil, defaultMode="day", activeThreadIcon=nil, inactiveThreadIcon=nil, userIcon=nil, adrInDgCallback=nil, flUseMappedMemberKeys=false) {
	<<This is the main bottleneck script for displaying discussion group threads.
		<<10/09/00; 6:24:12 PM by JES
	<<Changes
		<<4/4/02; 1:07:05 AM by JES
			<<New optional parameter, adrInDgCallback, is the address of a callback which determines whether a message should be included in the rendering. If the callback returns true, add the message. If it returns false, don't add the message.
		<<10/17/01; 10:46:43 AM by PBS
			<<Don't use IP address in links to built-in icons.
		<<9/26/01; 6:58:09 PM by PBS
			<<Don't call tcp.myDottedId, call tcp.dns.getMyDottedId since it caches.
		<<11/04/00; 10:08:13 PM by JES
			<<Fixed a bug where the incorrect URL would be generated for a thread's status icon. Fixed a bug where the incorrect icon would be used in the next and previous topic links.
		<<10/13/00; 12:06:11 AM by JES
			<<Added support for topic templates.
	
	bundle { //get page table, discussion group, and message table addresses
		if pta == nil {
			pta = html.getPageTableAddress ()};
		if adrData == nil {
			adrData = mainResponder.discuss.openRoot (pta)};
		if adrMsgTable == nil {
			adrMsgTable = @adrData^.messages.[string.padWithZeros (msgNum, 7)]}};
	
	if string.lower (pta^.method) != "post" { // redirect to the top message in the thread
		if adrMsgTable^.inResponseTo != 0 { // redirect to the topic reader page -- to the message requested.
			<<bundle //increment read stats before redirecting
				<<adrMsgTable^.ctReads++
				<<config.mainResponder.stats.ctDiscussionGroupReads++
			local (nomad = adrMsgTable);
			while (nomad^.inResponseTo > 0) {
				nomad = mainResponder.discuss.getMessageTable (nomad^.inResponseTo, pta, adrData)};
			local (url = pta^.responderAttributes.urls^.discussMsgReader);
			if not (url endsWith '$') {
				url = url + "$"};
			url = url + nomad^.msgNum;
			if url != (pta^.url + "$" + pta^.pathArgs) { // make sure we're not redirecting to ourselves
				try { // in case there are non-numeric pathArgs
					if number (pta^.pathArgs) != number (nomad^.msgNum) {
						url = url + "#" + pta^.pathArgs}};
				local (searchArgs = "");
				if defined (pta^.searchAgrs) and pta^.searchAgrs != "" {
					searchArgs = "?" + pta^.searchArgs};
				mainResponder.redirect (url + searchArgs)}}};
	
	local (membershipGroup = pta^.responderAttributes.defaultMembershipGroup);
	local (flShowEditButtons, flEditButtonsAtTop, flShowMessageHeaders);
	local (htmlText = "", indentLevel = 0);
	local (flMember = defined (pta^.adrMemberInfo));
	local (msgReaderUrl = pta^.responderAttributes.urls^.discussMsgReader);
	if not (msgReaderUrl endsWith "$") {
		msgReaderUrl = msgReaderUrl + "$"};
	local (oldestActiveThreadDate = date (clock.now () - (3600 * 24 * threadActiveDays)));
	
	on buildSearchArgs (adrArgTable) {
		local (ct = sizeOf (adrArgTable^), i, s = "?");
		for i = 1 to ct {
			s = s + nameOf (adrArgTable^[i]) + "=" + adrArgTable^[i] + "&"};
		s = string.mid (s, 1, sizeOf (s) - 1);
		return (s)};
	
	local (contextDate, y, m, d); // for context for next/prev topic
	local (mode, thisModeSearchArgs, otherModeSearchArgs);
	if pta^.searchArgs != "" { // get context information from the searchArgs, and build search args for mode flipping
		local (searchArgs = "", argTable);
		new (tableType, @argTable);
		if defined (pta^.searchArgs) {
			searchArgs = pta^.searchArgs};
		webserver.parseArgs (pta^.searchArgs, @argTable);
		bundle { // make sure year, month and day are defined
			if defined (argTable.y) {
				y = argTable.y}
			else {
				y = date.year ()};
			if defined (argTable.m) {
				m = argTable.m}
			else {
				m = date.month ()};
			if defined (argTable.d) {
				d = argTable.d}
			else {
				d = date.day ()}};
		if not defined (argTable.mode) {
			argTable.mode = defaultMode};
		argTable.mode = string.lower (argTable.mode);
		mode = argTable.mode;
		thisModeSearchArgs = buildSearchArgs (@argTable);
		if argTable.mode == "day" {
			argTable.mode = "topic"}
		else {
			argTable.mode = "day"};
		otherModeSearchArgs = buildSearchArgs (@argTable);
		contextDate = date.set (d, m, y, 0, 0, 0)}
	else { // context is assumed to be today
		y = date.year ();
		m = date.month ();
		d = date.day ();
		contextDate = date.set (d, m, y, 0, 0, 0)};
	local (flCss = (cssPrefix != "")); //if true, emit CSS classes, otherwise don't
	
	on add (s) {
		htmlText = htmlText + s};
	on getThreadLink (msgNum, adrThreadMsgTable, oldestThreadActiveDate) {
		local (lastMsgDate = adrThreadMsgTable^.postTime);
		if sizeOf (adrThreadMsgTable^.responses) {
			local (lastMsgInThread = adrThreadMsgTable^.responses[sizeOf (adrThreadMsgTable^.responses)]);
			local (adrLastMsgInThread = @adrData^.messages.[string.padWithZeros (lastMsgInThread, 7)]);
			lastMsgDate = adrLastMsgInThread^.postTime};
		local (statusIcon, startLinkTag);
		if lastMsgDate < oldestActiveThreadDate { // inactive thread
			statusIcon = inactiveThreadIcon} //"<img src=\"" + inactiveThreadImgUrl + "\" width=\"" + threadStatusIconWidth + "\" height=\"" + threadStatusIconHeight + "\" border=\"0\" alt=\"inactiveThreadIcon\">"
		else { // active thread
			statusIcon = activeThreadIcon}; //"<img src=\"" + activeThreadImgUrl + "\" width=\"" + threadStatusIconWidth + "\" height=\"" + threadStatusIconHeight + "\" border=\"0\" alt=\"activeThreadIcon\">"
		if flCss {
			startLinkTag = "<a href=\"" + msgReaderUrl + msgNum + "?mode=topic&y=" + y + "&m=" + m + "&d=" + d + "\" class=\"" + cssPrefix + "NextThreadLink\">"}
		else {
			startLinkTag = "<a href=\"" + msgReaderUrl + msgNum + "?mode=topic&y=" + y + "&m=" + m + "&d=" + d + "\">"};
		return (startLinkTag + statusIcon + "</a> " + startLinkTag + adrThreadMsgTable^.subject + "</a>")};
	on getResponseForm () {
		if flMember { //only members can add messages
			local (subject = adrMsgTable^.subject);
			
			if not (string.lower (subject) beginswith string.lower (string.trimWhiteSpace (mainResponder.getString ("discuss.rePrefix")))) {
				subject = mainResponder.getString ("discuss.rePrefix") + subject};
			
			local (buttontext, bodyprompt);
			buttontext = mainResponder.getString ("discuss.postResponseButtonText");
			bodyprompt = mainResponder.getString ("discuss.responseTextPrompt");
			local (redirect = pta^.responderAttributes.urls^.discussMsgReader);
			if not (redirect endsWith '$') {
				redirect = redirect + "$"};
			redirect = redirect + msgNum + "#";
			return (mainResponder.discuss.newMessageForm (msgNum, subject, buttontext:buttontext, redirectUrl:redirect, bodyprompt: bodyprompt, pta:pta, fleditableSubject:false))};
		return ("")};
	on getLastPostTime (adrMsgTable) {
		while sizeOf (adrMsgTable^.responses) {
			local (lastMsgNum = adrMsgTable^.responses[sizeOf (adrMsgTable^.responses)]);
			adrMsgTable = mainResponder.discuss.getMessageTable (lastMsgNum, pta, adrData)};
		return (adrMsgTable^.postTime)};
	
	bundle { //was this topic deleted?
		if mainResponder.discuss.isMessageDeleted (msgNum, adrMsgTable, pta) {
			add (mainResponder.getString ("discuss.messageDeleted"));
			return (htmlText)}};
	
	<<bundle //increment read stats on the top message
		<<adrMsgTable^.ctReads++
		<<config.mainResponder.stats.ctDiscussionGroupReads++
	
	local (topicTable);
	new (tableType, @topicTable);
	on addMessageTree (adrMsgTable) { //recursively walk the message tree
		local (flAddMessage = true);
		if adrInDgCallback != nil {
			flAddMessage = adrInDgCallback^ (adrMsgTable)};
		if flAddMessage {
			local (num = string.padWithZeros (adrMsgTable^.msgnum, 7));
			topicTable.[num] = adrMsgTable};
		local (i, responses = adrMsgTable^.responses, ct = sizeOf (responses));
		for i = 1 to ct { //dive into the responses tree
			addMessageTree (mainResponder.discuss.getMessageTable (responses[i], pta, adrData))}};
	addMessageTree (adrMsgTable);
	
	local (userIconUrl, userIconHeight = 16, userIconWidth = 16);
	local (activeThreadImgUrl, inactiveThreadImgUrl, threadStatusIconWidth = 17, threadStatusIconHeight = 14);
	bundle { // build image data for internal link arrows and message status icons
		<<local (myDottedId = tcp.myDottedId ())
		<<local (myDottedId = tcp.dns.getMyDottedId ()) //PBS 09/26/01: faster
		<<local (myPort = user.inetd.config.http.port)
		<<local (portString = "")
		<<if myPort != 80
			<<portString = ":" + myPort
		local (resourcesUrl = "/mainResponderResources/");
		
		activeThreadImgUrl = resourcesUrl + "icons/folder.open2";
		inactiveThreadImgUrl = resourcesUrl + "icons/folder2";
		userIconUrl = resourcesUrl + "icons/user";
		
		if userIcon == nil {
			userIcon = "<img src=\"" + userIconUrl + "\" height=\"" + userIconHeight + "\" width=\"" + userIconWidth + "\" border=\"0\" alt=\"user\">"};
		if activeThreadIcon == nil {
			activeThreadIcon = "<img src=\"" + activeThreadImgUrl + "\" height=\"" + threadStatusIconHeight + "\" width=\"" + threadStatusIconWidth + "\" border=\"0\" alt=\"activeTopic\">"};
		if inactiveThreadIcon == nil {
			inactiveThreadIcon = "<img src=\"" + inactiveThreadImgUrl + "\" height=\"" + threadStatusIconHeight + "\" width=\"" + threadStatusIconWidth + "\" border=\"0\" alt=\"inactiveTopic\">"}};
	
	local (prevThreadMsgNum = 0, nextThreadMsgNum = 0);
	local (adrPrevThreadMsgTable, adrNextThreadMsgTable);
	local (ct);
	if flShowThreadLinks { // find the previous and next topics
		local (threadData);
		mainResponder.discuss.getThreadData (@threadData, contextDate, threadAliveDays, pta, adrInDgCallback:adrInDgCallback);
		local (ctThreads = sizeOf (threadData));
		for i = 1 to ctThreads {
			if threadData[i].adrThread == adrMsgTable {
				if i > 1 {
					adrPrevThreadMsgTable = threadData[i - 1].adrThread;
					prevThreadMsgNum = adrPrevThreadMsgTable^.msgNum};
				if i < ctThreads {
					adrNextThreadMsgTable = threadData[i + 1].adrThread;
					nextThreadMsgNum = adrNextThreadMsgTable^.msgNum}}}};
	ct = sizeOf (topicTable);
	
	if adrTopicTemplate == nil {
		if flShowThreadInfo {
			add ("<table width=\"100%\" cellpadding=\"3\" cellspacing=\"0\" border=\"0\" bgcolor=\"" + headerColor + "\">");
			
			add ("<tr bgcolor=\"" + headerColor + "\"><td> ");
			if adrMsgTable^.postTime < oldestActiveThreadDate { // inactive thread
				add ("<img src=\"" + inactiveThreadImgUrl + "\" width=\"" + threadStatusIconWidth + "\" height=\"" + threadStatusIconHeight + "\" border=\"0\" alt=\"inactiveThreadIcon\">")}
			else { // active thread
				add ("<img src=\"" + activeThreadImgUrl + "\" width=\"" + threadStatusIconWidth + "\" height=\"" + threadStatusIconHeight + "\" border=\"0\" alt=\"activeThreadIcon\">")};
			
			add ("  <b><font color=\"" + headerTextColor + "\">" + adrMsgTable^.subject + "</font></b></td>");
			
			add ("<td align=\"right\">");
			local (memberName = mainResponder.members.getMemberName (membershipGroup, adrMsgTable^.member));
			if memberName contains "@" { // obscure email addresses
				memberName = string.nthField (memberName, '@', 1) + "@" + string.mid (string.nthField (memberName, '@', 2), 1, 1) + "..."};
			add ("<font color=\"" + headerTextColor + "\" size=\"-1\">started by " + memberName + " on " + mainResponder.localization.dateTimeString (adrMsgTable^.postTime, pta) + "</font>");
			add ("</td></tr>");
			
			add ("</table>")};
		if adrTemplate == nil {
			if flCss { // start the messages table
				add ("<table width=\"100%\" cellpadding=\"0\" cellspacing=\"1\" border=\"0\" bgcolor=\"" + headerColor + "\" class=\"" + cssPrefix + "Table\">")}
			else {
				add ("<table width=\"100%\" cellpadding=\"0\" cellspacing=\"1\" border=\"0\" bgcolor=\"" + headerColor + "\">")}};
		for i = 1 to ct { //render messages chronologically
			if adrInDgCallback != nil {
				if not adrInDgCallback^ (@topicTable[i]) {
					continue}};
			add (mainResponder.discuss.renderOneMessage (topicTable[i], adrTemplate, flShowAuthors, flShowPostTime, flShowReadCount, flShowResponseCount, flShowEnclosures, flResponseForm, flShowCowSkullLink, editPageUrl, adrData, bgColor, otherColor, headerColor, headerTextColor, adrGetBodyCallback, adrGetTimeCallback, cssPrefix, y, m, d, "topic", pta:pta, adrInDgCallback:adrInDgCallback, flUseMappedMemberKeys:flUseMappedMemberKeys))};
		if adrTemplate == nil {
			add ("</table>")};
		if flResponseForm { //provide a response form
			add (getResponseForm ())}}
	else { // flow the topic through the topicTemplate
		local (s = string (adrTopicTemplate^), lowerTemplate = string.lower (s));
		local (lastPostTime = getLastPostTime (adrMsgTable));
		
		bundle { // replace bgcolor, headerColor and headerTextColor
			s = string.replaceAll (s, "{bgcolor}", bgcolor, false);
			s = string.replaceAll (s, "{headerColor}", headerColor, false);
			s = string.replaceAll (s, "{headerTextColor}", headerTextColor, false)};
		bundle { // replace previousTopicLink
			if flShowThreadLinks {
				if prevThreadMsgNum {
					local (threadLink = getThreadLink (prevThreadMsgNum, adrPrevThreadMsgTable, oldestActiveThreadDate));
					s = string.replaceAll (s, "{previousTopicLink}", threadLink, false)}
				else {
					s = string.replaceAll (s, "{previousTopicLink}", "", false)}}
			else {
				s = string.replaceAll (s, "{previousTopicLink}", "", false)}};
		bundle { // replace nextTopicLink
			if flShowThreadLinks {
				if nextThreadMsgNum {
					local (threadLink = getThreadLink (nextThreadMsgNum, adrNextThreadMsgTable, oldestActiveThreadDate));
					s = string.replaceAll (s, "{nextTopicLink}", threadLink, false)}
				else {
					s = string.replaceAll (s, "{nextTopicLink}", "", false)}}
			else {
				s = string.replaceAll (s, "{nextTopicLink}", "", false)}};
		bundle { // replace statusIcon
			if lastPostTime < oldestActiveThreadDate { // inactive thread
				s = string.replaceAll (s, "{statusIcon}", inactiveThreadIcon, false)}
			else { // active thread
				s = string.replaceAll (s, "{statusIcon}", activeThreadIcon, false)}};
		bundle { // replace subject
			s = string.replaceAll (s, "{subject}", adrMsgTable^.subject, false)};
		bundle { // replace author
			local (memberName = mainResponder.members.getMemberName (membershipGroup, adrMsgTable^.member));
			if memberName contains "@" { // obscure email addresses
				memberName = string.nthField (memberName, '@', 1) + "@" + string.mid (string.nthField (memberName, '@', 2), 1, 1) + "..."};
			s = string.replaceAll (s, "{author}", memberName, false)};
		bundle { // replace postTime
			s = string.replaceAll (s, "{postTime}", adrMsgTable^.postTime, false)};
		bundle { // replace lastPostTime
			s = string.replaceAll (s, "{lastPostTime}", lastPostTime, false)};
		bundle { // replace responseForm
			if lowerTemplate contains "{responseform}" {
				if flResponseForm {
					s = string.replaceAll (s, "{responseForm}", getResponseForm (), false)}
				else {
					s = string.replaceAll (s, "{responseForm}", "", false)}}};
		bundle { // replace messages
			local (messages);
			for i = 1 to ct { //render messages chronologically
				if adrInDgCallback != nil {
					if not adrInDgCallback^ (@topicTable[i]) {
						continue}};
				messages = messages + mainResponder.discuss.renderOneMessage (topicTable[i], adrTemplate, flShowAuthors, flShowPostTime, flShowReadCount, flShowResponseCount, flShowEnclosures, flResponseForm, flShowCowSkullLink, editPageUrl, adrData, bgColor, otherColor, headerColor, headerTextColor, adrGetBodyCallback, adrGetTimeCallback, cssPrefix, y, m, d, "topic", pta:pta, userIcon:userIcon, adrInDgCallback:adrInDgCallback, flUseMappedMemberKeys:flUseMappedMemberKeys);
				if sizeOf (htmltext) > 102400 { //limit to 100KB
					break}};
			s = string.replaceAll (s, "{messages}", messages, false)};
		
		add (s)};
	return (htmlText)}



This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.