class Redwood::ThreadSet
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.
Attributes
Public Class Methods
# File lib/sup/thread.rb, line 264 def initialize index, thread_by_subj=true @index = index @num_messages = 0 ## map from message ids to container objects @messages = SavingHash.new { |id| Container.new id } ## map from subject strings or (or root message ids) to thread objects @threads = SavingHash.new { Thread.new } @thread_by_subj = thread_by_subj end
Public Instance Methods
the heart of the threading code
# File lib/sup/thread.rb, line 403 def add_message message 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: (message.refs + [el.id]).inject(nil) do |prev, ref_id| ref = @messages[ref_id] link prev, ref if prev ref end ## link via in-reply-to: message.replytos.each do |ref_id| ref = @messages[ref_id] link ref, el, true break # only do the first one end root = el.root key = if thread_by_subj? Message.normalize_subj root.subj else root.id end ## check to see if the subject is still the same (in the case ## that we first added a child message with a different ## subject) if root.thread if @threads.member?(key) && @threads[key] != root.thread @threads.delete key end else thread = @threads[key] thread << root root.thread = thread end ## last bit @num_messages += 1 end
merges in a pre-loaded thread
# File lib/sup/thread.rb, line 361 def add_thread t raise "duplicate" if @threads.values.member? t t.each { |m, *o| add_message m } end
# File lib/sup/thread.rb, line 277 def contains? m; contains_id? m.id end
# File lib/sup/thread.rb, line 275 def contains_id? id; @messages.member?(id) && !@messages[id].empty? end
# File lib/sup/thread.rb, line 396 def delete_message message el = @messages[message.id] return unless el.message el.message = nil end
# File lib/sup/thread.rb, line 282 def dump f=$stdout @threads.each do |s, t| f.puts "**********************" f.puts "** for subject #{s} **" f.puts "**********************" t.dump f end end
# File lib/sup/thread.rb, line 392 def is_relevant? m m.refs.any? { |ref_id| @messages.member? ref_id } end
merges two threads together. both must be members of this threadset. does its best, heuristically, to determine which is the parent.
# File lib/sup/thread.rb, line 368 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
load in (at most) num number of threads from the index
# File lib/sup/thread.rb, line 339 def load_n_threads num, opts={} @index.each_id_by_date opts do |mid, builder| break if size >= num unless num == -1 next if contains_id? mid m = builder.call load_thread_for_message m, :skip_killed => opts[:skip_killed], :load_deleted => opts[:load_deleted], :load_spam => opts[:load_spam] yield size if block_given? end end
loads in all messages needed to thread m may do nothing if m's thread is killed
# File lib/sup/thread.rb, line 352 def load_thread_for_message m, opts={} good = @index.each_message_in_thread_for m, opts do |mid, builder| next if contains_id? mid add_message builder.call end add_message m if good end
# File lib/sup/thread.rb, line 324 def remove_id mid return unless @messages.member?(mid) c = @messages[mid] remove_container c prune_thread_of c end
# File lib/sup/thread.rb, line 331 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
# File lib/sup/thread.rb, line 280 def size; @threads.size end
# File lib/sup/thread.rb, line 276 def thread_for m; thread_for_id m.id end
# File lib/sup/thread.rb, line 274 def thread_for_id mid; @messages.member?(mid) && @messages[mid].root.thread end
# File lib/sup/thread.rb, line 279 def threads; @threads.values end
Private Instance Methods
link two containers
# File lib/sup/thread.rb, line 292 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.id} and child #{c.id} would create a loop" return 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
# File lib/sup/thread.rb, line 316 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
# File lib/sup/thread.rb, line 311 def remove_container c c.parent.children.delete c if c.parent # remove from tree end