# /$INBOX/$MESSAGE_ID/#R
sub msg_reply {
my ($ctx, $hdr) = @_;
- my $se_url = 'https://git-htmldocs.bogomips.org/git-send-email.html';
+ my $se_url =
+ 'https://kernel.org/pub/software/scm/git/docs/git-send-email.html';
my ($arg, $link) = mailto_arg_link($hdr);
push @$arg, '/path/to/YOUR_REPLY';
# 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');
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);
- 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);
push @tocc, "$f: $dst" if $dst ne '';
}
- $rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC\n";
+ $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 .= ' '.join('; +', @tocc) . "\n" if @tocc;
+
+ my $mapping = $ctx->{mapping};
+ if (!$mapping && $irt) {
+ my $mirt = PublicInbox::Hval->msgid($irt);
+ my $href = $upfx . $mirt->as_href . '/';
+ my $html = $mirt->as_html;
+ $rv .= qq(In-Reply-To: <<a\nhref="$href/">$html</a>>\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
"<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}%]";
+ } 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\">$flat</a>";
+ $rv .= "|<a\nhref=\"${mhref}t/#u\">$threaded</a>]";
+ $rv .= " / <a\nhref=#r$id>$ctx->{s_nr}</a>";
}
+
$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, $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};
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');
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) {
- $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>\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};
+ $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 {
- 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];
$idx = scalar @$m;
push @$m, $node;
}
- $mapping->{$node->messageid} = [ [], '', $node, $idx ];
- skel_dump($state, $level, $node);
+ $mapping->{$node->messageid} = [ [], '', $node, $idx, $level ];
+ 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 $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";
- 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);
-
- # lazy load the full message from mini_mime:
+ $ctx->{-upfx} = '../../';
+ $ctx->{cur_level} = 0;
+ $ctx->{dst} = \$skel;
+ $ctx->{prev_attr} = '';
+ $ctx->{prev_level} = 0;
+ $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);
+ $skel .= '</pre>';
+ return stream_thread($th, $ctx) unless $ctx->{flat};
+
+ # 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, $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;
}
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>';
+ '</pre>'.$skel;
});
[ 200, ['Content-Type', 'text/html; charset=UTF-8'], $body ];
}
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>);
$$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;
}
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};
$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";
$$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;
# 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);
$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;
}
$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";
}
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};
}
+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;