X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=lib%2Fsup%2Fthread.rb;h=2300305c0710da7dfcb55891b5967abdc27e7f8f;hb=4efc8adaf8c63ff054c71149de6c12fe8194ff03;hp=4d92ad3dfb1efe4fab37cc41a616aeeef5052755;hpb=e047e53b436eaed1c323c037ad3abcbe7ef67f71;p=sup diff --git a/lib/sup/thread.rb b/lib/sup/thread.rb index 4d92ad3..2300305 100644 --- a/lib/sup/thread.rb +++ b/lib/sup/thread.rb @@ -24,6 +24,8 @@ ## a faked root object tying them all together into one tree ## structure. +require 'set' + module Redwood class Thread @@ -47,8 +49,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 +60,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 @@ -101,17 +103,16 @@ class Thread def toggle_label label if has_label? label remove_label label - return false + false else apply_label label - return true + true end end def set_labels l; each { |m, *o| m && m.labels = l }; end - def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end - def save index; each { |m, *o| m && m.save(index) }; end + def save_state index; each { |m, *o| m && m.save_state(index) }; end def direct_participants map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq @@ -123,15 +124,14 @@ class Thread def size; map { |m, *o| m ? 1 : 0 }.sum; end def subj; argfind { |m, *o| m && m.subj }; end - def labels - map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s } - end + def labels; inject(Set.new) { |s, (m, *o)| m ? s | m.labels : s } end def labels= l - each { |m, *o| m && m.labels = l.clone } + raise ArgumentError, "not a set" unless l.is_a?(Set) + each { |m, *o| m && m.labels = l.dup } end def latest_message - inject(nil) do |a, b| + inject(nil) do |a, b| b = b.first if a.nil? b @@ -162,7 +162,7 @@ class Container @id = id @message, @parent, @thread = nil, nil, nil @children = [] - end + end def each_with_stuff parent=nil yield self, 0, parent @@ -207,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}", @@ -222,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 @@ -235,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 @@ -256,21 +259,15 @@ class ThreadSet @thread_by_subj = thread_by_subj end - def thread_for_id mid; (c = @messages[mid]) && c.root.thread end + 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 delete_cruft - @threads.each { |k, v| @threads.delete(k) if v.empty? } - end - private :delete_cruft - - 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} **" @@ -279,38 +276,57 @@ 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_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]) + return unless @messages.member?(mid) + 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 + return unless @messages.member?(mid) + c = @messages[mid] + 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 @@ -335,6 +351,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.member?(t.first.id) ? @messages[t.first.id] : nil + 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 @@ -344,17 +386,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| @@ -364,13 +406,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 @@ -382,14 +417,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]