user/dev discussion of public-inbox itself
 help / color / mirror / code / Atom feed
* [PATCH 0/13] www: hybrid flat+thread conversation view
@ 2016-06-30  9:21 Eric Wong
  2016-06-30  9:21 ` [PATCH 01/13] www: implement " Eric Wong
                   ` (12 more replies)
  0 siblings, 13 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

I've been long-dreaming of this and finally it's at least
publishable (I hope :x).  This flat view with thread skeletons
is 100% more usable than the dumb old one, but a little slower
(naturally :<)

I was originally hoping to remove the threaded /t/ endpoint
conversation view entirely to reduce server/caching overheads
but I still find it more usable in some situations.

What I still enjoy is being able to toggle between
[flat|threaded] views.

Eric Wong (13):
      www: implement hybrid flat+thread conversation view
      www: use WwwStream for dumping thread and search views
      view: show thread context in the thread-aware flat view
      view: merge $state hash with existing $ctx
      feed: add $INBOX/new.html endpoint
      view: tweak thread/index header slightly
      view: show more nearby messages in flat thread view
      www: reinstate old thread view as an option
      view: fix up some HTML injection via Message-ID vectors
      view: default to flat/hybrid thread display
      view: show thread size when linking to summary
      view: fixup bad reference to new_msgid
      www_stream: add response wrapper sub

 TODO                          |   2 -
 lib/PublicInbox/Feed.pm       |  51 +++-
 lib/PublicInbox/SearchView.pm | 141 +++++-----
 lib/PublicInbox/View.pm       | 590 +++++++++++++++++++++++-------------------
 lib/PublicInbox/WWW.pm        |  22 +-
 lib/PublicInbox/WwwStream.pm  |  20 +-
 t/view.t                      |   3 +-
 7 files changed, 464 insertions(+), 365 deletions(-)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH 01/13] www: implement hybrid flat+thread conversation view
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 02/13] www: use WwwStream for dumping thread and search views Eric Wong
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This should be more accessible to readers on narrow terminals
(or giant fonts) while providing a chronological view which
is also aware of message threading relationships.
---
 TODO                          |   2 -
 lib/PublicInbox/SearchView.pm |  42 +++----
 lib/PublicInbox/View.pm       | 248 ++++++++++++++++--------------------------
 lib/PublicInbox/WWW.pm        |   9 +-
 t/plack.t                     |   2 +-
 5 files changed, 115 insertions(+), 188 deletions(-)

diff --git a/TODO b/TODO
index f29f2f0..3b6401f 100644
--- a/TODO
+++ b/TODO
@@ -4,8 +4,6 @@ TODO items for public-inbox
 
 * mailmap support (same as git) for remapping expired email addresses
 
-* WWW: Hybrid flat view + thread skeleton (requires Xapian)
-
 * POP3 server, since some webmail providers support external POP3:
   https://public-inbox.org/meta/20160411034104.GA7817@dcvr.yhbt.net/
 
diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm
index ae875bf..fbef411 100644
--- a/lib/PublicInbox/SearchView.pm
+++ b/lib/PublicInbox/SearchView.pm
@@ -163,44 +163,34 @@ sub tdump {
 	} else { # order by time (default for threaded view)
 		$th->order(*PublicInbox::View::sort_ts);
 	}
+	my $skel = '';
 	my $state = {
+		-inbox => $ctx->{-inbox},
+		anchor_idx => 1,
 		ctx => $ctx,
-		anchor_idx => 0,
-		pct => \%pct,
 		cur_level => 0,
-		-inbox => $ctx->{-inbox},
+		dst => \$skel,
 		fh => $fh,
+		mapping => {},
+		pct => \%pct,
+		prev_attr => '',
+		prev_level => 0,
+		seen => {},
+		srch => $ctx->{srch},
+		upfx => './',
 	};
 	$ctx->{searchview} = 1;
-	PublicInbox::View::walk_thread($th, $state, *tdump_ent);
-	PublicInbox::View::thread_adj_level($state, 0);
+	PublicInbox::View::walk_thread($th, $state,
+		*PublicInbox::View::pre_thread);
+
+	PublicInbox::View::thread_entry($state, $_, 0) for @m;
 
-	$fh->write(search_nav_bot($mset, $q). "\n\n" .
+	$fh->write(search_nav_bot($mset, $q). "\n\n" . $skel . "\n" .
 			foot($ctx). '</pre></body></html>');
 
 	$fh->close;
 }
 
-sub tdump_ent {
-	my ($state, $level, $node) = @_;
-	my $mime = $node->message;
-
-	if ($mime) {
-		# lazy load the full message from mini_mime:
-		my $mid = mid_mime($mime);
-		$mime = eval { $state->{-inbox}->msg_by_mid($mid) } and
-			$mime = Email::MIME->new($mime);
-	}
-	if ($mime) {
-		my $end = PublicInbox::View::thread_adj_level($state, $level);
-		PublicInbox::View::index_entry($mime, $level, $state);
-		$state->{fh}->write($end) if $end;
-	} else {
-		my $mid = $node->messageid;
-		PublicInbox::View::ghost_flush($state, '', $mid, $level);
-	}
-}
-
 sub foot {
 	my ($ctx) = @_;
 	my $foot = $ctx->{footer} || '';
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 30339cd..65788db 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -89,77 +89,72 @@ sub _hdr_names ($$) {
 	ascii_html(join(', ', PublicInbox::Address::names($val)));
 }
 
+sub nr_to_s ($$$) {
+	my ($nr, $singular, $plural) = @_;
+	return "0 $plural" if $nr == 0;
+	$nr == 1 ? "$nr $singular" : "$nr $plural";
+}
+
 # this is already inside a <pre>
 sub index_entry {
 	my ($mime, $level, $state) = @_;
-	my $midx = $state->{anchor_idx}++;
+	$state->{anchor_idx}++;
 	my $ctx = $state->{ctx};
 	my $srch = $ctx->{srch};
 	my $hdr = $mime->header_obj;
 	my $subj = $hdr->header('Subject');
 
 	my $mid_raw = mid_clean(mid_mime($mime));
-	my $id = anchor_for($mid_raw);
-	my $seen = $state->{seen};
-	$seen->{$id} = "#$id"; # save the anchor for children, later
-
+	my $id = id_compress($mid_raw);
+	my $id_m = 'm'.$id;
 	my $mid = PublicInbox::Hval->new_msgid($mid_raw);
 
 	my $root_anchor = $state->{root_anchor} || '';
 	my $path = $root_anchor ? '../../' : '';
 	my $href = $mid->as_href;
 	my $irt = in_reply_to($hdr);
-	my $parent_anchor = $seen->{anchor_for($irt)} if defined $irt;
 
-	$subj = ascii_html($subj);
-	$subj = "<a\nhref=\"${path}$href/\">$subj</a>";
-	$subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id;
+	$subj = '<b>'.ascii_html($subj).'</b>';
+	$subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id_m;
 
 	my $ts = _msg_date($hdr);
-	my $rv = "<pre\nid=s$midx>";
-	$rv .= "<b\nid=$id>$subj</b>\n";
-	my $txt = "${path}$href/raw";
-	my $fh = $state->{fh};
+	my $rv = "<pre><a\nhref=#e$id\nid=$id_m>#</a> ";
+	$rv .= $subj;
+	my $mhref = $path.$href.'/';
 	my $from = _hdr_names($hdr, 'From');
-	$rv .= "- $from @ $ts UTC (<a\nhref=\"$txt\">raw</a>)\n";
+	$rv .= "\n- $from @ $ts UTC\n";
 	my @tocc;
 	foreach my $f (qw(To Cc)) {
 		my $dst = _hdr_names($hdr, $f);
 		push @tocc, "$f: $dst" if $dst ne '';
 	}
 	$rv .= '  '.join('; +', @tocc) . "\n" if @tocc;
-	$fh->write($rv .= "\n");
-
-	my $mhref = "${path}$href/";
+	$rv .= "\n";
 
 	# scan through all parts, looking for displayable text
-	msg_iter($mime, sub { index_walk($fh, $mhref, $_[0]) });
-	$rv = "\n" . html_footer($hdr, 0, $ctx, "$path$href/#R");
-
+	msg_iter($mime, sub { $rv .= add_text_body($mhref, $_[0]) });
+	$rv .= "\n<a\nhref=\"$mhref\"\n>permalink</a>" .
+		" / <a\nhref=\"${mhref}raw\">raw</a> / ";
+	my $mapping = $state->{mapping};
+	my $nr_c = $mapping->{$mid_raw} || 0;
+	my $nr_s = 0;
 	if (defined $irt) {
-		unless (defined $parent_anchor) {
-			my $v = PublicInbox::Hval->new_msgid($irt, 1);
-			$v = $v->as_href;
-			$parent_anchor = "${path}$v/";
-		}
-		$rv .= " <a\nhref=\"$parent_anchor\">parent</a>";
+		$nr_s = ($mapping->{$irt} || 0) - 1;
+		$nr_s = 0 if $nr_s < 0;
+		$irt = anchor_for($irt);
+		$rv .= "<a\nhref=#$irt>#parent</a>,";
+	} else {
+		$rv .= 'root message:';
 	}
+	$nr_s = nr_to_s($nr_s, 'sibling', 'siblings');
+	$nr_c = nr_to_s($nr_c, 'reply', 'replies');
+	$rv .= " <a\nhref=#r$id\nid=e$id>$nr_s, $nr_c</a>";
+	$rv .= " / <a\nhref=\"${mhref}#R\">reply</a>";
+
 	if (my $pct = $state->{pct}) { # used by SearchView.pm
 		$rv .= " [relevance $pct->{$mid_raw}%]";
-	} elsif ($srch) {
-		my $threaded = 'threaded';
-		my $flat = 'flat';
-		my $end = '';
-		if ($ctx->{flat}) {
-			$flat = "<b>$flat</b>";
-			$end = "\n"; # for lynx
-		} else {
-			$threaded = "<b>$threaded</b>";
-		}
-		$rv .= " [<a\nhref=\"${path}$href/t/#u\">$threaded</a>";
-		$rv .= "|<a\nhref=\"${path}$href/T/#u\">$flat</a>]$end";
 	}
-	$fh->write($rv .= '</pre>');
+	$state->{fh}->write($rv .= "\n</pre>"); # '\n' for lynx
 }
 
 sub thread_html {
@@ -179,63 +174,62 @@ sub walk_thread {
 	}
 }
 
+sub pre_thread  {
+	my ($state, $level, $node) = @_;
+	my $parent = $node->parent;
+	if ($parent) {
+		my $mid = $parent->messageid;
+		my $m = $state->{mapping};
+		$m->{$mid} ||= 0;
+		$m->{$mid}++;
+	}
+	skel_dump($state, $level, $node);
+}
+
 # only private functions below.
 
 sub emit_thread_html {
 	my ($res, $ctx, $foot, $srch) = @_;
 	my $mid = $ctx->{mid};
-	my $flat = $ctx->{flat};
-	my $msgs = load_results($srch->get_thread($mid, { asc => $flat }));
-	my $nr = scalar @$msgs;
+	my $sres = $srch->get_thread($mid, { asc => 1 });
+	my $msgs = load_results($sres);
+	my $nr = $sres->{total};
 	return missing_thread($res, $ctx) if $nr == 0;
-	my $seen = {};
+	my $skel = '';
 	my $state = {
-		res => $res,
-		ctx => $ctx,
-		seen => $seen,
-		root_anchor => anchor_for($mid),
 		anchor_idx => 0,
+		ctx => $ctx,
 		cur_level => 0,
+		dst => \$skel,
+		mapping => {}, # mid -> reply count
+		prev_attr => '',
+		prev_level => 0,
+		res => $res,
+		root_anchor => anchor_for($mid),
+		seen => {},
+		srch => $ctx->{srch},
+		upfx => '../../',
 	};
 
-	require PublicInbox::Git;
-	$ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
-	if ($flat) {
-		pre_anchor_entry($seen, $_) for (@$msgs);
-		__thread_entry($state, $_, 0) for (@$msgs);
-	} else {
-		walk_thread(thread_results($msgs), $state, *thread_entry);
-		if (my $max = $state->{cur_level}) {
-			$state->{fh}->write(
-				('</ul></li>' x ($max - 1)) . '</ul>');
-		}
-	}
+	walk_thread(thread_results($msgs), $state, *pre_thread);
+
+	thread_entry($state, $_, 0) for @$msgs;
 
 	# there could be a race due to a message being deleted in git
 	# but still being in the Xapian index:
 	my $fh = delete $state->{fh} or return missing_thread($res, $ctx);
 
-	my $final_anchor = $state->{anchor_idx};
-	my $next = "<a\nid=s$final_anchor>";
-	$next .= $final_anchor == 1 ? 'only message in' : 'end of';
-	$next .= " thread</a>, back to <a\nhref=\"../../\">index</a>";
-	$next .= "\ndownload thread: ";
+	my $next = @$msgs == 1 ? 'only message in thread' : 'end of thread';
+	$next .= ", back to <a\nhref=\"../../\">index</a>";
+	$next .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
 	$next .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
-	$next .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>";
+	$next .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
+	$next .= $skel;
 	$fh->write('<hr /><pre>' . $next . "\n\n".
 			$foot .  '</pre></body></html>');
 	$fh->close;
 }
 
-sub index_walk {
-	my ($fh, $upfx, $p) = @_;
-	my $s = add_text_body($upfx, $p);
-
-	return if $s eq '';
-
-	$fh->write($s);
-}
-
 sub multipart_text_as_html {
 	my ($mime, $upfx) = @_;
 	my $rv = "";
@@ -542,11 +536,7 @@ sub linkify_ref_nosrch {
 
 sub anchor_for {
 	my ($msgid) = @_;
-	my $id = $msgid;
-	if ($id !~ /\A[a-f0-9]{40}\z/) {
-		$id = id_compress(mid_clean($id), 1);
-	}
-	'm' . $id;
+	'm' . id_compress($msgid, 1);
 }
 
 sub thread_html_head {
@@ -563,12 +553,6 @@ sub thread_html_head {
 		"</head><body>");
 }
 
-sub pre_anchor_entry {
-	my ($seen, $mime) = @_;
-	my $id = anchor_for(mid_mime($mime));
-	$seen->{$id} = "#$id"; # save the anchor for children, later
-}
-
 sub ghost_parent {
 	my ($upfx, $mid) = @_;
 	# 'subject dummy' is used internally by Mail::Thread
@@ -580,39 +564,7 @@ sub ghost_parent {
 	qq{[parent not found: &lt;<a\nhref="$upfx$href/">$html</a>&gt;]};
 }
 
-sub thread_adj_level {
-	my ($state, $level) = @_;
-
-	my $max = $state->{cur_level};
-	if ($level <= 0) {
-		return '' if $max == 0; # flat output
-
-		# reset existing lists
-		my $x = $max > 1 ? ('</ul></li>' x ($max - 1)) : '';
-		$state->{fh}->write($x . '</ul>');
-		$state->{cur_level} = 0;
-		return '';
-	}
-	if ($level == $max) { # continue existing list
-		$state->{fh}->write('<li>');
-	} elsif ($level < $max) {
-		my $x = $max > 1 ? ('</ul></li>' x ($max - $level)) : '';
-		$state->{fh}->write($x .= '<li>');
-		$state->{cur_level} = $level;
-	} else { # ($level > $max) # start a new level
-		$state->{cur_level} = $level;
-		$state->{fh}->write(($max ? '<li>' : '') . '<ul><li>');
-	}
-	'</li>';
-}
-
-sub ghost_flush {
-	my ($state, $upfx, $mid, $level) = @_;
-	my $end = '<pre>'. ghost_parent($upfx, $mid) . '</pre>';
-	$state->{fh}->write($end .= thread_adj_level($state, $level));
-}
-
-sub __thread_entry {
+sub thread_entry {
 	my ($state, $mime, $level) = @_;
 
 	# lazy load the full message from mini_mime:
@@ -623,16 +575,7 @@ sub __thread_entry {
 	$mime = Email::MIME->new($mime);
 
 	thread_html_head($mime, $state) if $state->{anchor_idx} == 0;
-	if (my $ghost = delete $state->{ghost}) {
-		# n.b. ghost messages may only be parents, not children
-		foreach my $g (@$ghost) {
-			ghost_flush($state, '../../', @$g);
-		}
-	}
-	my $end = thread_adj_level($state, $level);
 	index_entry($mime, $level, $state);
-	$state->{fh}->write($end) if $end;
-
 	1;
 }
 
@@ -641,23 +584,6 @@ sub indent_for {
 	INDENT x ($level - 1);
 }
 
-sub __ghost_prepare {
-	my ($state, $node, $level) = @_;
-	my $ghost = $state->{ghost} ||= [];
-	push @$ghost, [ $node->messageid, $level ];
-}
-
-sub thread_entry {
-	my ($state, $level, $node) = @_;
-	if (my $mime = $node->message) {
-		unless (__thread_entry($state, $mime, $level)) {
-			__ghost_prepare($state, $node, $level);
-		}
-	} else {
-		__ghost_prepare($state, $node, $level);
-	}
-}
-
 sub load_results {
 	my ($sres) = @_;
 
@@ -738,30 +664,44 @@ sub _skel_header {
 		$s = $s->as_html;
 	}
 	my $m = PublicInbox::Hval->new_msgid($mid);
-	$m = $state->{upfx} . $m->as_href . '/';
-	$$dst .= "$pfx<a\nhref=\"$m\">";
+	my $id = '';
+	if ($state->{mapping}) {
+		$id = id_compress($mid, 1);
+		$m = '#m'.$id;
+		$id = "\nid=r".$id;
+	} else {
+		$m = $state->{upfx}.$m->as_href.'/';
+	}
+	$$dst .= "$pfx<a\nhref=\"$m\"$id>";
 	$$dst .= defined($s) ? "$s</a> $f\n" : "$f</a>\n";
 }
 
 sub skel_dump {
 	my ($state, $level, $node) = @_;
 	if (my $mime = $node->message) {
-		my $hdr = $mime->header_obj;
-		my $mid = mid_clean($hdr->header_raw('Message-ID'));
-		_skel_header($state, $hdr, $level);
+		_skel_header($state, $mime->header_obj, $level);
 	} else {
 		my $mid = $node->messageid;
 		my $dst = $state->{dst};
 		if ($mid eq 'subject dummy') {
 			$$dst .= "\t[no common parent]\n";
+			return;
+		}
+		if ($state->{pct}) { # search result
+			$$dst .= '    [irrelevant] ';
 		} else {
 			$$dst .= '     [not found] ';
-			$$dst .= indent_for($level) . th_pfx($level);
-			$mid = PublicInbox::Hval->new_msgid($mid);
-			my $href = $state->{upfx} . $mid->as_href . '/';
-			my $html = $mid->as_html;
-			$$dst .= qq{&lt;<a\nhref="$href">$html</a>&gt;\n};
 		}
+		$$dst .= indent_for($level) . th_pfx($level);
+		my $upfx = $state->{upfx};
+		my $id = '';
+		if ($state->{mapping}) { # thread index view
+			$id = "\nid=".anchor_for($mid);
+		}
+		$mid = PublicInbox::Hval->new_msgid($mid);
+		my $href = $upfx . $mid->as_href . '/';
+		my $html = $mid->as_html;
+		$$dst .= qq{&lt;<a\nhref="$href"$id>$html</a>&gt;\n};
 	}
 }
 
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index d6b07bf..984268e 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -23,7 +23,7 @@ require PublicInbox::Git;
 use PublicInbox::GitHTTPBackend;
 our $INBOX_RE = qr!\A/([\w\.\-]+)!;
 our $MID_RE = qr!([^/]+)!;
-our $END_RE = qr!(T/|t/|t\.mbox(?:\.gz)?|t\.atom|raw|)!;
+our $END_RE = qr!(t/|t\.mbox(?:\.gz)?|t\.atom|raw|)!;
 our $ATTACH_RE = qr!(\d[\.\d]*)-([[:alnum:]][\w\.-]+[[:alnum:]])!i;
 
 sub new {
@@ -91,10 +91,9 @@ sub call {
 		invalid_inbox_mid($self, $ctx, $1, $2) ||
 			get_attach($ctx, $idx, $fn);
 	# in case people leave off the trailing slash:
-	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/(T|t)\z!o) {
-		my ($inbox, $mid, $suffix) = ($1, $2, $3);
-		$suffix .= $suffix =~ /\A[tT]\z/ ? '/#u' : '/';
-		r301($ctx, $inbox, $mid, $suffix);
+	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/(?:T|T/|t)\z!o) {
+		my ($inbox, $mid) = ($1, $2);
+		r301($ctx, $inbox, $mid, 't/#u');
 
 	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/R/?\z!o) {
 		my ($inbox, $mid) = ($1, $2);
diff --git a/t/plack.t b/t/plack.t
index a4f3245..209c6f9 100644
--- a/t/plack.t
+++ b/t/plack.t
@@ -101,7 +101,7 @@ EOF
 			my $res = $cb->(GET($u));
 			is(301, $res->code, "redirect for missing /");
 			my $location = $res->header('Location');
-			like($location, qr!/\Q$t\E/#u\z!,
+			like($location, qr!/t/#u\z!,
 				'redirected with missing /');
 		});
 	}
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 02/13] www: use WwwStream for dumping thread and search views
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
  2016-06-30  9:21 ` [PATCH 01/13] www: implement " Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 03/13] view: show thread context in the thread-aware flat view Eric Wong
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This allows us the HTTP server to react to backpressure
from slow clients when writing.  As a side effect, this
also makes it easier for us to maintain a consistent
header/footer across our HTML.
---
 lib/PublicInbox/Feed.pm       |   3 +-
 lib/PublicInbox/SearchView.pm | 106 +++++++++++++++++++++---------------------
 lib/PublicInbox/View.pm       | 103 +++++++++++++++-------------------------
 lib/PublicInbox/WWW.pm        |   9 ++--
 lib/PublicInbox/WwwStream.pm  |  14 +++++-
 5 files changed, 108 insertions(+), 127 deletions(-)

diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index 8e23306..36802fa 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -168,12 +168,13 @@ sub emit_html_index {
 sub emit_index_nosrch {
 	my ($ctx, $state) = @_;
 	my $ibx = $ctx->{-inbox};
+	my $fh = $state->{fh};
 	my (undef, $last) = each_recent_blob($ctx, sub {
 		my ($path, $commit, $ts, $u, $subj) = @_;
 		$state->{first} ||= $commit;
 
 		my $mime = do_cat_mail($ibx, $path) or return 0;
-		PublicInbox::View::index_entry($mime, 0, $state);
+		$fh->write(PublicInbox::View::index_entry($mime, $state));
 		1;
 	});
 	$last;
diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm
index fbef411..488822e 100644
--- a/lib/PublicInbox/SearchView.pm
+++ b/lib/PublicInbox/SearchView.pm
@@ -8,12 +8,14 @@ use warnings;
 use PublicInbox::SearchMsg;
 use PublicInbox::Hval qw/ascii_html/;
 use PublicInbox::View;
-use PublicInbox::MID qw(mid2path mid_mime);
+use PublicInbox::MID qw(mid2path mid_mime mid_clean);
 use Email::MIME;
 require PublicInbox::Git;
 require PublicInbox::Thread;
 our $LIM = 50;
 
+sub noop {}
+
 sub sres_top_html {
 	my ($ctx) = @_;
 	my $q = PublicInbox::SearchQuery->new($ctx->{qp});
@@ -27,44 +29,46 @@ sub sres_top_html {
 		relevance => $q->{r},
 	};
 	my ($mset, $total);
-
 	eval {
-		$mset = $ctx->{srch}->query($q->{q}, $opts);
+		$mset = $ctx->{srch}->query($q->{'q'}, $opts);
 		$total = $mset->get_matches_estimated;
 	};
 	my $err = $@;
-	my $res = html_start($q, $ctx) . '<pre>';
+	ctx_prepare($q, $ctx);
+	my $cb;
 	if ($err) {
 		$code = 400;
-		$res .= err_txt($ctx, $err) . "</pre><hr /><pre>" . foot($ctx);
+		$ctx->{-html_tip} = '<pre>'.err_txt($ctx, $err).'</pre><hr />';
+		$cb = *noop;
 	} elsif ($total == 0) {
 		$code = 404;
-		$res .= "\n\n[No results found]</pre><hr /><pre>".foot($ctx);
+		$ctx->{-html_tip} = "<pre>\n[No results found]</pre><hr />";
+		$cb = *noop;
 	} else {
 		my $x = $q->{x};
 		return sub { adump($_[0], $mset, $q, $ctx) } if ($x eq 'A');
 
-		$res .= search_nav_top($mset, $q) . "\n\n";
+		$ctx->{-html_tip} = search_nav_top($mset, $q) . "\n\n";
 		if ($x eq 't') {
-			return sub { tdump($_[0], $res, $mset, $q, $ctx) };
+			$cb = mset_thread($ctx, $mset, $q);
+		} else {
+			$cb = mset_summary($ctx, $mset, $q);
 		}
-		dump_mset(\$res, $mset);
-		$res .= '</pre>' . search_nav_bot($mset, $q) .
-			"\n\n" . foot($ctx);
 	}
 
-	$res .= "</pre></body></html>";
-	[$code, ['Content-Type'=>'text/html; charset=UTF-8'], [$res]];
+	[ $code, ['Content-Type', 'text/html; charset=UTF-8'],
+		PublicInbox::WwwStream->new($ctx, $cb) ];
 }
 
 # display non-threaded search results similar to what users expect from
 # regular WWW search engines:
-sub dump_mset {
-	my ($res, $mset) = @_;
+sub mset_summary {
+	my ($ctx, $mset, $q) = @_;
 
 	my $total = $mset->get_matches_estimated;
 	my $pad = length("$total");
 	my $pfx = ' ' x $pad;
+	my $res = \($ctx->{-html_tip});
 	foreach my $m ($mset->items) {
 		my $rank = sprintf("%${pad}d", $m->get_rank + 1);
 		my $pct = $m->get_percent;
@@ -77,6 +81,8 @@ sub dump_mset {
 			$s . "</a></b>\n";
 		$$res .= "$pfx  - by $f @ $ts UTC [$pct%]\n\n";
 	}
+	$$res .= search_nav_bot($mset, $q);
+	*noop;
 }
 
 sub err_txt {
@@ -85,14 +91,14 @@ sub err_txt {
 	$u = PublicInbox::Hval::prurl($ctx->{cgi}->{env}, $u);
 	$err =~ s/^\s*Exception:\s*//; # bad word to show users :P
 	$err = ascii_html($err);
-	"\n\nBad query: <b>$err</b>\n" .
+	"\nBad query: <b>$err</b>\n" .
 		qq{See <a\nhref="$u">$u</a> for Xapian query syntax};
 }
 
 sub search_nav_top {
 	my ($mset, $q) = @_;
 
-	my $rv = "Search results ordered by [";
+	my $rv = "<pre>Search results ordered by [";
 	if ($q->{r}) {
 		my $d = $q->qs_html(r => 0);
 		$rv .= qq{<a\nhref="?$d">date</a>|<b>relevance</b>};
@@ -122,7 +128,7 @@ sub search_nav_bot {
 	my $o = $q->{o};
 	my $end = $o + $nr;
 	my $beg = $o + 1;
-	my $rv = "<hr /><pre>Results $beg-$end of $total";
+	my $rv = "</pre><hr /><pre>Results $beg-$end of $total";
 	my $n = $o + $LIM;
 
 	if ($n < $total) {
@@ -135,13 +141,11 @@ sub search_nav_bot {
 		my $qs = $q->qs_html(o => ($p > 0 ? $p : 0));
 		$rv .= qq{<a\nhref="?$qs"\nrel=prev>prev</a>};
 	}
-	$rv;
+	$rv .= '</pre>';
 }
 
-sub tdump {
-	my ($cb, $res, $mset, $q, $ctx) = @_;
-	my $fh = $cb->([200, ['Content-Type'=>'text/html; charset=UTF-8']]);
-	$fh->write($res .= '</pre>');
+sub mset_thread {
+	my ($ctx, $mset, $q) = @_;
 	my %pct;
 	my @m = map {
 		my $i = $_;
@@ -163,14 +167,14 @@ sub tdump {
 	} else { # order by time (default for threaded view)
 		$th->order(*PublicInbox::View::sort_ts);
 	}
-	my $skel = '';
+	my $skel = search_nav_bot($mset, $q). "<pre>";
+	my $inbox = $ctx->{-inbox};
 	my $state = {
-		-inbox => $ctx->{-inbox},
+		-inbox => $inbox,
 		anchor_idx => 1,
 		ctx => $ctx,
 		cur_level => 0,
 		dst => \$skel,
-		fh => $fh,
 		mapping => {},
 		pct => \%pct,
 		prev_attr => '',
@@ -179,42 +183,40 @@ sub tdump {
 		srch => $ctx->{srch},
 		upfx => './',
 	};
-	$ctx->{searchview} = 1;
+
 	PublicInbox::View::walk_thread($th, $state,
 		*PublicInbox::View::pre_thread);
 
-	PublicInbox::View::thread_entry($state, $_, 0) for @m;
-
-	$fh->write(search_nav_bot($mset, $q). "\n\n" . $skel . "\n" .
-			foot($ctx). '</pre></body></html>');
-
-	$fh->close;
-}
-
-sub foot {
-	my ($ctx) = @_;
-	my $foot = $ctx->{footer} || '';
-	qq{Back to <a\nhref=".">index</a>.\n$foot};
+	my $msgs = \@m;
+	my $mime;
+	sub {
+		return unless $msgs;
+		while ($mime = shift @$msgs) {
+			my $mid = mid_clean(mid_mime($mime));
+			$mime = $inbox->msg_by_mid($mid) and last;
+		}
+		if ($mime) {
+			$mime = Email::MIME->new($mime);
+			return PublicInbox::View::index_entry($mime, $state);
+		}
+		$msgs = undef;
+		$skel .= "\n</pre>";
+	};
 }
 
-sub html_start {
+sub ctx_prepare {
 	my ($q, $ctx) = @_;
 	my $qh = ascii_html($q->{'q'});
-	my $A = $q->qs_html(x => 'A', r => undef);
-	my $res = '<html><head>' . PublicInbox::Hval::STYLE .
-		"<title>$qh - search results</title>" .
-		qq{<link\nrel=alternate\ntitle="Atom feed"\n} .
-		qq!href="?$A"\ntype="application/atom+xml"/></head>! .
-		qq{<body><form\naction="">} .
-		qq{<input\nname=q\nvalue="$qh"\ntype=text />};
-
-	$res .= qq{<input\ntype=hidden\nname=r />} if $q->{r};
+	$ctx->{-q_value_html} = $qh;
+	$ctx->{-atom} = '?'.$q->qs_html(x => 'A', r => undef);
+	$ctx->{-title_html} = "$qh - search results";
+	my $extra = '';
+	$extra .= qq{<input\ntype=hidden\nname=r />} if $q->{r};
 	if (my $x = $q->{x}) {
 		$x = ascii_html($x);
-		$res .= qq{<input\ntype=hidden\nname=x\nvalue="$x" />};
+		$extra .= qq{<input\ntype=hidden\nname=x\nvalue="$x" />};
 	}
-
-	$res .= qq{<input\ntype=submit\nvalue=search /></form>};
+	$ctx->{-extra_form_html} = $extra;
 }
 
 sub adump {
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 65788db..a774feb 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -97,8 +97,7 @@ sub nr_to_s ($$$) {
 
 # this is already inside a <pre>
 sub index_entry {
-	my ($mime, $level, $state) = @_;
-	$state->{anchor_idx}++;
+	my ($mime, $state) = @_;
 	my $ctx = $state->{ctx};
 	my $srch = $ctx->{srch};
 	my $hdr = $mime->header_obj;
@@ -118,7 +117,7 @@ sub index_entry {
 	$subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id_m;
 
 	my $ts = _msg_date($hdr);
-	my $rv = "<pre><a\nhref=#e$id\nid=$id_m>#</a> ";
+	my $rv = "<a\nhref=#e$id\nid=$id_m>#</a> ";
 	$rv .= $subj;
 	my $mhref = $path.$href.'/';
 	my $from = _hdr_names($hdr, 'From');
@@ -154,13 +153,7 @@ sub index_entry {
 	if (my $pct = $state->{pct}) { # used by SearchView.pm
 		$rv .= " [relevance $pct->{$mid_raw}%]";
 	}
-	$state->{fh}->write($rv .= "\n</pre>"); # '\n' for lynx
-}
-
-sub thread_html {
-	my ($ctx, $foot, $srch) = @_;
-	# $_[0] in sub is the Plack callback
-	sub { emit_thread_html($_[0], $ctx, $foot, $srch) }
+	$rv .= "\n\n";
 }
 
 sub walk_thread {
@@ -186,25 +179,26 @@ sub pre_thread  {
 	skel_dump($state, $level, $node);
 }
 
-# only private functions below.
-
-sub emit_thread_html {
-	my ($res, $ctx, $foot, $srch) = @_;
+sub thread_html {
+	my ($ctx) = @_;
 	my $mid = $ctx->{mid};
-	my $sres = $srch->get_thread($mid, { asc => 1 });
+	my $sres = $ctx->{srch}->get_thread($mid, { asc => 1 });
 	my $msgs = load_results($sres);
 	my $nr = $sres->{total};
-	return missing_thread($res, $ctx) if $nr == 0;
-	my $skel = '';
+	return missing_thread($ctx) if $nr == 0;
+	my $skel = '</pre><hr /><pre>';
+	$skel .= $nr == 1 ? 'only message in thread' : 'end of thread';
+	$skel .= ", back to <a\nhref=\"../../\">index</a>";
+	$skel .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
+	$skel .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
+	$skel .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
 	my $state = {
-		anchor_idx => 0,
 		ctx => $ctx,
 		cur_level => 0,
 		dst => \$skel,
 		mapping => {}, # mid -> reply count
 		prev_attr => '',
 		prev_level => 0,
-		res => $res,
 		root_anchor => anchor_for($mid),
 		seen => {},
 		srch => $ctx->{srch},
@@ -213,21 +207,28 @@ sub emit_thread_html {
 
 	walk_thread(thread_results($msgs), $state, *pre_thread);
 
-	thread_entry($state, $_, 0) for @$msgs;
-
-	# there could be a race due to a message being deleted in git
-	# but still being in the Xapian index:
-	my $fh = delete $state->{fh} or return missing_thread($res, $ctx);
-
-	my $next = @$msgs == 1 ? 'only message in thread' : 'end of thread';
-	$next .= ", back to <a\nhref=\"../../\">index</a>";
-	$next .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
-	$next .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
-	$next .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
-	$next .= $skel;
-	$fh->write('<hr /><pre>' . $next . "\n\n".
-			$foot .  '</pre></body></html>');
-	$fh->close;
+	# lazy load the full message from mini_mime:
+	my $inbox = $ctx->{-inbox};
+	my $mime;
+	while ($mime = shift @$msgs) {
+		$mime = $inbox->msg_by_mid(mid_clean(mid_mime($mime))) and last;
+	}
+	$mime = Email::MIME->new($mime);
+	$ctx->{-upfx} = '../../';
+	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
+	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $state);
+	$mime = undef;
+	my $body = PublicInbox::WwwStream->new($ctx, sub {
+		return unless $msgs;
+		while ($mime = shift @$msgs) {
+			$mid = mid_clean(mid_mime($mime));
+			$mime = $inbox->msg_by_mid($mid) and last;
+		}
+		return index_entry(Email::MIME->new($mime), $state) if $mime;
+		$msgs = undef;
+		$skel .= "</pre>";
+	});
+	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
 }
 
 sub multipart_text_as_html {
@@ -539,20 +540,6 @@ sub anchor_for {
 	'm' . id_compress($msgid, 1);
 }
 
-sub thread_html_head {
-	my ($hdr, $state) = @_;
-	my $res = delete $state->{res} or die "BUG: no Plack callback in {res}";
-	my $fh = $res->([200, ['Content-Type'=> 'text/html; charset=UTF-8']]);
-	$state->{fh} = $fh;
-
-	my $s = ascii_html($hdr->header('Subject'));
-	$fh->write("<html><head><title>$s</title>".
-		qq{<link\nrel=alternate\ntitle="Atom feed"\n} .
-		qq!href="../t.atom"\ntype="application/atom+xml"/>! .
-		PublicInbox::Hval::STYLE .
-		"</head><body>");
-}
-
 sub ghost_parent {
 	my ($upfx, $mid) = @_;
 	# 'subject dummy' is used internally by Mail::Thread
@@ -564,21 +551,6 @@ sub ghost_parent {
 	qq{[parent not found: &lt;<a\nhref="$upfx$href/">$html</a>&gt;]};
 }
 
-sub thread_entry {
-	my ($state, $mime, $level) = @_;
-
-	# lazy load the full message from mini_mime:
-	$mime = eval {
-		my $mid = mid_clean(mid_mime($mime));
-		$state->{ctx}->{-inbox}->msg_by_mid($mid);
-	} or return;
-	$mime = Email::MIME->new($mime);
-
-	thread_html_head($mime, $state) if $state->{anchor_idx} == 0;
-	index_entry($mime, $level, $state);
-	1;
-}
-
 sub indent_for {
 	my ($level) = @_;
 	INDENT x ($level - 1);
@@ -606,10 +578,9 @@ sub thread_results {
 }
 
 sub missing_thread {
-	my ($res, $ctx) = @_;
+	my ($ctx) = @_;
 	require PublicInbox::ExtMsg;
-
-	$res->(PublicInbox::ExtMsg::ext_msg($ctx))
+	PublicInbox::ExtMsg::ext_msg($ctx);
 }
 
 sub _msg_date {
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index 984268e..196486f 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -233,12 +233,10 @@ sub get_mid_html {
 
 # /$INBOX/$MESSAGE_ID/t/
 sub get_thread {
-	my ($ctx, $flat) = @_;
-	my $srch = searcher($ctx) or return need_search($ctx);
+	my ($ctx) = @_;
+	searcher($ctx) or return need_search($ctx);
 	require PublicInbox::View;
-	my $foot = footer($ctx);
-	$ctx->{flat} = $flat;
-	PublicInbox::View::thread_html($ctx, $foot, $srch);
+	PublicInbox::View::thread_html($ctx);
 }
 
 sub ctx_get {
@@ -414,7 +412,6 @@ sub msg_page {
 	't.atom' eq $e and return get_thread_atom($ctx);
 	't.mbox' eq $e and return get_thread_mbox($ctx);
 	't.mbox.gz' eq $e and return get_thread_mbox($ctx, '.gz');
-	'T/' eq $e and return get_thread($ctx, 1);
 	'raw' eq $e and return get_mid_txt($ctx);
 
 	# legacy, but no redirect for compatibility:
diff --git a/lib/PublicInbox/WwwStream.pm b/lib/PublicInbox/WwwStream.pm
index 34f32c0..d2bf318 100644
--- a/lib/PublicInbox/WwwStream.pm
+++ b/lib/PublicInbox/WwwStream.pm
@@ -22,10 +22,20 @@ sub _html_top ($) {
 	my $title = $ctx->{-title_html} || $desc;
 	my $upfx = $ctx->{-upfx} || '';
 	my $atom = $ctx->{-atom} || $upfx.'new.atom';
+	my $tip = $ctx->{-html_tip} || '';
 	my $top = "<b>$desc</b> (<a\nhref=\"$atom\">Atom feed</a>)";
 	if ($obj->search) {
+		my $q_val = $ctx->{-q_value_html};
+		if (defined $q_val && $q_val ne '') {
+			$q_val = qq(\nvalue="$q_val" );
+		} else {
+			$q_val = '';
+		}
+		# XXX gross, for SearchView.pm
+		my $extra = $ctx->{-extra_form_html} || '';
 		$top = qq{<form\naction="$upfx"><pre>$top} .
-			  qq{ <input\nname=q\ntype=text />} .
+			  qq{ <input\nname=q\ntype=text$q_val/>} .
+			  $extra .
 			  qq{<input\ntype=submit\nvalue=search />} .
 			  q{</pre></form>}
 	} else {
@@ -35,7 +45,7 @@ sub _html_top ($) {
 		"<link\nrel=alternate\ntitle=\"Atom feed\"\n".
 		"href=\"$atom\"\ntype=\"application/atom+xml\"/>" .
 		PublicInbox::Hval::STYLE .
-		"</head><body>$top";
+		"</head><body>". $top . $tip;
 }
 
 sub _html_end {
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 03/13] view: show thread context in the thread-aware flat view
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
  2016-06-30  9:21 ` [PATCH 01/13] www: implement " Eric Wong
  2016-06-30  9:21 ` [PATCH 02/13] www: use WwwStream for dumping thread and search views Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 04/13] view: merge $state hash with existing $ctx Eric Wong
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This lets user have a small window of the context of
the current message relative to other threads.
---
 lib/PublicInbox/Feed.pm       |   3 +-
 lib/PublicInbox/SearchView.pm |   2 +-
 lib/PublicInbox/View.pm       | 160 +++++++++++++++++++++++++-----------------
 3 files changed, 100 insertions(+), 65 deletions(-)

diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index 36802fa..73986e8 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -138,6 +138,7 @@ sub emit_html_index {
 	my $fh = $res->([200,['Content-Type'=>'text/html; charset=UTF-8']]);
 
 	my $max = $ctx->{max} || MAX_PER_PAGE;
+	$ctx->{-upfx} = '';
 
 	my ($footer, $param, $last);
 	my $state = { ctx => $ctx, seen => {}, anchor_idx => 0, fh => $fh };
@@ -174,7 +175,7 @@ sub emit_index_nosrch {
 		$state->{first} ||= $commit;
 
 		my $mime = do_cat_mail($ibx, $path) or return 0;
-		$fh->write(PublicInbox::View::index_entry($mime, $state));
+		$fh->write(PublicInbox::View::index_entry($mime, $state, 1));
 		1;
 	});
 	$last;
diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm
index 488822e..8771d5d 100644
--- a/lib/PublicInbox/SearchView.pm
+++ b/lib/PublicInbox/SearchView.pm
@@ -169,6 +169,7 @@ sub mset_thread {
 	}
 	my $skel = search_nav_bot($mset, $q). "<pre>";
 	my $inbox = $ctx->{-inbox};
+	$ctx->{-upfx} = '';
 	my $state = {
 		-inbox => $inbox,
 		anchor_idx => 1,
@@ -181,7 +182,6 @@ sub mset_thread {
 		prev_level => 0,
 		seen => {},
 		srch => $ctx->{srch},
-		upfx => './',
 	};
 
 	PublicInbox::View::walk_thread($th, $state,
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index a774feb..eac541d 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -97,7 +97,7 @@ sub nr_to_s ($$$) {
 
 # this is already inside a <pre>
 sub index_entry {
-	my ($mime, $state) = @_;
+	my ($mime, $state, $more) = @_;
 	my $ctx = $state->{ctx};
 	my $srch = $ctx->{srch};
 	my $hdr = $mime->header_obj;
@@ -109,51 +109,76 @@ sub index_entry {
 	my $mid = PublicInbox::Hval->new_msgid($mid_raw);
 
 	my $root_anchor = $state->{root_anchor} || '';
-	my $path = $root_anchor ? '../../' : '';
-	my $href = $mid->as_href;
 	my $irt = in_reply_to($hdr);
 
-	$subj = '<b>'.ascii_html($subj).'</b>';
-	$subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id_m;
-
-	my $ts = _msg_date($hdr);
-	my $rv = "<a\nhref=#e$id\nid=$id_m>#</a> ";
-	$rv .= $subj;
-	my $mhref = $path.$href.'/';
-	my $from = _hdr_names($hdr, 'From');
-	$rv .= "\n- $from @ $ts UTC\n";
+	my $rv = '<b>'.ascii_html($subj).'</b>';
+	$rv = "<u\nid=u>$rv</u>" if $root_anchor eq $id_m;
+	$rv .= "\n";
+	$rv .= _th_index_lite($mid_raw, $irt, $id, $state);
 	my @tocc;
 	foreach my $f (qw(To Cc)) {
 		my $dst = _hdr_names($hdr, $f);
 		push @tocc, "$f: $dst" if $dst ne '';
 	}
+	$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC\n";
 	$rv .= '  '.join('; +', @tocc) . "\n" if @tocc;
 	$rv .= "\n";
 
 	# scan through all parts, looking for displayable text
+	my $href = $mid->as_href;
+	my $mhref = $ctx->{-upfx}.$href.'/';
 	msg_iter($mime, sub { $rv .= add_text_body($mhref, $_[0]) });
-	$rv .= "\n<a\nhref=\"$mhref\"\n>permalink</a>" .
-		" / <a\nhref=\"${mhref}raw\">raw</a> / ";
-	my $mapping = $state->{mapping};
-	my $nr_c = $mapping->{$mid_raw} || 0;
+
+	# add the footer
+	$rv .= "\n<a\nhref=#$id_m\nid=e$id>^</a> ".
+		"<a\nhref=\"$mhref\">permalink</a>" .
+		" / <a\nhref=\"${mhref}raw\">raw</a>" .
+		" / <a\nhref=\"${mhref}#R\">reply</a>";
+	if (my $pct = $state->{pct}) { # used by SearchView.pm
+		$rv .= " [relevance $pct->{$mid_raw}%]";
+	}
+	$rv .= $more ? "\n\n" : "\n";
+}
+
+sub _th_index_lite {
+	my ($mid_raw, $irt, $id, $state) = @_;
+	my $rv = '';
+	my $mapping = $state->{mapping} or return $rv;
+	my $pad = '  ';
+	# map = [children, attr, node, idx, level]
+	my $map = $mapping->{$mid_raw};
+	my $nr_c = scalar @{$map->[0]};
 	my $nr_s = 0;
 	if (defined $irt) {
-		$nr_s = ($mapping->{$irt} || 0) - 1;
+		my $irt_map = $mapping->{$irt};
+		my $siblings = $irt_map->[0];
+		$nr_s = scalar(@$siblings) - 1;
 		$nr_s = 0 if $nr_s < 0;
-		$irt = anchor_for($irt);
-		$rv .= "<a\nhref=#$irt>#parent</a>,";
-	} else {
-		$rv .= 'root message:';
+		$rv .= $pad . $irt_map->[1];
+		my $idx = $map->[3];
+		if ($idx > 0) {
+			my $prev = $siblings->[$idx - 1];
+			$rv .= $pad . $mapping->{$prev->messageid}->[1];
+		}
 	}
-	$nr_s = nr_to_s($nr_s, 'sibling', 'siblings');
-	$nr_c = nr_to_s($nr_c, 'reply', 'replies');
-	$rv .= " <a\nhref=#r$id\nid=e$id>$nr_s, $nr_c</a>";
-	$rv .= " / <a\nhref=\"${mhref}#R\">reply</a>";
-
-	if (my $pct = $state->{pct}) { # used by SearchView.pm
-		$rv .= " [relevance $pct->{$mid_raw}%]";
+	my $s_s = nr_to_s($nr_s, 'sibling', 'siblings');
+	my $s_c = nr_to_s($nr_c, 'reply', 'replies');
+	my $this = $map->[1];
+	$this =~ s!\n\z!</b>\n!s;
+	$this =~ s!<a\nhref.*a> !!s; # no point in duplicating subject
+	$rv .= "<b>@ $this";
+	my $node = $map->[2];
+	if (my $child = $node->child) {
+		$rv .= $pad . $mapping->{$child->messageid}->[1];
+	}
+	if (my $next = $node->next) {
+		$rv .= $pad .  $mapping->{$next->messageid}->[1];
 	}
-	$rv .= "\n\n";
+	$rv .= "<a\nhref=#e$id\nid=m$id>.<a>\t\t\t";
+	$rv .= "(<a\nhref=#r$id\n>$s_s, $s_c</a> / ";
+	my $upfx = $state->{ctx}->{-upfx};
+	$rv .= qq{<a\nhref="$upfx$mid_raw/">permalink</a> / };
+	$rv .= qq{<a\nhref="$upfx$mid_raw/raw">raw</a>)\n};
 }
 
 sub walk_thread {
@@ -169,13 +194,14 @@ sub walk_thread {
 
 sub pre_thread  {
 	my ($state, $level, $node) = @_;
-	my $parent = $node->parent;
-	if ($parent) {
-		my $mid = $parent->messageid;
-		my $m = $state->{mapping};
-		$m->{$mid} ||= 0;
-		$m->{$mid}++;
+	my $mapping = $state->{mapping};
+	my $idx = -1;
+	if (my $parent = $node->parent) {
+		my $m = $mapping->{$parent->messageid}->[0];
+		$idx = scalar @$m;
+		push @$m, $node;
 	}
+	$mapping->{$node->messageid} = [ [], '', $node, $idx ];
 	skel_dump($state, $level, $node);
 }
 
@@ -196,13 +222,12 @@ sub thread_html {
 		ctx => $ctx,
 		cur_level => 0,
 		dst => \$skel,
-		mapping => {}, # mid -> reply count
+		mapping => {}, # mid -> [ reply count, from@date, node ];
 		prev_attr => '',
 		prev_level => 0,
 		root_anchor => anchor_for($mid),
 		seen => {},
 		srch => $ctx->{srch},
-		upfx => '../../',
 	};
 
 	walk_thread(thread_results($msgs), $state, *pre_thread);
@@ -216,7 +241,7 @@ sub thread_html {
 	$mime = Email::MIME->new($mime);
 	$ctx->{-upfx} = '../../';
 	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
-	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $state);
+	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $state, scalar @$msgs);
 	$mime = undef;
 	my $body = PublicInbox::WwwStream->new($ctx, sub {
 		return unless $msgs;
@@ -224,9 +249,12 @@ sub thread_html {
 			$mid = mid_clean(mid_mime($mime));
 			$mime = $inbox->msg_by_mid($mid) and last;
 		}
-		return index_entry(Email::MIME->new($mime), $state) if $mime;
+		if ($mime) {
+			$mime = Email::MIME->new($mime);
+			return index_entry($mime, $state, scalar @$msgs);
+		}
 		$msgs = undef;
-		$skel .= "</pre>";
+		$skel .= '</pre>';
 	});
 	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
 }
@@ -405,7 +433,6 @@ sub thread_skel {
 		cur => $mid,
 		prev_attr => '',
 		prev_level => 0,
-		upfx => "$tpfx../",
 		dst => $dst,
 	};
 	walk_thread(thread_results(load_results($sres)), $state, *skel_dump);
@@ -598,8 +625,7 @@ sub _skel_header {
 	my $cur = $state->{cur};
 	my $mid = mid_clean($hdr->header_raw('Message-ID'));
 	my $f = ascii_html($hdr->header('X-PI-From'));
-	my $d = _msg_date($hdr);
-	my $pfx = "$d " . indent_for($level) . th_pfx($level);
+	my $d = _msg_date($hdr) . ' ' . indent_for($level) . th_pfx($level);
 	my $attr = $f;
 	$state->{first_level} ||= $level;
 
@@ -613,9 +639,9 @@ sub _skel_header {
 	if ($cur) {
 		if ($cur eq $mid) {
 			delete $state->{cur};
-			$$dst .= "$pfx<b><a\nid=r\nhref=\"#t\">".
+			$$dst .= $d;
+			$$dst .= "<b><a\nid=r\nhref=\"#t\">".
 				 "$attr [this message]</a></b>\n";
-
 			return;
 		}
 	} else {
@@ -636,15 +662,18 @@ sub _skel_header {
 	}
 	my $m = PublicInbox::Hval->new_msgid($mid);
 	my $id = '';
-	if ($state->{mapping}) {
+	my $mapping = $state->{mapping};
+	my $end = defined($s) ? "$s</a> $f\n" : "$f</a>\n";
+	if ($mapping) {
+		my $map = $mapping->{$mid};
 		$id = id_compress($mid, 1);
 		$m = '#m'.$id;
+		$map->[1] = "$d<a\nhref=\"$m\">$end";
 		$id = "\nid=r".$id;
 	} else {
-		$m = $state->{upfx}.$m->as_href.'/';
+		$m = $state->{ctx}->{-upfx}.$m->as_href.'/';
 	}
-	$$dst .= "$pfx<a\nhref=\"$m\"$id>";
-	$$dst .= defined($s) ? "$s</a> $f\n" : "$f</a>\n";
+	$$dst .=  $d . "<a\nhref=\"$m\"$id>" . $end;
 }
 
 sub skel_dump {
@@ -654,25 +683,30 @@ sub skel_dump {
 	} else {
 		my $mid = $node->messageid;
 		my $dst = $state->{dst};
+		my $mapping = $state->{mapping};
+		my $map = $mapping->{$mid} if $mapping;
 		if ($mid eq 'subject dummy') {
-			$$dst .= "\t[no common parent]\n";
+			my $ncp = "\t[no common parent]\n";
+			$map->[1] = $ncp if $map;
+			$$dst .= $ncp;
 			return;
 		}
-		if ($state->{pct}) { # search result
-			$$dst .= '    [irrelevant] ';
+		my $d = $state->{pct} ? '    [irrelevant] ' # search result
+				      : '     [not found] ';
+		$d .= indent_for($level) . th_pfx($level);
+		my $upfx = $state->{ctx}->{-upfx};
+		my $m = PublicInbox::Hval->new_msgid($mid);
+		my $href = $upfx . $m->as_href . '/';
+		my $html = $m->as_html;
+
+		if ($map) {
+			my $id = id_compress($mid, 1);
+			$map->[1] = $d . qq{&lt;<a\nhref=#r$id>$html</a>&gt;\n};
+			$d .= qq{&lt;<a\nhref="$href"\nid=r$id>$html</a>&gt;\n};
 		} else {
-			$$dst .= '     [not found] ';
-		}
-		$$dst .= indent_for($level) . th_pfx($level);
-		my $upfx = $state->{upfx};
-		my $id = '';
-		if ($state->{mapping}) { # thread index view
-			$id = "\nid=".anchor_for($mid);
+			$d .= qq{&lt;<a\nhref="$href">$html</a>&gt;\n};
 		}
-		$mid = PublicInbox::Hval->new_msgid($mid);
-		my $href = $upfx . $mid->as_href . '/';
-		my $html = $mid->as_html;
-		$$dst .= qq{&lt;<a\nhref="$href"$id>$html</a>&gt;\n};
+		$$dst .= $d;
 	}
 }
 
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 04/13] view: merge $state hash with existing $ctx
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (2 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 03/13] view: show thread context in the thread-aware flat view Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 05/13] feed: add $INBOX/new.html endpoint Eric Wong
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This reduces the level of indirection to reach certain objects
within the hash and there are no namespace or lifetime conflicts
anyways.
---
 lib/PublicInbox/Feed.pm       |  25 +++----
 lib/PublicInbox/SearchView.pm |  25 +++----
 lib/PublicInbox/View.pm       | 149 ++++++++++++++++++++----------------------
 3 files changed, 93 insertions(+), 106 deletions(-)

diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index 73986e8..ddc1e3c 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -141,7 +141,9 @@ sub emit_html_index {
 	$ctx->{-upfx} = '';
 
 	my ($footer, $param, $last);
-	my $state = { ctx => $ctx, seen => {}, anchor_idx => 0, fh => $fh };
+	$ctx->{seen} = {};
+	$ctx->{anchor_idx} = 0;
+	$ctx->{fh} = $fh;
 	my $srch = $ctx->{srch};
 	$fh->write(_html_index_top($feed_opts, $srch));
 
@@ -149,14 +151,13 @@ sub emit_html_index {
 	# which we must continue supporting:
 	my $qp = $ctx->{qp};
 	if ($qp && !$qp->{r} && $srch) {
-		$state->{srch} = $srch;
-		$last = PublicInbox::View::emit_index_topics($state);
+		$last = PublicInbox::View::emit_index_topics($ctx);
 		$param = 'o';
 	} else {
-		$last = emit_index_nosrch($ctx, $state);
+		$last = emit_index_nosrch($ctx);
 		$param = 'r';
 	}
-	$footer = nav_footer($ctx, $last, $feed_opts, $state, $param);
+	$footer = nav_footer($ctx, $last, $feed_opts, $param);
 	if ($footer) {
 		my $list_footer = $ctx->{footer};
 		$footer .= "\n\n" . $list_footer if $list_footer;
@@ -167,28 +168,28 @@ sub emit_html_index {
 }
 
 sub emit_index_nosrch {
-	my ($ctx, $state) = @_;
+	my ($ctx) = @_;
 	my $ibx = $ctx->{-inbox};
-	my $fh = $state->{fh};
+	my $fh = $ctx->{fh};
 	my (undef, $last) = each_recent_blob($ctx, sub {
 		my ($path, $commit, $ts, $u, $subj) = @_;
-		$state->{first} ||= $commit;
+		$ctx->{first} ||= $commit;
 
 		my $mime = do_cat_mail($ibx, $path) or return 0;
-		$fh->write(PublicInbox::View::index_entry($mime, $state, 1));
+		$fh->write(PublicInbox::View::index_entry($mime, $ctx, 1));
 		1;
 	});
 	$last;
 }
 
 sub nav_footer {
-	my ($ctx, $last, $feed_opts, $state, $param) = @_;
+	my ($ctx, $last, $feed_opts, $param) = @_;
 	my $qp = $ctx->{qp} or return '';
 	my $old_r = $qp->{$param};
 	my $head = '    ';
 	my $next = '    ';
-	my $first = $state->{first};
-	my $anchor = $state->{anchor_idx};
+	my $first = $ctx->{first};
+	my $anchor = $ctx->{anchor_idx};
 
 	if ($last) {
 		$next = qq!<a\nhref="?$param=$last"\nrel=next>next</a>!;
diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm
index 8771d5d..4af6cad 100644
--- a/lib/PublicInbox/SearchView.pm
+++ b/lib/PublicInbox/SearchView.pm
@@ -170,21 +170,16 @@ sub mset_thread {
 	my $skel = search_nav_bot($mset, $q). "<pre>";
 	my $inbox = $ctx->{-inbox};
 	$ctx->{-upfx} = '';
-	my $state = {
-		-inbox => $inbox,
-		anchor_idx => 1,
-		ctx => $ctx,
-		cur_level => 0,
-		dst => \$skel,
-		mapping => {},
-		pct => \%pct,
-		prev_attr => '',
-		prev_level => 0,
-		seen => {},
-		srch => $ctx->{srch},
-	};
+	$ctx->{anchor_idx} = 1;
+	$ctx->{cur_level} = 0;
+	$ctx->{dst} = \$skel;
+	$ctx->{mapping} = {};
+	$ctx->{pct} = \%pct;
+	$ctx->{prev_attr} = '';
+	$ctx->{prev_level} = 0;
+	$ctx->{seen} = {};
 
-	PublicInbox::View::walk_thread($th, $state,
+	PublicInbox::View::walk_thread($th, $ctx,
 		*PublicInbox::View::pre_thread);
 
 	my $msgs = \@m;
@@ -197,7 +192,7 @@ sub mset_thread {
 		}
 		if ($mime) {
 			$mime = Email::MIME->new($mime);
-			return PublicInbox::View::index_entry($mime, $state);
+			return PublicInbox::View::index_entry($mime, $ctx);
 		}
 		$msgs = undef;
 		$skel .= "\n</pre>";
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index eac541d..0b47c89 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -97,8 +97,7 @@ sub nr_to_s ($$$) {
 
 # this is already inside a <pre>
 sub index_entry {
-	my ($mime, $state, $more) = @_;
-	my $ctx = $state->{ctx};
+	my ($mime, $ctx, $more) = @_;
 	my $srch = $ctx->{srch};
 	my $hdr = $mime->header_obj;
 	my $subj = $hdr->header('Subject');
@@ -108,13 +107,13 @@ sub index_entry {
 	my $id_m = 'm'.$id;
 	my $mid = PublicInbox::Hval->new_msgid($mid_raw);
 
-	my $root_anchor = $state->{root_anchor} || '';
+	my $root_anchor = $ctx->{root_anchor} || '';
 	my $irt = in_reply_to($hdr);
 
 	my $rv = '<b>'.ascii_html($subj).'</b>';
 	$rv = "<u\nid=u>$rv</u>" if $root_anchor eq $id_m;
 	$rv .= "\n";
-	$rv .= _th_index_lite($mid_raw, $irt, $id, $state);
+	$rv .= _th_index_lite($mid_raw, $irt, $id, $ctx);
 	my @tocc;
 	foreach my $f (qw(To Cc)) {
 		my $dst = _hdr_names($hdr, $f);
@@ -134,16 +133,16 @@ sub index_entry {
 		"<a\nhref=\"$mhref\">permalink</a>" .
 		" / <a\nhref=\"${mhref}raw\">raw</a>" .
 		" / <a\nhref=\"${mhref}#R\">reply</a>";
-	if (my $pct = $state->{pct}) { # used by SearchView.pm
+	if (my $pct = $ctx->{pct}) { # used by SearchView.pm
 		$rv .= " [relevance $pct->{$mid_raw}%]";
 	}
 	$rv .= $more ? "\n\n" : "\n";
 }
 
 sub _th_index_lite {
-	my ($mid_raw, $irt, $id, $state) = @_;
+	my ($mid_raw, $irt, $id, $ctx) = @_;
 	my $rv = '';
-	my $mapping = $state->{mapping} or return $rv;
+	my $mapping = $ctx->{mapping} or return $rv;
 	my $pad = '  ';
 	# map = [children, attr, node, idx, level]
 	my $map = $mapping->{$mid_raw};
@@ -176,25 +175,25 @@ sub _th_index_lite {
 	}
 	$rv .= "<a\nhref=#e$id\nid=m$id>.<a>\t\t\t";
 	$rv .= "(<a\nhref=#r$id\n>$s_s, $s_c</a> / ";
-	my $upfx = $state->{ctx}->{-upfx};
+	my $upfx = $ctx->{-upfx};
 	$rv .= qq{<a\nhref="$upfx$mid_raw/">permalink</a> / };
 	$rv .= qq{<a\nhref="$upfx$mid_raw/raw">raw</a>)\n};
 }
 
 sub walk_thread {
-	my ($th, $state, $cb) = @_;
+	my ($th, $ctx, $cb) = @_;
 	my @q = map { (0, $_) } $th->rootset;
 	while (@q) {
 		my $level = shift @q;
 		my $node = shift @q or next;
-		$cb->($state, $level, $node);
+		$cb->($ctx, $level, $node);
 		unshift @q, $level+1, $node->child, $level, $node->next;
 	}
 }
 
 sub pre_thread  {
-	my ($state, $level, $node) = @_;
-	my $mapping = $state->{mapping};
+	my ($ctx, $level, $node) = @_;
+	my $mapping = $ctx->{mapping};
 	my $idx = -1;
 	if (my $parent = $node->parent) {
 		my $m = $mapping->{$parent->messageid}->[0];
@@ -202,7 +201,7 @@ sub pre_thread  {
 		push @$m, $node;
 	}
 	$mapping->{$node->messageid} = [ [], '', $node, $idx ];
-	skel_dump($state, $level, $node);
+	skel_dump($ctx, $level, $node);
 }
 
 sub thread_html {
@@ -218,19 +217,15 @@ sub thread_html {
 	$skel .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
 	$skel .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
 	$skel .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
-	my $state = {
-		ctx => $ctx,
-		cur_level => 0,
-		dst => \$skel,
-		mapping => {}, # mid -> [ reply count, from@date, node ];
-		prev_attr => '',
-		prev_level => 0,
-		root_anchor => anchor_for($mid),
-		seen => {},
-		srch => $ctx->{srch},
-	};
-
-	walk_thread(thread_results($msgs), $state, *pre_thread);
+	$ctx->{cur_level} = 0;
+	$ctx->{dst} = \$skel;
+	$ctx->{mapping} = {}; # mid -> [ reply count, from@date, node ];
+	$ctx->{prev_attr} = '';
+	$ctx->{prev_level} = 0;
+	$ctx->{root_anchor} = anchor_for($mid);
+	$ctx->{seen} = {};
+
+	walk_thread(thread_results($msgs), $ctx, *pre_thread);
 
 	# lazy load the full message from mini_mime:
 	my $inbox = $ctx->{-inbox};
@@ -241,7 +236,7 @@ sub thread_html {
 	$mime = Email::MIME->new($mime);
 	$ctx->{-upfx} = '../../';
 	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
-	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $state, scalar @$msgs);
+	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $ctx, scalar @$msgs);
 	$mime = undef;
 	my $body = PublicInbox::WwwStream->new($ctx, sub {
 		return unless $msgs;
@@ -251,7 +246,7 @@ sub thread_html {
 		}
 		if ($mime) {
 			$mime = Email::MIME->new($mime);
-			return index_entry($mime, $state, scalar @$msgs);
+			return index_entry($mime, $ctx, scalar @$msgs);
 		}
 		$msgs = undef;
 		$skel .= '</pre>';
@@ -427,16 +422,12 @@ sub thread_skel {
 	$$dst .= qq! / <a\nhref="#b">[top]</a>)\n!;
 
 	my $subj = $srch->subject_path($hdr->header('Subject'));
-	my $state = {
-		seen => { $subj => 1 },
-		srch => $srch,
-		cur => $mid,
-		prev_attr => '',
-		prev_level => 0,
-		dst => $dst,
-	};
-	walk_thread(thread_results(load_results($sres)), $state, *skel_dump);
-	$ctx->{next_msg} = $state->{next_msg};
+	$ctx->{seen} = { $subj => 1 };
+	$ctx->{cur} = $mid;
+	$ctx->{prev_attr} = '';
+	$ctx->{prev_level} = 0;
+	$ctx->{dst} = $dst;
+	walk_thread(thread_results(load_results($sres)), $ctx, *skel_dump);
 	$ctx->{parent_msg} = $parent;
 }
 
@@ -619,50 +610,50 @@ sub _msg_date {
 sub fmt_ts { POSIX::strftime('%Y-%m-%d %k:%M', gmtime($_[0])) }
 
 sub _skel_header {
-	my ($state, $hdr, $level) = @_;
+	my ($ctx, $hdr, $level) = @_;
 
-	my $dst = $state->{dst};
-	my $cur = $state->{cur};
+	my $dst = $ctx->{dst};
+	my $cur = $ctx->{cur};
 	my $mid = mid_clean($hdr->header_raw('Message-ID'));
 	my $f = ascii_html($hdr->header('X-PI-From'));
 	my $d = _msg_date($hdr) . ' ' . indent_for($level) . th_pfx($level);
 	my $attr = $f;
-	$state->{first_level} ||= $level;
+	$ctx->{first_level} ||= $level;
 
-	if ($attr ne $state->{prev_attr} || $state->{prev_level} > $level) {
-		$state->{prev_attr} = $attr;
+	if ($attr ne $ctx->{prev_attr} || $ctx->{prev_level} > $level) {
+		$ctx->{prev_attr} = $attr;
 	} else {
 		$attr = '';
 	}
-	$state->{prev_level} = $level;
+	$ctx->{prev_level} = $level;
 
 	if ($cur) {
 		if ($cur eq $mid) {
-			delete $state->{cur};
+			delete $ctx->{cur};
 			$$dst .= $d;
 			$$dst .= "<b><a\nid=r\nhref=\"#t\">".
 				 "$attr [this message]</a></b>\n";
 			return;
 		}
 	} else {
-		$state->{next_msg} ||= $mid;
+		$ctx->{next_msg} ||= $mid;
 	}
 
 	# Subject is never undef, this mail was loaded from
 	# our Xapian which would've resulted in '' if it were
 	# really missing (and Filter rejects empty subjects)
 	my $s = $hdr->header('Subject');
-	my $h = $state->{srch}->subject_path($s);
-	if ($state->{seen}->{$h}) {
+	my $h = $ctx->{srch}->subject_path($s);
+	if ($ctx->{seen}->{$h}) {
 		$s = undef;
 	} else {
-		$state->{seen}->{$h} = 1;
+		$ctx->{seen}->{$h} = 1;
 		$s = PublicInbox::Hval->new($s);
 		$s = $s->as_html;
 	}
 	my $m = PublicInbox::Hval->new_msgid($mid);
 	my $id = '';
-	my $mapping = $state->{mapping};
+	my $mapping = $ctx->{mapping};
 	my $end = defined($s) ? "$s</a> $f\n" : "$f</a>\n";
 	if ($mapping) {
 		my $map = $mapping->{$mid};
@@ -671,19 +662,19 @@ sub _skel_header {
 		$map->[1] = "$d<a\nhref=\"$m\">$end";
 		$id = "\nid=r".$id;
 	} else {
-		$m = $state->{ctx}->{-upfx}.$m->as_href.'/';
+		$m = $ctx->{-upfx}.$m->as_href.'/';
 	}
 	$$dst .=  $d . "<a\nhref=\"$m\"$id>" . $end;
 }
 
 sub skel_dump {
-	my ($state, $level, $node) = @_;
+	my ($ctx, $level, $node) = @_;
 	if (my $mime = $node->message) {
-		_skel_header($state, $mime->header_obj, $level);
+		_skel_header($ctx, $mime->header_obj, $level);
 	} else {
 		my $mid = $node->messageid;
-		my $dst = $state->{dst};
-		my $mapping = $state->{mapping};
+		my $dst = $ctx->{dst};
+		my $mapping = $ctx->{mapping};
 		my $map = $mapping->{$mid} if $mapping;
 		if ($mid eq 'subject dummy') {
 			my $ncp = "\t[no common parent]\n";
@@ -691,10 +682,10 @@ sub skel_dump {
 			$$dst .= $ncp;
 			return;
 		}
-		my $d = $state->{pct} ? '    [irrelevant] ' # search result
-				      : '     [not found] ';
+		my $d = $ctx->{pct} ? '    [irrelevant] ' # search result
+				    : '     [not found] ';
 		$d .= indent_for($level) . th_pfx($level);
-		my $upfx = $state->{ctx}->{-upfx};
+		my $upfx = $ctx->{-upfx};
 		my $m = PublicInbox::Hval->new_msgid($mid);
 		my $href = $upfx . $m->as_href . '/';
 		my $html = $m->as_html;
@@ -726,8 +717,8 @@ sub _tryload_ghost ($$) {
 # accumulate recent topics if search is supported
 # returns 1 if done, undef if not
 sub add_topic {
-	my ($state, $level, $node) = @_;
-	my $srch = $state->{srch};
+	my ($ctx, $level, $node) = @_;
+	my $srch = $ctx->{srch};
 	my $mid = $node->messageid;
 	my $x = $node->message || _tryload_ghost($srch, $mid);
 	my ($subj, $ts);
@@ -740,21 +731,21 @@ sub add_topic {
 		$ts = -666;
 		$subj = "<$mid>";
 	}
-	if (++$state->{subjs}->{$subj} == 1) {
-		push @{$state->{order}}, [ $level, $subj ];
+	if (++$ctx->{subjs}->{$subj} == 1) {
+		push @{$ctx->{order}}, [ $level, $subj ];
 	}
-	my $exist = $state->{latest}->{$subj};
+	my $exist = $ctx->{latest}->{$subj};
 	if (!$exist || $exist->[1] < $ts) {
-		$state->{latest}->{$subj} = [ $mid, $ts ];
+		$ctx->{latest}->{$subj} = [ $mid, $ts ];
 	}
 }
 
 sub emit_topics {
-	my ($state) = @_;
-	my $order = $state->{order};
-	my $subjs = $state->{subjs};
-	my $latest = $state->{latest};
-	my $fh = $state->{fh};
+	my ($ctx) = @_;
+	my $order = $ctx->{order};
+	my $subjs = $ctx->{subjs};
+	my $latest = $ctx->{latest};
+	my $fh = $ctx->{fh};
 	return $fh->write("\n[No topics in range]</pre>") unless scalar @$order;
 	my $pfx;
 	my $prev = 0;
@@ -803,22 +794,22 @@ sub emit_topics {
 }
 
 sub emit_index_topics {
-	my ($state) = @_;
-	my ($off) = (($state->{ctx}->{cgi}->param('o') || '0') =~ /(\d+)/);
-	$state->{order} = [];
-	$state->{subjs} = {};
-	$state->{latest} = {};
+	my ($ctx) = @_;
+	my ($off) = (($ctx->{cgi}->param('o') || '0') =~ /(\d+)/);
+	$ctx->{order} = [];
+	$ctx->{subjs} = {};
+	$ctx->{latest} = {};
 	my $max = 25;
 	my %opts = ( offset => $off, limit => $max * 4 );
-	while (scalar @{$state->{order}} < $max) {
-		my $sres = $state->{srch}->query('', \%opts);
+	while (scalar @{$ctx->{order}} < $max) {
+		my $sres = $ctx->{srch}->query('', \%opts);
 		my $nr = scalar @{$sres->{msgs}} or last;
 		$sres = load_results($sres);
-		walk_thread(thread_results($sres), $state, *add_topic);
+		walk_thread(thread_results($sres), $ctx, *add_topic);
 		$opts{offset} += $nr;
 	}
 
-	emit_topics($state);
+	emit_topics($ctx);
 	$opts{offset};
 }
 
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 05/13] feed: add $INBOX/new.html endpoint
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (3 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 04/13] view: merge $state hash with existing $ctx Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 06/13] view: tweak thread/index header slightly Eric Wong
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This acts like the Atom feed; but should be viewable directly
from browsers.
---
 lib/PublicInbox/Feed.pm | 27 +++++++++++++++++++++++++++
 lib/PublicInbox/WWW.pm  | 10 +++++++++-
 2 files changed, 36 insertions(+), 1 deletion(-)

diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index ddc1e3c..c16c417 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -34,6 +34,33 @@ sub generate_html_index {
 	sub { emit_html_index($_[0], $ctx) };
 }
 
+sub new_html {
+	my ($ctx) = @_;
+	my @paths;
+	my (undef, $last) = each_recent_blob($ctx, sub {
+		my ($path, $commit, $ts, $u, $subj) = @_;
+		$ctx->{first} ||= $commit;
+		push @paths, $path;
+	});
+	if (!@paths) {
+		return [404, ['Content-Type', 'text/plain'],
+			["No messages, yet\n"] ];
+	}
+	$ctx->{-html_tip} = '<pre>';
+	$ctx->{-upfx} = '';
+	my $res = PublicInbox::WwwStream->new($ctx, sub {
+		while (my $path = shift @paths) {
+			my $m = do_cat_mail($ctx->{-inbox}, $path) or next;
+			my $more = scalar @paths;
+			my $s = PublicInbox::View::index_entry($m, $ctx, $more);
+			$s .= '</pre>' unless $more;
+			return $s;
+		}
+		undef;
+	});
+	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $res ]
+}
+
 # private subs
 
 sub title_tag {
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index 196486f..da5c1d3 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -75,7 +75,8 @@ sub call {
 		invalid_inbox($self, $ctx, $1) || get_index($ctx);
 	} elsif ($path_info =~ m!$INBOX_RE/(?:atom\.xml|new\.atom)\z!o) {
 		invalid_inbox($self, $ctx, $1) || get_atom($ctx);
-
+	} elsif ($path_info =~ m!$INBOX_RE/new\.html\z!o) {
+		invalid_inbox($self, $ctx, $1) || get_new($ctx);
 	} elsif ($path_info =~ m!$INBOX_RE/
 				($PublicInbox::GitHTTPBackend::ANY)\z!ox) {
 		my $path = $2;
@@ -189,6 +190,13 @@ sub get_atom {
 	PublicInbox::Feed::generate($ctx);
 }
 
+# /$INBOX/new.html			-> HTML only
+sub get_new {
+	my ($ctx) = @_;
+	require PublicInbox::Feed;
+	PublicInbox::Feed::new_html($ctx);
+}
+
 # /$INBOX/?r=$GIT_COMMIT                 -> HTML only
 sub get_index {
 	my ($ctx) = @_;
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 06/13] view: tweak thread/index header slightly
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (4 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 05/13] feed: add $INBOX/new.html endpoint Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 07/13] view: show more nearby messages in flat thread view Eric Wong
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This makes the top permalink/raw as well as the In-Reply-To
show up without search.  While we're at it, try to make
the links on the thread index from the "X siblings, Y replies"
more obvious.
---
 lib/PublicInbox/View.pm | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 0b47c89..9393d44 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -119,8 +119,15 @@ sub index_entry {
 		my $dst = _hdr_names($hdr, $f);
 		push @tocc, "$f: $dst" if $dst ne '';
 	}
-	$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC\n";
+	my $mapping = $ctx->{mapping};
+	$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC";
+	my $upfx = $ctx->{-upfx};
+	$rv .= qq{ (<a\nhref="$upfx$mid_raw/">permalink</a> / };
+	$rv .= qq{<a\nhref="$upfx$mid_raw/raw">raw</a>)\n};
 	$rv .= '  '.join('; +', @tocc) . "\n" if @tocc;
+	if (!$mapping && $irt) {
+		$rv .= qq(In-Reply-To: &lt;<a\nhref="$upfx$irt/">$irt</a>&gt;\n)
+	}
 	$rv .= "\n";
 
 	# scan through all parts, looking for displayable text
@@ -173,11 +180,7 @@ sub _th_index_lite {
 	if (my $next = $node->next) {
 		$rv .= $pad .  $mapping->{$next->messageid}->[1];
 	}
-	$rv .= "<a\nhref=#e$id\nid=m$id>.<a>\t\t\t";
-	$rv .= "(<a\nhref=#r$id\n>$s_s, $s_c</a> / ";
-	my $upfx = $ctx->{-upfx};
-	$rv .= qq{<a\nhref="$upfx$mid_raw/">permalink</a> / };
-	$rv .= qq{<a\nhref="$upfx$mid_raw/raw">raw</a>)\n};
+	$rv .= "<a\nhref=#e$id\nid=m$id>_<a> <a\nhref=#r$id\n>$s_s, $s_c</a>\n";
 }
 
 sub walk_thread {
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 07/13] view: show more nearby messages in flat thread view
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (5 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 06/13] view: tweak thread/index header slightly Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 08/13] www: reinstate old thread view as an option Eric Wong
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

Context is important, but so is conserving precious screen
space.  Decisions :<
---
 lib/PublicInbox/View.pm | 42 ++++++++++++++++++++++++++++++++++++------
 1 file changed, 36 insertions(+), 6 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 9393d44..0b96bda 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -146,6 +146,13 @@ sub index_entry {
 	$rv .= $more ? "\n\n" : "\n";
 }
 
+sub pad_link ($$;$) {
+	my ($mid, $level, $s) = @_;
+	$s ||= '...';
+	my $id = id_compress($mid, 1);
+	(' 'x19).indent_for($level).th_pfx($level)."<a\nhref=#r$id>($s)<a>\n";
+}
+
 sub _th_index_lite {
 	my ($mid_raw, $irt, $id, $ctx) = @_;
 	my $rv = '';
@@ -155,16 +162,24 @@ sub _th_index_lite {
 	my $map = $mapping->{$mid_raw};
 	my $nr_c = scalar @{$map->[0]};
 	my $nr_s = 0;
+	my $level = $map->[4];
+	my $idx = $map->[3];
 	if (defined $irt) {
 		my $irt_map = $mapping->{$irt};
 		my $siblings = $irt_map->[0];
 		$nr_s = scalar(@$siblings) - 1;
-		$nr_s = 0 if $nr_s < 0;
 		$rv .= $pad . $irt_map->[1];
-		my $idx = $map->[3];
 		if ($idx > 0) {
 			my $prev = $siblings->[$idx - 1];
-			$rv .= $pad . $mapping->{$prev->messageid}->[1];
+			my $pmid = $prev->messageid;
+			if ($idx > 2) {
+				my $s = ($idx - 1). ' preceding siblings ...';
+				$rv .= pad_link($pmid, $level, $s);
+			} elsif ($idx == 2) {
+				my $ppmid = $siblings->[0]->messageid;
+				$rv .= $pad . $mapping->{$ppmid}->[1];
+			}
+			$rv .= $pad . $mapping->{$pmid}->[1];
 		}
 	}
 	my $s_s = nr_to_s($nr_s, 'sibling', 'siblings');
@@ -175,10 +190,25 @@ sub _th_index_lite {
 	$rv .= "<b>@ $this";
 	my $node = $map->[2];
 	if (my $child = $node->child) {
-		$rv .= $pad . $mapping->{$child->messageid}->[1];
+		my $cmid = $child->messageid;
+		$rv .= $pad . $mapping->{$cmid}->[1];
+		if ($nr_c > 2) {
+			my $s = ($nr_c - 1). ' more replies';
+			$rv .= pad_link($cmid, $level + 1, $s);
+		} elsif (my $cn = $child->next) {
+			$rv .= $pad . $mapping->{$cn->messageid}->[1];
+		}
 	}
 	if (my $next = $node->next) {
-		$rv .= $pad .  $mapping->{$next->messageid}->[1];
+		my $nmid = $next->messageid;
+		$rv .= $pad . $mapping->{$nmid}->[1];
+		my $nnext = $nr_s - $idx;
+		if ($nnext > 2) {
+			my $s = ($nnext - 1).' subsequent siblings';
+			$rv .= pad_link($nmid, $level, $s);
+		} elsif (my $nn = $next->next) {
+			$rv .= $pad . $mapping->{$nn->messageid}->[1];
+		}
 	}
 	$rv .= "<a\nhref=#e$id\nid=m$id>_<a> <a\nhref=#r$id\n>$s_s, $s_c</a>\n";
 }
@@ -203,7 +233,7 @@ sub pre_thread  {
 		$idx = scalar @$m;
 		push @$m, $node;
 	}
-	$mapping->{$node->messageid} = [ [], '', $node, $idx ];
+	$mapping->{$node->messageid} = [ [], '', $node, $idx, $level ];
 	skel_dump($ctx, $level, $node);
 }
 
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 08/13] www: reinstate old thread view as an option
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (6 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 07/13] view: show more nearby messages in flat thread view Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 09/13] view: fix up some HTML injection via Message-ID vectors Eric Wong
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This hybrid view is better than the old flat, but can
still fall down compared to the old threaded view in
some cases.
---
 lib/PublicInbox/View.pm | 109 ++++++++++++++++++++++++++++++++++++++++++++----
 lib/PublicInbox/WWW.pm  |  13 +++---
 t/plack.t               |   2 +-
 3 files changed, 109 insertions(+), 15 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 0b96bda..17d6de5 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -142,7 +142,19 @@ sub index_entry {
 		" / <a\nhref=\"${mhref}#R\">reply</a>";
 	if (my $pct = $ctx->{pct}) { # used by SearchView.pm
 		$rv .= " [relevance $pct->{$mid_raw}%]";
+	} elsif ($mapping) {
+		my $threaded = 'threaded';
+		my $flat = 'flat';
+		my $end = '';
+		if ($ctx->{flat}) {
+			$flat = "<b>$flat</b>";
+		} else {
+			$threaded = "<b>$threaded</b>";
+		}
+		$rv .= " [<a\nhref=\"${mhref}t/#u\">$threaded</a>";
+		$rv .= "|<a\nhref=\"${mhref}T/#u\">$flat</a>]";
 	}
+
 	$rv .= $more ? "\n\n" : "\n";
 }
 
@@ -150,7 +162,7 @@ sub pad_link ($$;$) {
 	my ($mid, $level, $s) = @_;
 	$s ||= '...';
 	my $id = id_compress($mid, 1);
-	(' 'x19).indent_for($level).th_pfx($level)."<a\nhref=#r$id>($s)<a>\n";
+	(' 'x19).indent_for($level).th_pfx($level)."<a\nhref=#r$id>($s)</a>\n";
 }
 
 sub _th_index_lite {
@@ -186,7 +198,7 @@ sub _th_index_lite {
 	my $s_c = nr_to_s($nr_c, 'reply', 'replies');
 	my $this = $map->[1];
 	$this =~ s!\n\z!</b>\n!s;
-	$this =~ s!<a\nhref.*a> !!s; # no point in duplicating subject
+	$this =~ s!<a\nhref.*</a> !!s; # no point in duplicating subject
 	$rv .= "<b>@ $this";
 	my $node = $map->[2];
 	if (my $child = $node->child) {
@@ -210,7 +222,7 @@ sub _th_index_lite {
 			$rv .= $pad . $mapping->{$nn->messageid}->[1];
 		}
 	}
-	$rv .= "<a\nhref=#e$id\nid=m$id>_<a> <a\nhref=#r$id\n>$s_s, $s_c</a>\n";
+	$rv .= "<a\nhref=#e$id\nid=m$id>_</a> <a\nhref=#r$id>$s_s, $s_c</a>\n";
 }
 
 sub walk_thread {
@@ -237,6 +249,51 @@ sub pre_thread  {
 	skel_dump($ctx, $level, $node);
 }
 
+sub thread_index_entry {
+	my ($ctx, $level, $mime) = @_;
+	my ($beg, $end) = thread_adj_level($ctx, $level);
+	$beg . '<pre>' . index_entry($mime, $ctx, 0) . '</pre>' . $end;
+}
+
+sub stream_thread ($$) {
+	my ($th, $ctx) = @_;
+	my $inbox = $ctx->{-inbox};
+	my $mime;
+	my @q = map { (0, $_) } $th->rootset;
+	my $level;
+	while (@q) {
+		$level = shift @q;
+		my $node = shift @q or next;
+		unshift @q, $level+1, $node->child, $level, $node->next;
+		$mime = $inbox->msg_by_mid($node->messageid) and last;
+	}
+	return missing_thread($ctx) unless $mime;
+
+	$mime = Email::MIME->new($mime);
+	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
+	$ctx->{-html_tip} = thread_index_entry($ctx, $level, $mime);
+	my $body = PublicInbox::WwwStream->new($ctx, sub {
+		return unless $ctx;
+		while (@q) {
+			$level = shift @q;
+			my $node = shift @q or next;
+			unshift @q, $level+1, $node->child, $level, $node->next;
+			my $mid = $node->messageid;
+			if ($mime = $inbox->msg_by_mid($mid)) {
+				$mime = Email::MIME->new($mime);
+				return thread_index_entry($ctx, $level, $mime);
+			} else {
+				return ghost_index_entry($ctx, $level, $mid);
+			}
+		}
+		my $ret = join('', thread_adj_level($ctx, 0));
+		$ret .= ${$ctx->{dst}}; # skel
+		$ctx = undef;
+		$ret;
+	});
+	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
+}
+
 sub thread_html {
 	my ($ctx) = @_;
 	my $mid = $ctx->{mid};
@@ -244,30 +301,34 @@ sub thread_html {
 	my $msgs = load_results($sres);
 	my $nr = $sres->{total};
 	return missing_thread($ctx) if $nr == 0;
-	my $skel = '</pre><hr /><pre>';
+	my $skel = '<hr /><pre>';
 	$skel .= $nr == 1 ? 'only message in thread' : 'end of thread';
 	$skel .= ", back to <a\nhref=\"../../\">index</a>";
 	$skel .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
 	$skel .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
 	$skel .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
+	$ctx->{-upfx} = '../../';
 	$ctx->{cur_level} = 0;
 	$ctx->{dst} = \$skel;
-	$ctx->{mapping} = {}; # mid -> [ reply count, from@date, node ];
 	$ctx->{prev_attr} = '';
 	$ctx->{prev_level} = 0;
 	$ctx->{root_anchor} = anchor_for($mid);
 	$ctx->{seen} = {};
+	$ctx->{mapping} = {};
 
-	walk_thread(thread_results($msgs), $ctx, *pre_thread);
+	my $th = thread_results($msgs);
+	walk_thread($th, $ctx, *pre_thread);
+	$skel .= '</pre>';
+	return stream_thread($th, $ctx) unless $ctx->{flat};
 
-	# lazy load the full message from mini_mime:
+	# flat display: lazy load the full message from mini_mime:
 	my $inbox = $ctx->{-inbox};
 	my $mime;
 	while ($mime = shift @$msgs) {
 		$mime = $inbox->msg_by_mid(mid_clean(mid_mime($mime))) and last;
 	}
+	return missing_thread($ctx) unless $mime;
 	$mime = Email::MIME->new($mime);
-	$ctx->{-upfx} = '../../';
 	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
 	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $ctx, scalar @$msgs);
 	$mime = undef;
@@ -282,7 +343,7 @@ sub thread_html {
 			return index_entry($mime, $ctx, scalar @$msgs);
 		}
 		$msgs = undef;
-		$skel .= '</pre>';
+		'</pre>'.$skel;
 	});
 	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
 }
@@ -846,4 +907,34 @@ sub emit_index_topics {
 	$opts{offset};
 }
 
+sub thread_adj_level {
+	my ($ctx, $level) = @_;
+
+	my $max = $ctx->{cur_level};
+	if ($level <= 0) {
+		return ('', '') if $max == 0; # flat output
+
+		# reset existing lists
+		my $beg = $max > 1 ? ('</ul></li>' x ($max - 1)) : '';
+		$ctx->{cur_level} = 0;
+		("$beg</ul>", '');
+	} elsif ($level == $max) { # continue existing list
+		qw(<li> </li>);
+	} elsif ($level < $max) {
+		my $beg = $max > 1 ? ('</ul></li>' x ($max - $level)) : '';
+		$ctx->{cur_level} = $level;
+		("$beg<li>", '</li>');
+	} else { # ($level > $max) # start a new level
+		$ctx->{cur_level} = $level;
+		my $beg = ($max ? '<li>' : '') . '<ul><li>';
+		($beg, '</li>');
+	}
+}
+
+sub ghost_index_entry {
+	my ($ctx, $level, $mid) = @_;
+	my ($beg, $end) = thread_adj_level($ctx,  $level);
+	$beg . '<pre>'. ghost_parent($ctx->{-upfx}, $mid) . '</pre>' . $end;
+}
+
 1;
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index da5c1d3..cbd3142 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -23,7 +23,7 @@ require PublicInbox::Git;
 use PublicInbox::GitHTTPBackend;
 our $INBOX_RE = qr!\A/([\w\.\-]+)!;
 our $MID_RE = qr!([^/]+)!;
-our $END_RE = qr!(t/|t\.mbox(?:\.gz)?|t\.atom|raw|)!;
+our $END_RE = qr!(T/|t/|t\.mbox(?:\.gz)?|t\.atom|raw|)!;
 our $ATTACH_RE = qr!(\d[\.\d]*)-([[:alnum:]][\w\.-]+[[:alnum:]])!i;
 
 sub new {
@@ -92,9 +92,10 @@ sub call {
 		invalid_inbox_mid($self, $ctx, $1, $2) ||
 			get_attach($ctx, $idx, $fn);
 	# in case people leave off the trailing slash:
-	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/(?:T|T/|t)\z!o) {
-		my ($inbox, $mid) = ($1, $2);
-		r301($ctx, $inbox, $mid, 't/#u');
+	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/(T|t)\z!o) {
+		my ($inbox, $mid, $suffix) = ($1, $2, $3);
+		$suffix .= $suffix =~ /\A[tT]\z/ ? '/#u' : '/';
+		r301($ctx, $inbox, $mid, $suffix);
 
 	} elsif ($path_info =~ m!$INBOX_RE/$MID_RE/R/?\z!o) {
 		my ($inbox, $mid) = ($1, $2);
@@ -241,8 +242,9 @@ sub get_mid_html {
 
 # /$INBOX/$MESSAGE_ID/t/
 sub get_thread {
-	my ($ctx) = @_;
+	my ($ctx, $flat) = @_;
 	searcher($ctx) or return need_search($ctx);
+	$ctx->{flat} = $flat;
 	require PublicInbox::View;
 	PublicInbox::View::thread_html($ctx);
 }
@@ -416,6 +418,7 @@ sub msg_page {
 	my $ret;
 	$ret = invalid_inbox_mid($self, $ctx, $inbox, $mid) and return $ret;
 	'' eq $e and return get_mid_html($ctx);
+	'T/' eq $e and return get_thread($ctx, 1);
 	't/' eq $e and return get_thread($ctx);
 	't.atom' eq $e and return get_thread_atom($ctx);
 	't.mbox' eq $e and return get_thread_mbox($ctx);
diff --git a/t/plack.t b/t/plack.t
index 209c6f9..a4f3245 100644
--- a/t/plack.t
+++ b/t/plack.t
@@ -101,7 +101,7 @@ EOF
 			my $res = $cb->(GET($u));
 			is(301, $res->code, "redirect for missing /");
 			my $location = $res->header('Location');
-			like($location, qr!/t/#u\z!,
+			like($location, qr!/\Q$t\E/#u\z!,
 				'redirected with missing /');
 		});
 	}
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 09/13] view: fix up some HTML injection via Message-ID vectors
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (7 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 08/13] www: reinstate old thread view as an option Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30 19:03   ` [PATCH 14/13] view: fix permalink and raw links at the top Eric Wong
  2016-06-30  9:21 ` [PATCH 10/13] view: default to flat/hybrid thread display Eric Wong
                   ` (3 subsequent siblings)
  12 siblings, 1 reply; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

Oops, these were only introduced during the hybrid flat thread
view reworking and never deployed.
---
 lib/PublicInbox/View.pm | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 17d6de5..44130b9 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -103,7 +103,7 @@ sub index_entry {
 	my $subj = $hdr->header('Subject');
 
 	my $mid_raw = mid_clean(mid_mime($mime));
-	my $id = id_compress($mid_raw);
+	my $id = id_compress($mid_raw, 1);
 	my $id_m = 'm'.$id;
 	my $mid = PublicInbox::Hval->new_msgid($mid_raw);
 
@@ -119,20 +119,23 @@ sub index_entry {
 		my $dst = _hdr_names($hdr, $f);
 		push @tocc, "$f: $dst" if $dst ne '';
 	}
-	my $mapping = $ctx->{mapping};
 	$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC";
 	my $upfx = $ctx->{-upfx};
-	$rv .= qq{ (<a\nhref="$upfx$mid_raw/">permalink</a> / };
-	$rv .= qq{<a\nhref="$upfx$mid_raw/raw">raw</a>)\n};
+	my $mhref = $upfx . $mid->as_href . '/';
+	$rv .= qq{ (<a\nhref="$mhref/">permalink</a> / };
+	$rv .= qq{<a\nhref="$mhref/raw">raw</a>)\n};
 	$rv .= '  '.join('; +', @tocc) . "\n" if @tocc;
+
+	my $mapping = $ctx->{mapping};
 	if (!$mapping && $irt) {
-		$rv .= qq(In-Reply-To: &lt;<a\nhref="$upfx$irt/">$irt</a>&gt;\n)
+		my $mirt = PublicInbox::Hval->msgid($irt);
+		my $href = $upfx . $mirt->as_href . '/';
+		my $html = $mirt->as_html;
+		$rv .= qq(In-Reply-To: &lt;<a\nhref="$href/">$html</a>&gt;\n)
 	}
 	$rv .= "\n";
 
 	# scan through all parts, looking for displayable text
-	my $href = $mid->as_href;
-	my $mhref = $ctx->{-upfx}.$href.'/';
 	msg_iter($mime, sub { $rv .= add_text_body($mhref, $_[0]) });
 
 	# add the footer
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 10/13] view: default to flat/hybrid thread display
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (8 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 09/13] view: fix up some HTML injection via Message-ID vectors Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 11/13] view: show thread size when linking to summary Eric Wong
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This is friendlier for people on small screens and usually
eliminates the need to scroll horizontally.
---
 lib/PublicInbox/View.pm | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 44130b9..22d7250 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -154,8 +154,9 @@ sub index_entry {
 		} else {
 			$threaded = "<b>$threaded</b>";
 		}
-		$rv .= " [<a\nhref=\"${mhref}t/#u\">$threaded</a>";
-		$rv .= "|<a\nhref=\"${mhref}T/#u\">$flat</a>]";
+		$rv .= " / [<a\nhref=\"${mhref}T/#u\">$flat</a>";
+		$rv .= "|<a\nhref=\"${mhref}t/#u\">$threaded</a>]";
+		$rv .= " / <a\nhref=#r$id>thread overview</a>";
 	}
 
 	$rv .= $more ? "\n\n" : "\n";
@@ -498,7 +499,7 @@ sub thread_skel {
 	my $mid = mid_clean($hdr->header_raw('Message-ID'));
 	my $sres = $srch->get_thread($mid);
 	my $nr = $sres->{total};
-	my $expand = qq(<a\nhref="${tpfx}t/#u">expand</a> ) .
+	my $expand = qq(<a\nhref="${tpfx}T/#u">expand</a> ) .
 			qq(/ <a\nhref="${tpfx}t.mbox.gz">mbox.gz</a> ) .
 			qq(/ <a\nhref="${tpfx}t.atom">Atom feed</a>);
 
@@ -869,7 +870,7 @@ sub emit_topics {
 		}
 
 		$subj = PublicInbox::Hval->new($subj)->as_html;
-		$cur->[1] .= "<a\nhref=\"$mid/t/#u\"><b>$subj</b></a>\n";
+		$cur->[1] .= "<a\nhref=\"$mid/T/#u\"><b>$subj</b></a>\n";
 		$ts = fmt_ts($ts);
 		my $attr = " $ts UTC";
 
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 11/13] view: show thread size when linking to summary
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (9 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 10/13] view: default to flat/hybrid thread display Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 12/13] view: fixup bad reference to new_msgid Eric Wong
  2016-06-30  9:21 ` [PATCH 13/13] www_stream: add response wrapper sub Eric Wong
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This should give readers a better idea of what to expect.
---
 lib/PublicInbox/View.pm | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 22d7250..fac53eb 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -156,7 +156,7 @@ sub index_entry {
 		}
 		$rv .= " / [<a\nhref=\"${mhref}T/#u\">$flat</a>";
 		$rv .= "|<a\nhref=\"${mhref}t/#u\">$threaded</a>]";
-		$rv .= " / <a\nhref=#r$id>thread overview</a>";
+		$rv .= " / <a\nhref=#r$id>$ctx->{s_nr}</a>";
 	}
 
 	$rv .= $more ? "\n\n" : "\n";
@@ -226,7 +226,8 @@ sub _th_index_lite {
 			$rv .= $pad . $mapping->{$nn->messageid}->[1];
 		}
 	}
-	$rv .= "<a\nhref=#e$id\nid=m$id>_</a> <a\nhref=#r$id>$s_s, $s_c</a>\n";
+	$rv .= "<a\nhref=#e$id\nid=m$id>_</a> ";
+	$rv .= "<a\nhref=#r$id>$s_s, $s_c; $ctx->{s_nr}</a>\n";
 }
 
 sub walk_thread {
@@ -319,6 +320,7 @@ sub thread_html {
 	$ctx->{root_anchor} = anchor_for($mid);
 	$ctx->{seen} = {};
 	$ctx->{mapping} = {};
+	$ctx->{s_nr} = "$nr+ messages in thread";
 
 	my $th = thread_results($msgs);
 	walk_thread($th, $ctx, *pre_thread);
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 12/13] view: fixup bad reference to new_msgid
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (10 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 11/13] view: show thread size when linking to summary Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  2016-06-30  9:21 ` [PATCH 13/13] www_stream: add response wrapper sub Eric Wong
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

Oops, this endpoint needs testing :x
---
 lib/PublicInbox/View.pm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index db2bd20..b4f80d1 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -129,7 +129,7 @@ sub index_entry {
 
 	my $mapping = $ctx->{mapping};
 	if (!$mapping && $irt) {
-		my $mirt = PublicInbox::Hval->msgid($irt);
+		my $mirt = PublicInbox::Hval->new_msgid($irt);
 		my $href = $upfx . $mirt->as_href . '/';
 		my $html = $mirt->as_html;
 		$rv .= qq(In-Reply-To: &lt;<a\nhref="$href/">$html</a>&gt;\n)
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 13/13] www_stream: add response wrapper sub
  2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
                   ` (11 preceding siblings ...)
  2016-06-30  9:21 ` [PATCH 12/13] view: fixup bad reference to new_msgid Eric Wong
@ 2016-06-30  9:21 ` Eric Wong
  12 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30  9:21 UTC (permalink / raw)
  To: meta

This encapsulates an entire PSGI response array, hopefully
making it easier to generate responses and avoid typos when
setting the Content-Type.
---
 lib/PublicInbox/Feed.pm       | 3 +--
 lib/PublicInbox/SearchView.pm | 4 +---
 lib/PublicInbox/View.pm       | 8 +++-----
 lib/PublicInbox/WWW.pm        | 3 +--
 lib/PublicInbox/WwwStream.pm  | 6 ++++++
 t/view.t                      | 3 ++-
 6 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index c16c417..2f141c4 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -48,7 +48,7 @@ sub new_html {
 	}
 	$ctx->{-html_tip} = '<pre>';
 	$ctx->{-upfx} = '';
-	my $res = PublicInbox::WwwStream->new($ctx, sub {
+	PublicInbox::WwwStream->response($ctx, 200, sub {
 		while (my $path = shift @paths) {
 			my $m = do_cat_mail($ctx->{-inbox}, $path) or next;
 			my $more = scalar @paths;
@@ -58,7 +58,6 @@ sub new_html {
 		}
 		undef;
 	});
-	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $res ]
 }
 
 # private subs
diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm
index 4af6cad..30a310c 100644
--- a/lib/PublicInbox/SearchView.pm
+++ b/lib/PublicInbox/SearchView.pm
@@ -55,9 +55,7 @@ sub sres_top_html {
 			$cb = mset_summary($ctx, $mset, $q);
 		}
 	}
-
-	[ $code, ['Content-Type', 'text/html; charset=UTF-8'],
-		PublicInbox::WwwStream->new($ctx, $cb) ];
+	PublicInbox::WwwStream->response($ctx, $code, $cb);
 }
 
 # display non-threaded search results similar to what users expect from
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index b4f80d1..27dd155 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -27,7 +27,7 @@ sub msg_html {
 	my ($ctx, $mime, $footer) = @_;
 	my $hdr = $mime->header_obj;
 	my $tip = _msg_html_prepare($hdr, $ctx);
-	PublicInbox::WwwStream->new($ctx, sub {
+	PublicInbox::WwwStream->response($ctx, 200, sub {
 		my ($nr, undef) = @_;
 		if ($nr == 1) {
 			$tip . multipart_text_as_html($mime, '') .
@@ -278,7 +278,7 @@ sub stream_thread ($$) {
 	$mime = Email::MIME->new($mime);
 	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
 	$ctx->{-html_tip} = thread_index_entry($ctx, $level, $mime);
-	my $body = PublicInbox::WwwStream->new($ctx, sub {
+	PublicInbox::WwwStream->response($ctx, 200, sub {
 		return unless $ctx;
 		while (@q) {
 			$level = shift @q;
@@ -297,7 +297,6 @@ sub stream_thread ($$) {
 		$ctx = undef;
 		$ret;
 	});
-	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
 }
 
 sub thread_html {
@@ -339,7 +338,7 @@ sub thread_html {
 	$ctx->{-title_html} = ascii_html($mime->header('Subject'));
 	$ctx->{-html_tip} = '<pre>'.index_entry($mime, $ctx, scalar @$msgs);
 	$mime = undef;
-	my $body = PublicInbox::WwwStream->new($ctx, sub {
+	PublicInbox::WwwStream->response($ctx, 200, sub {
 		return unless $msgs;
 		while ($mime = shift @$msgs) {
 			$mid = mid_clean(mid_mime($mime));
@@ -352,7 +351,6 @@ sub thread_html {
 		$msgs = undef;
 		'</pre>'.$skel;
 	});
-	[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
 }
 
 sub multipart_text_as_html {
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index cbd3142..c4509bd 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -236,8 +236,7 @@ sub get_mid_html {
 	require Email::MIME;
 	my $mime = Email::MIME->new($x);
 	searcher($ctx);
-	[ 200, [ 'Content-Type' => 'text/html; charset=UTF-8' ],
-	  PublicInbox::View::msg_html($ctx, $mime, $foot) ];
+	PublicInbox::View::msg_html($ctx, $mime, $foot);
 }
 
 # /$INBOX/$MESSAGE_ID/t/
diff --git a/lib/PublicInbox/WwwStream.pm b/lib/PublicInbox/WwwStream.pm
index d2bf318..6de1b31 100644
--- a/lib/PublicInbox/WwwStream.pm
+++ b/lib/PublicInbox/WwwStream.pm
@@ -14,6 +14,12 @@ sub new {
 	bless { nr => 0, cb => $cb, ctx => $ctx }, $class;
 }
 
+sub response {
+	my ($class, $ctx, $code, $cb) = @_;
+	[ $code, [ 'Content-Type', 'text/html; charset=UTF-8' ],
+	  $class->new($ctx, $cb) ]
+}
+
 sub _html_top ($) {
 	my ($self) = @_;
 	my $ctx = $self->{ctx};
diff --git a/t/view.t b/t/view.t
index 8a898fe..4fdd151 100644
--- a/t/view.t
+++ b/t/view.t
@@ -34,7 +34,8 @@ sub msg_html ($) {
 	my ($mime) = @_;
 
 	my $s = '';
-	my $body = PublicInbox::View::msg_html($ctx, $mime);
+	my $r = PublicInbox::View::msg_html($ctx, $mime);
+	my $body = $r->[2];
 	while (defined(my $buf = $body->getline)) {
 		$s .= $buf;
 	}
-- 
EW


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [PATCH 14/13] view: fix permalink and raw links at the top
  2016-06-30  9:21 ` [PATCH 09/13] view: fix up some HTML injection via Message-ID vectors Eric Wong
@ 2016-06-30 19:03   ` Eric Wong
  0 siblings, 0 replies; 15+ messages in thread
From: Eric Wong @ 2016-06-30 19:03 UTC (permalink / raw)
  To: meta

Oops :x  I really need to whip check-inbox.perl into
shape or at least start running it, again.

Fixes: e29518088b3f ("view: fix up some HTML injection via Message-ID vectors")
---
 lib/PublicInbox/View.pm | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 11d8dd5..140cfee 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -138,8 +138,8 @@ sub index_entry {
 	$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC";
 	my $upfx = $ctx->{-upfx};
 	my $mhref = $upfx . $mid->as_href . '/';
-	$rv .= qq{ (<a\nhref="$mhref/">permalink</a> / };
-	$rv .= qq{<a\nhref="$mhref/raw">raw</a>)\n};
+	$rv .= qq{ (<a\nhref="$mhref">permalink</a> / };
+	$rv .= qq{<a\nhref="${mhref}raw">raw</a>)\n};
 	$rv .= '  '.join('; +', @tocc) . "\n" if @tocc;
 
 	my $mapping = $ctx->{mapping};

^ permalink raw reply related	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2016-06-30 19:03 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2016-06-30  9:21 [PATCH 0/13] www: hybrid flat+thread conversation view Eric Wong
2016-06-30  9:21 ` [PATCH 01/13] www: implement " Eric Wong
2016-06-30  9:21 ` [PATCH 02/13] www: use WwwStream for dumping thread and search views Eric Wong
2016-06-30  9:21 ` [PATCH 03/13] view: show thread context in the thread-aware flat view Eric Wong
2016-06-30  9:21 ` [PATCH 04/13] view: merge $state hash with existing $ctx Eric Wong
2016-06-30  9:21 ` [PATCH 05/13] feed: add $INBOX/new.html endpoint Eric Wong
2016-06-30  9:21 ` [PATCH 06/13] view: tweak thread/index header slightly Eric Wong
2016-06-30  9:21 ` [PATCH 07/13] view: show more nearby messages in flat thread view Eric Wong
2016-06-30  9:21 ` [PATCH 08/13] www: reinstate old thread view as an option Eric Wong
2016-06-30  9:21 ` [PATCH 09/13] view: fix up some HTML injection via Message-ID vectors Eric Wong
2016-06-30 19:03   ` [PATCH 14/13] view: fix permalink and raw links at the top Eric Wong
2016-06-30  9:21 ` [PATCH 10/13] view: default to flat/hybrid thread display Eric Wong
2016-06-30  9:21 ` [PATCH 11/13] view: show thread size when linking to summary Eric Wong
2016-06-30  9:21 ` [PATCH 12/13] view: fixup bad reference to new_msgid Eric Wong
2016-06-30  9:21 ` [PATCH 13/13] www_stream: add response wrapper sub Eric Wong

Code repositories for project(s) associated with this public inbox

	https://80x24.org/public-inbox.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).