From 5c5dfb7d5558ff248477fa57aef08e2a8bbd9800 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:13 +0000 Subject: www: implement hybrid flat+thread conversation view 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. --- lib/PublicInbox/SearchView.pm | 42 +++---- lib/PublicInbox/View.pm | 248 ++++++++++++++++-------------------------- lib/PublicInbox/WWW.pm | 9 +- 3 files changed, 114 insertions(+), 185 deletions(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm index ae875bf7..fbef4116 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). ''); $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 30339cd4..65788dbe 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
 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 = "$subj";
-	$subj = "$subj" if $root_anchor eq $id;
+	$subj = ''.ascii_html($subj).'';
+	$subj = "$subj" if $root_anchor eq $id_m;
 
 	my $ts = _msg_date($hdr);
-	my $rv = "";
-	$rv .= "$subj\n";
-	my $txt = "${path}$href/raw";
-	my $fh = $state->{fh};
+	my $rv = "
# ";
+	$rv .= $subj;
+	my $mhref = $path.$href.'/';
 	my $from = _hdr_names($hdr, 'From');
-	$rv .= "- $from @ $ts UTC (raw)\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 .= "\npermalink" .
+		" / raw / ";
+	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 .= " parent";
+		$nr_s = ($mapping->{$irt} || 0) - 1;
+		$nr_s = 0 if $nr_s < 0;
+		$irt = anchor_for($irt);
+		$rv .= "#parent,";
+	} else {
+		$rv .= 'root message:';
 	}
+	$nr_s = nr_to_s($nr_s, 'sibling', 'siblings');
+	$nr_c = nr_to_s($nr_c, 'reply', 'replies');
+	$rv .= " $nr_s, $nr_c";
+	$rv .= " / reply";
+
 	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 = "$flat";
-			$end = "\n"; # for lynx
-		} else {
-			$threaded = "$threaded";
-		}
-		$rv .= " [$threaded";
-		$rv .= "|$flat]$end";
 	}
-	$fh->write($rv .= '
'); + $state->{fh}->write($rv .= "\n
"); # '\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( - ('' x ($max - 1)) . ''); - } - } + 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 = ""; - $next .= $final_anchor == 1 ? 'only message in' : 'end of'; - $next .= " thread, back to index"; - $next .= "\ndownload thread: "; + my $next = @$msgs == 1 ? 'only message in thread' : 'end of thread'; + $next .= ", back to index"; + $next .= "\n$nr+ messages in thread: (download: "; $next .= "mbox.gz"; - $next .= " / follow: Atom feed"; + $next .= " / follow: Atom feed)\n"; + $next .= $skel; $fh->write('
' . $next . "\n\n".
 			$foot .  '
'); $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 { ""); } -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: <$html>]}; } -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 ? ('' x ($max - 1)) : ''; - $state->{fh}->write($x . ''); - $state->{cur_level} = 0; - return ''; - } - if ($level == $max) { # continue existing list - $state->{fh}->write('
  • '); - } elsif ($level < $max) { - my $x = $max > 1 ? ('
  • ' x ($max - $level)) : ''; - $state->{fh}->write($x .= '
  • '); - $state->{cur_level} = $level; - } else { # ($level > $max) # start a new level - $state->{cur_level} = $level; - $state->{fh}->write(($max ? '
  • ' : '') . '
    • '); - } - '
    • '; -} - -sub ghost_flush { - my ($state, $upfx, $mid, $level) = @_; - my $end = '
      '. ghost_parent($upfx, $mid) . '
      '; - $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"; + 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"; $$dst .= defined($s) ? "$s $f\n" : "$f\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{<$html>\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{<$html>\n}; } } diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm index d6b07bf7..984268e9 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); -- cgit v1.2.3-24-ge0c7 From 5422a844b7384c32b3532d128e15e0b50d24435b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:14 +0000 Subject: www: use WwwStream for dumping thread and search views 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm index 8e233061..36802fa1 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 fbef4116..488822e6 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) . '
      ';
      +	ctx_prepare($q, $ctx);
      +	my $cb;
       	if ($err) {
       		$code = 400;
      -		$res .= err_txt($ctx, $err) . "

      " . foot($ctx);
      +		$ctx->{-html_tip} = '
      '.err_txt($ctx, $err).'

      '; + $cb = *noop; } elsif ($total == 0) { $code = 404; - $res .= "\n\n[No results found]

      ".foot($ctx);
      +		$ctx->{-html_tip} = "
      \n[No results found]

      "; + $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 .= '
      ' . search_nav_bot($mset, $q) . - "\n\n" . foot($ctx); } - $res .= ""; - [$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 . "\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: $err\n" . + "\nBad query: $err\n" . qq{See $u for Xapian query syntax}; } sub search_nav_top { my ($mset, $q) = @_; - my $rv = "Search results ordered by ["; + my $rv = "
      Search results ordered by [";
       	if ($q->{r}) {
       		my $d = $q->qs_html(r => 0);
       		$rv .= qq{date|relevance};
      @@ -122,7 +128,7 @@ sub search_nav_bot {
       	my $o = $q->{o};
       	my $end = $o + $nr;
       	my $beg = $o + 1;
      -	my $rv = "
      Results $beg-$end of $total";
      +	my $rv = "

      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{prev};
       	}
      -	$rv;
      +	$rv .= '
      '; } -sub tdump { - my ($cb, $res, $mset, $q, $ctx) = @_; - my $fh = $cb->([200, ['Content-Type'=>'text/html; charset=UTF-8']]); - $fh->write($res .= '
      '); +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). "
      ";
      +	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). '
      '); - - $fh->close; -} - -sub foot { - my ($ctx) = @_; - my $foot = $ctx->{footer} || ''; - qq{Back to index.\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"; + }; } -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 = '' . PublicInbox::Hval::STYLE . - "$qh - search results" . - qq{! . - qq{} . - qq{}; - - $res .= qq{} 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{} if $q->{r}; if (my $x = $q->{x}) { $x = ascii_html($x); - $res .= qq{}; + $extra .= qq{}; } - - $res .= qq{}; + $ctx->{-extra_form_html} = $extra; } sub adump { diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 65788dbe..a774febd 100644 --- a/lib/PublicInbox/View.pm +++ b/lib/PublicInbox/View.pm @@ -97,8 +97,7 @@ sub nr_to_s ($$$) { # this is already inside a
       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 = "$subj" if $root_anchor eq $id_m;
       
       	my $ts = _msg_date($hdr);
      -	my $rv = "
      # ";
      +	my $rv = "# ";
       	$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
      "); # '\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 = '

      ';
      +	$skel .= $nr == 1 ? 'only message in thread' : 'end of thread';
      +	$skel .= ", back to index";
      +	$skel .= "\n$nr+ messages in thread: (download: ";
      +	$skel .= "mbox.gz";
      +	$skel .= " / follow: Atom feed)\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 index";
      -	$next .= "\n$nr+ messages in thread: (download: ";
      -	$next .= "mbox.gz";
      -	$next .= " / follow: Atom feed)\n";
      -	$next .= $skel;
      -	$fh->write('
      ' . $next . "\n\n".
      -			$foot .  '
      '); - $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} = '
      '.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 .= "
      "; + }); + [ 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("$s". - qq{! . - PublicInbox::Hval::STYLE . - ""); -} - 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: <$html>]}; } -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 984268e9..196486f2 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 34f32c0b..d2bf318b 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 = "$desc (Atom feed)"; 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{
      $top} .
      -			  qq{ } .
      +			  qq{ } .
      +			  $extra .
       			  qq{} .
       			  q{
      } } else { @@ -35,7 +45,7 @@ sub _html_top ($) { "" . PublicInbox::Hval::STYLE . - "$top"; + "". $top . $tip; } sub _html_end { -- cgit v1.2.3-24-ge0c7 From 1e0b530449910eb35561e068de7e4084175b78e6 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:15 +0000 Subject: view: show thread context in the thread-aware flat view 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm index 36802fa1..73986e83 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 488822e6..8771d5d3 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). "
      ";
       	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 a774febd..eac541d2 100644
      --- a/lib/PublicInbox/View.pm
      +++ b/lib/PublicInbox/View.pm
      @@ -97,7 +97,7 @@ sub nr_to_s ($$$) {
       
       # this is already inside a 
       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 = ''.ascii_html($subj).'';
      -	$subj = "$subj" if $root_anchor eq $id_m;
      -
      -	my $ts = _msg_date($hdr);
      -	my $rv = "# ";
      -	$rv .= $subj;
      -	my $mhref = $path.$href.'/';
      -	my $from = _hdr_names($hdr, 'From');
      -	$rv .= "\n- $from @ $ts UTC\n";
      +	my $rv = ''.ascii_html($subj).'';
      +	$rv = "$rv" 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 .= "\npermalink" .
      -		" / raw / ";
      -	my $mapping = $state->{mapping};
      -	my $nr_c = $mapping->{$mid_raw} || 0;
      +
      +	# add the footer
      +	$rv .= "\n^ ".
      +		"permalink" .
      +		" / raw" .
      +		" / reply";
      +	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 .= "#parent,";
      -	} 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 .= " $nr_s, $nr_c";
      -	$rv .= " / reply";
      -
      -	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!\n!s;
      +	$this =~ s! !!s; # no point in duplicating subject
      +	$rv .= "@ $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 .= ".\t\t\t";
      +	$rv .= "($s_s, $s_c / ";
      +	my $upfx = $state->{ctx}->{-upfx};
      +	$rv .= qq{permalink / };
      +	$rv .= qq{raw)\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} = '
      '.index_entry($mime, $state);
      +	$ctx->{-html_tip} = '
      '.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 .= "
      "; + $skel .= '
      '; }); [ 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". + $$dst .= $d; + $$dst .= "". "$attr [this message]\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 $f\n" : "$f\n"; + if ($mapping) { + my $map = $mapping->{$mid}; $id = id_compress($mid, 1); $m = '#m'.$id; + $map->[1] = "$d$end"; $id = "\nid=r".$id; } else { - $m = $state->{upfx}.$m->as_href.'/'; + $m = $state->{ctx}->{-upfx}.$m->as_href.'/'; } - $$dst .= "$pfx"; - $$dst .= defined($s) ? "$s $f\n" : "$f\n"; + $$dst .= $d . "" . $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{<$html>\n}; + $d .= qq{<$html>\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{<$html>\n}; } - $mid = PublicInbox::Hval->new_msgid($mid); - my $href = $upfx . $mid->as_href . '/'; - my $html = $mid->as_html; - $$dst .= qq{<$html>\n}; + $$dst .= $d; } } -- cgit v1.2.3-24-ge0c7 From 0c28e937c80f3134898bc41f10bc9de350851944 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:17 +0000 Subject: view: merge $state hash with existing $ctx 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm index 73986e83..ddc1e3c1 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!next!; diff --git a/lib/PublicInbox/SearchView.pm b/lib/PublicInbox/SearchView.pm index 8771d5d3..4af6cadb 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). "
      ";
       	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
      "; diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index eac541d2..0b47c899 100644 --- a/lib/PublicInbox/View.pm +++ b/lib/PublicInbox/View.pm @@ -97,8 +97,7 @@ sub nr_to_s ($$$) { # this is already inside a
       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 = ''.ascii_html($subj).'';
       	$rv = "$rv" 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 {
       		"permalink" .
       		" / raw" .
       		" / reply";
      -	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 .= ".\t\t\t";
       	$rv .= "($s_s, $s_c / ";
      -	my $upfx = $state->{ctx}->{-upfx};
      +	my $upfx = $ctx->{-upfx};
       	$rv .= qq{permalink / };
       	$rv .= qq{raw)\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$nr+ messages in thread: (download: ";
       	$skel .= "mbox.gz";
       	$skel .= " / follow: Atom feed)\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} = '
      '.index_entry($mime, $state, scalar @$msgs);
      +	$ctx->{-html_tip} = '
      '.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 .= '
      '; @@ -427,16 +422,12 @@ sub thread_skel { $$dst .= qq! / [top])\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 .= "". "$attr [this message]\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 $f\n" : "$f\n"; if ($mapping) { my $map = $mapping->{$mid}; @@ -671,19 +662,19 @@ sub _skel_header { $map->[1] = "$d$end"; $id = "\nid=r".$id; } else { - $m = $state->{ctx}->{-upfx}.$m->as_href.'/'; + $m = $ctx->{-upfx}.$m->as_href.'/'; } $$dst .= $d . "" . $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]
      ") 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}; } -- cgit v1.2.3-24-ge0c7 From 5bc94392bd67d8e2a919e357d569751b9295475a Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:18 +0000 Subject: feed: add $INBOX/new.html endpoint 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm index ddc1e3c1..c16c417a 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} = '
      ';
      +	$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 .= '
      ' 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 196486f2..da5c1d30 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) = @_; -- cgit v1.2.3-24-ge0c7 From d4560fca4d1dddafd21a78c793679a1b19cd4db4 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:19 +0000 Subject: view: tweak thread/index header slightly 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 0b47c899..9393d443 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{ (permalink / }; + $rv .= qq{raw)\n}; $rv .= ' '.join('; +', @tocc) . "\n" if @tocc; + if (!$mapping && $irt) { + $rv .= qq(In-Reply-To: <$irt>\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 .= ".\t\t\t"; - $rv .= "($s_s, $s_c / "; - my $upfx = $ctx->{-upfx}; - $rv .= qq{permalink / }; - $rv .= qq{raw)\n}; + $rv .= "_ $s_s, $s_c\n"; } sub walk_thread { -- cgit v1.2.3-24-ge0c7 From dfaff07b1578d5b32aae3225cb09c6a3f2177896 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:20 +0000 Subject: view: show more nearby messages in flat thread view Context is important, but so is conserving precious screen space. Decisions :< --- lib/PublicInbox/View.pm | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 9393d443..0b96bda8 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)."($s)\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 .= "@ $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 .= "_ $s_s, $s_c\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); } -- cgit v1.2.3-24-ge0c7 From b30bcf12bdc0c3658e5e31c02b05c3609da04e7f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 02:35:21 +0000 Subject: www: reinstate old thread view as an option 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 +++--- 2 files changed, 108 insertions(+), 14 deletions(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 0b96bda8..17d6de56 100644 --- a/lib/PublicInbox/View.pm +++ b/lib/PublicInbox/View.pm @@ -142,7 +142,19 @@ sub index_entry { " / reply"; 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 = "$flat"; + } else { + $threaded = "$threaded"; + } + $rv .= " [$threaded"; + $rv .= "|$flat]"; } + $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)."($s)\n"; + (' 'x19).indent_for($level).th_pfx($level)."($s)\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!\n!s; - $this =~ s! !!s; # no point in duplicating subject + $this =~ s! !!s; # no point in duplicating subject $rv .= "@ $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 .= "_ $s_s, $s_c\n"; + $rv .= "_ $s_s, $s_c\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 . '
      ' . index_entry($mime, $ctx, 0) . '
      ' . $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 = '

      ';
      +	my $skel = '
      ';
       	$skel .= $nr == 1 ? 'only message in thread' : 'end of thread';
       	$skel .= ", back to index";
       	$skel .= "\n$nr+ messages in thread: (download: ";
       	$skel .= "mbox.gz";
       	$skel .= " / follow: Atom feed)\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 .= '
      '; + 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} = '
      '.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 .= '
      '; + '
      '.$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 ? ('
  • ' x ($max - 1)) : ''; + $ctx->{cur_level} = 0; + ("$beg", ''); + } elsif ($level == $max) { # continue existing list + qw(
  • ); + } elsif ($level < $max) { + my $beg = $max > 1 ? ('' x ($max - $level)) : ''; + $ctx->{cur_level} = $level; + ("$beg
  • ", '
  • '); + } else { # ($level > $max) # start a new level + $ctx->{cur_level} = $level; + my $beg = ($max ? '
  • ' : '') . '
    • '; + ($beg, '
    • '); + } +} + +sub ghost_index_entry { + my ($ctx, $level, $mid) = @_; + my ($beg, $end) = thread_adj_level($ctx, $level); + $beg . '
      '. ghost_parent($ctx->{-upfx}, $mid) . '
      ' . $end; +} + 1; diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm index da5c1d30..cbd3142d 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); -- cgit v1.2.3-24-ge0c7 From e29518088b3f4a4759280f76dd5416c376ee683e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 07:41:41 +0000 Subject: view: fix up some HTML injection via Message-ID vectors 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 17d6de56..44130b90 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{ (permalink / }; - $rv .= qq{raw)\n}; + my $mhref = $upfx . $mid->as_href . '/'; + $rv .= qq{ (permalink / }; + $rv .= qq{raw)\n}; $rv .= ' '.join('; +', @tocc) . "\n" if @tocc; + + my $mapping = $ctx->{mapping}; if (!$mapping && $irt) { - $rv .= qq(In-Reply-To: <$irt>\n) + my $mirt = PublicInbox::Hval->msgid($irt); + my $href = $upfx . $mirt->as_href . '/'; + my $html = $mirt->as_html; + $rv .= qq(In-Reply-To: <$html>\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 -- cgit v1.2.3-24-ge0c7 From 26fed2421f96f2a3f15dea1b730a3f3f6634f01c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 08:01:39 +0000 Subject: view: default to flat/hybrid thread display 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(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 44130b90..22d72500 100644 --- a/lib/PublicInbox/View.pm +++ b/lib/PublicInbox/View.pm @@ -154,8 +154,9 @@ sub index_entry { } else { $threaded = "$threaded"; } - $rv .= " [$threaded"; - $rv .= "|$flat]"; + $rv .= " / [$flat"; + $rv .= "|$threaded]"; + $rv .= " / thread overview"; } $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(expand ) . + my $expand = qq(expand ) . qq(/ mbox.gz ) . qq(/ Atom feed); @@ -869,7 +870,7 @@ sub emit_topics { } $subj = PublicInbox::Hval->new($subj)->as_html; - $cur->[1] .= "$subj\n"; + $cur->[1] .= "$subj\n"; $ts = fmt_ts($ts); my $attr = " $ts UTC"; -- cgit v1.2.3-24-ge0c7 From 8e52aa5d5a9186a1c90bc77e444d77d6130c8779 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Jun 2016 08:39:35 +0000 Subject: view: show thread size when linking to summary This should give readers a better idea of what to expect. --- lib/PublicInbox/View.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm index 22d72500..fac53eb9 100644 --- a/lib/PublicInbox/View.pm +++ b/lib/PublicInbox/View.pm @@ -156,7 +156,7 @@ sub index_entry { } $rv .= " / [$flat"; $rv .= "|$threaded]"; - $rv .= " / thread overview"; + $rv .= " / $ctx->{s_nr}"; } $rv .= $more ? "\n\n" : "\n"; @@ -226,7 +226,8 @@ sub _th_index_lite { $rv .= $pad . $mapping->{$nn->messageid}->[1]; } } - $rv .= "_ $s_s, $s_c\n"; + $rv .= "_ "; + $rv .= "$s_s, $s_c; $ctx->{s_nr}\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); -- cgit v1.2.3-24-ge0c7