Class: Redwood::ThreadSet

Inherits:
Object show all
Defined in:
lib/sup/thread.rb

Overview

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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(index, thread_by_subj = true) ⇒ ThreadSet

Returns a new instance of ThreadSet.



258
259
260
261
262
263
264
265
266
# File 'lib/sup/thread.rb', line 258

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

Instance Attribute Details

#num_messagesObject (readonly)

Returns the value of attribute num_messages.



255
256
257
# File 'lib/sup/thread.rb', line 255

def num_messages
  @num_messages
end

Instance Method Details

#add_message(message) ⇒ Object

the heart of the threading code



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/sup/thread.rb', line 391

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

#add_thread(t) ⇒ Object

merges in a pre-loaded thread



355
356
357
358
# File 'lib/sup/thread.rb', line 355

def add_thread t
  raise "duplicate" if @threads.values.member? t
  t.each { |m, *o| add_message m }
end

#contains?(m) ⇒ Boolean

Returns:

  • (Boolean)


271
# File 'lib/sup/thread.rb', line 271

def contains? m; contains_id? m.id end

#contains_id?(id) ⇒ Boolean

Returns:

  • (Boolean)


269
# File 'lib/sup/thread.rb', line 269

def contains_id? id; @messages.member?(id) && !@messages[id].empty? end

#dump(f = $stdout) ⇒ Object



276
277
278
279
280
281
282
283
# File 'lib/sup/thread.rb', line 276

def dump f=$stdout
  @threads.each do |s, t|
    f.puts "**********************"
    f.puts "** for subject #{s} **"
    f.puts "**********************"
    t.dump f
  end
end

#is_relevant?(m) ⇒ Boolean

Returns:

  • (Boolean)


386
387
388
# File 'lib/sup/thread.rb', line 386

def is_relevant? m
  m.refs.any? { |ref_id| @messages.member? ref_id }
end

#join_threads(threads) ⇒ Object

merges two threads together. both must be members of this threadset. does its best, heuristically, to determine which is the parent.



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/sup/thread.rb', line 362

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_n_threads(num, opts = {}) ⇒ Object

load in (at most) num number of threads from the index



333
334
335
336
337
338
339
340
341
342
# File 'lib/sup/thread.rb', line 333

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

#load_thread_for_message(m, opts = {}) ⇒ Object

loads in all messages needed to thread m may do nothing if m’s thread is killed



346
347
348
349
350
351
352
# File 'lib/sup/thread.rb', line 346

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

#remove_id(mid) ⇒ Object



318
319
320
321
322
323
# File 'lib/sup/thread.rb', line 318

def remove_id mid
  return unless @messages.member?(mid)
  c = @messages[mid]
  remove_container c
  prune_thread_of c
end

#remove_thread_containing_id(mid) ⇒ Object



325
326
327
328
329
330
# File 'lib/sup/thread.rb', line 325

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

#sizeObject



274
# File 'lib/sup/thread.rb', line 274

def size; @threads.size end

#thread_for(m) ⇒ Object



270
# File 'lib/sup/thread.rb', line 270

def thread_for m; thread_for_id m.id end

#thread_for_id(mid) ⇒ Object



268
# File 'lib/sup/thread.rb', line 268

def thread_for_id mid; @messages.member?(mid) && @messages[mid].root.thread end

#threadsObject



273
# File 'lib/sup/thread.rb', line 273

def threads; @threads.values end