X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=lib%2Fsup%2Fthread.rb;h=99f21dc31a07bfcb3424a1fc206e20133e2ec8ab;hb=47801067362e6bb2ba503da7c6bdea4a16153ac8;hp=722ba344bb5343fd345214329fc92f046e2ab693;hpb=9b93ad36260cb6fe905c180a13fb15c4139cb146;p=sup diff --git a/lib/sup/thread.rb b/lib/sup/thread.rb index 722ba34..99f21dc 100644 --- a/lib/sup/thread.rb +++ b/lib/sup/thread.rb @@ -11,7 +11,7 @@ ## zero or more Threads. A Thread represents all the message related ## to a particular subject. Each Thread has one or more Containers. A ## Container is a recursive structure that holds the message tree as -## determined by the references: and in-reply-to: headers. EAch +## determined by the references: and in-reply-to: headers. Each ## Container holds zero or one messages. In the case of zero messages, ## it means we've seen a reference to the message but haven't (yet) ## seen the message itself. @@ -47,8 +47,8 @@ class Thread ## unused def dump f=$stdout - f.puts "=== start thread #{self} with #{@containers.length} trees ===" - @containers.each { |c| c.dump_recursive f } + f.puts "=== start thread with #{@containers.length} trees ===" + @containers.each { |c| c.dump_recursive f; f.puts } f.puts "=== end thread ===" end @@ -58,7 +58,7 @@ class Thread ## messages). def each fake_root=false adj = 0 - root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date || 0 } + root = @containers.find_all { |c| c.message && !Message.subj_is_reply?(c.message.subj) }.argmin { |c| c.date } if root adj = 1 @@ -86,8 +86,12 @@ class Thread def dirty?; any? { |m, *o| m && m.dirty? }; end def date; map { |m, *o| m.date if m }.compact.max; end def snippet - last_m, last_stuff = select { |m, *o| m && m.snippet && !m.snippet.empty? }.sort_by { |m, *o| m.date }.last - last_m ? last_m.snippet : "" + with_snippets = select { |m, *o| m && m.snippet && !m.snippet.empty? } + first_unread, * = with_snippets.select { |m, *o| m.has_label?(:unread) }.sort_by { |m, *o| m.date }.first + return first_unread.snippet if first_unread + last_read, * = with_snippets.sort_by { |m, *o| m.date }.last + return last_read.snippet if last_read + "" end def authors; map { |m, *o| m.from if m }.compact.uniq; end @@ -181,6 +185,10 @@ class Container def root?; @parent.nil?; end def root; root? ? self : @parent.root; end + ## skip over any containers which are empty and have only one child. we use + ## this make the threaded display a little nicer, and only stick in the + ## "missing message" line when it's graphically necessary, i.e. when the + ## missing message has more than one descendent. def first_useful_descendant if empty? && @children.size == 1 @children.first.first_useful_descendant @@ -199,7 +207,7 @@ class Container def subj; find_attr :subj; end def date; find_attr :date; end - def is_reply?; subj && Message.subject_is_reply?(subj); end + def is_reply?; subj && Message.subj_is_reply?(subj); end def to_s [ "<#{id}", @@ -214,9 +222,9 @@ class Container f.print " " * indent f.print "+->" end - line = #"[#{useful? ? 'U' : ' '}] " + + line = "[#{thread.nil? ? ' ' : '*'}] " + #"[#{useful? ? 'U' : ' '}] " + if @message - "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}" + message.subj ##{@message.refs.inspect} / #{@message.replytos.inspect}" else "" end @@ -227,13 +235,16 @@ class Container end end -## A set of threads (so a forest). Builds thread structures by reading -## messages from an index. +## A set of threads, so a forest. Is integrated with the index and +## builds thread structures by reading messages from it. ## ## If 'thread_by_subj' is true, puts messages with the same subject in ## one thread, even if they don't reference each other. This is ## helpful for crappy MUAs that don't set In-reply-to: or References: ## headers, but means that messages may be threaded unnecessarily. +## +## The following invariants are maintained: every Thread has at least one +## Container tree, and every Container tree has at least one Message. class ThreadSet attr_reader :num_messages bool_reader :thread_by_subj @@ -248,21 +259,15 @@ class ThreadSet @thread_by_subj = thread_by_subj end - def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end - def thread_for m - (c = @messages[m.id]) && c.root.thread - end - - def delete_cruft - @threads.each { |k, v| @threads.delete(k) if v.empty? } - end - private :delete_cruft + def thread_for_id mid; @messages.member?(mid) && @messages[mid].root.thread end + def contains_id? id; @messages.member?(id) && !@messages[id].empty? end + def thread_for m; thread_for_id m.id end + def contains? m; contains_id? m.id end - def threads; delete_cruft; @threads.values; end - def size; delete_cruft; @threads.size; end + def threads; @threads.values end + def size; @threads.size end - ## unused - def dump f + def dump f=$stdout @threads.each do |s, t| f.puts "**********************" f.puts "** for subject #{s} **" @@ -271,38 +276,55 @@ class ThreadSet end end + ## link two containers def link p, c, overwrite=false if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop -# puts "*** linking parent #{p} and child #{c} would create a loop" + #puts "*** linking parent #{p.id} and child #{c.id} would create a loop" return end - if c.parent.nil? || overwrite - c.parent.children.delete c if overwrite && c.parent - if c.thread - c.thread.drop c - c.thread = nil - end - p.children << c - c.parent = p - end + #puts "in link for #{p.id} to #{c.id}, perform? #{c.parent.nil?} || #{overwrite}" + + return unless c.parent.nil? || overwrite + remove_container c + p.children << c + c.parent = p + + ## if the child was previously a top-level container, it now ain't, + ## so ditch our thread and kill it if necessary + prune_thread_of c end private :link - def remove mid + def remove_container c + c.parent.children.delete c if c.parent # remove from tree + end + private :remove_container + + def prune_thread_of c + return unless c.thread + c.thread.drop c + @threads.delete_if { |k, v| v == c.thread } if c.thread.empty? + c.thread = nil + end + private :prune_thread_of + + def remove_id mid return unless(c = @messages[mid]) + remove_container c + prune_thread_of c + end - c.parent.children.delete c if c.parent - if c.thread - c.thread.drop c - c.thread = nil - end + def remove_thread_containing_id mid + c = @messages[mid] or return + t = c.root.thread + @threads.delete_if { |key, thread| t == thread } end ## load in (at most) num number of threads from the index def load_n_threads num, opts={} @index.each_id_by_date opts do |mid, builder| - break if size >= num + break if size >= num unless num == -1 next if contains_id? mid m = builder.call @@ -327,6 +349,32 @@ class ThreadSet t.each { |m, *o| add_message m } end + ## merges two threads together. both must be members of this threadset. + ## does its best, heuristically, to determine which is the parent. + def join_threads threads + return if threads.size < 2 + + containers = threads.map do |t| + c = @messages[t.first.id] + raise "not in threadset: #{t.first.id}" unless c && c.message + c + end + + ## use subject headers heuristically + parent = containers.find { |c| !c.is_reply? } + + ## no thread was rooted by a non-reply, so make a fake parent + parent ||= @messages["joining-ref-" + containers.map { |c| c.id }.join("-")] + + containers.each do |c| + next if c == parent + c.message.add_ref parent.id + link parent, c + end + + true + end + def is_relevant? m m.refs.any? { |ref_id| @messages.member? ref_id } end @@ -336,17 +384,17 @@ class ThreadSet el = @messages[message.id] return if el.message # we've seen it before + #puts "adding: #{message.id}, refs #{message.refs.inspect}" + el.message = message oldroot = el.root ## link via references: - prev = nil - message.refs.each do |ref_id| + (message.refs + [el.id]).inject(nil) do |prev, ref_id| ref = @messages[ref_id] link prev, ref if prev - prev = ref + ref end - link prev, el, true if prev ## link via in-reply-to: message.replytos.each do |ref_id| @@ -356,13 +404,6 @@ class ThreadSet end root = el.root - - ## new root. need to drop old one and put this one in its place - if root != oldroot && oldroot.thread - oldroot.thread.drop oldroot - oldroot.thread = nil - end - key = if thread_by_subj? Message.normalize_subj root.subj @@ -374,14 +415,8 @@ class ThreadSet ## that we first added a child message with a different ## subject) if root.thread - unless @threads[key] == root.thread - if @threads[key] - root.thread.empty! - @threads[key] << root - root.thread = @threads[key] - else - @threads[key] = root.thread - end + if @threads.member?(key) && @threads[key] != root.thread + @threads.delete key end else thread = @threads[key]