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.



262
263
264
265
266
267
268
269
270
# File 'lib/sup/thread.rb', line 262

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.



259
260
261
# File 'lib/sup/thread.rb', line 259

def num_messages
  @num_messages
end

Instance Method Details

#add_message(message) ⇒ Object

the heart of the threading code



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
438
439
440
441
442
443
444
445
446
447
# File 'lib/sup/thread.rb', line 401

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



359
360
361
362
# File 'lib/sup/thread.rb', line 359

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

#contains?(m) ⇒ Boolean

Returns:

  • (Boolean)


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

def contains? m; contains_id? m.id end

#contains_id?(id) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#delete_message(message) ⇒ Object



394
395
396
397
398
# File 'lib/sup/thread.rb', line 394

def delete_message message
  el = @messages[message.id]
  return unless el.message
  el.message = nil
end

#dump(f = $stdout) ⇒ Object



280
281
282
283
284
285
286
287
# File 'lib/sup/thread.rb', line 280

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)


390
391
392
# File 'lib/sup/thread.rb', line 390

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.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/sup/thread.rb', line 366

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



337
338
339
340
341
342
343
344
345
346
# File 'lib/sup/thread.rb', line 337

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



350
351
352
353
354
355
356
# File 'lib/sup/thread.rb', line 350

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



322
323
324
325
326
327
# File 'lib/sup/thread.rb', line 322

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



329
330
331
332
333
334
# File 'lib/sup/thread.rb', line 329

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



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

def size; @threads.size end

#thread_for(m) ⇒ Object



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

def thread_for m; thread_for_id m.id end

#thread_for_id(mid) ⇒ Object



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

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

#threadsObject



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

def threads; @threads.values end