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.