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.



264
265
266
267
268
269
270
271
272
# 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

Instance Attribute Details

#num_messagesObject (readonly)

Returns the value of attribute num_messages.



261
262
263
# File 'lib/sup/thread.rb', line 261

def num_messages
  @num_messages
end

Instance Method Details

#add_message(message) ⇒ Object

the heart of the threading code



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
448
# 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

  ## 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



361
362
363
364
# File 'lib/sup/thread.rb', line 361

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

#contains?(m) ⇒ Boolean

Returns:

  • (Boolean)


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

def contains? m; contains_id? m.id end

#contains_id?(id) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#delete_message(message) ⇒ Object



396
397
398
399
400
# File 'lib/sup/thread.rb', line 396

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

#dump(f = $stdout) ⇒ Object



282
283
284
285
286
287
288
289
# 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

#is_relevant?(m) ⇒ Boolean

Returns:

  • (Boolean)


392
393
394
# File 'lib/sup/thread.rb', line 392

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.



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# 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_n_threads(num, opts = {}) ⇒ Object

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



339
340
341
342
343
344
345
346
347
348
# 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

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

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



352
353
354
355
356
357
358
# 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

#remove_id(mid) ⇒ Object



324
325
326
327
328
329
# 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

#remove_thread_containing_id(mid) ⇒ Object



331
332
333
334
335
336
# 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

#sizeObject



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

def size; @threads.size end

#thread_for(m) ⇒ Object



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

def thread_for m; thread_for_id m.id end

#thread_for_id(mid) ⇒ Object



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

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

#threadsObject



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

def threads; @threads.values end