@encrypted = false
@chunks = nil
+ ## we need to initialize this. see comments in parse_header as to
+ ## why.
+ @refs = []
+
parse_header(opts[:header] || @source.load_header(@source_info))
end
@to = PersonManager.people_for header["to"]
@cc = PersonManager.people_for header["cc"]
@bcc = PersonManager.people_for header["bcc"]
- @refs = (header["references"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
+
+ ## before loading our full header from the source, we can actually
+ ## have some extra refs set by the UI. (this happens when the user
+ ## joins threads manually). so we will merge the current refs values
+ ## in here.
+ refs = (header["references"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
+ @refs = (@refs + refs).uniq
@replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
@replyto = PersonManager.person_for header["reply-to"]
end
private :parse_header
+ def add_ref ref
+ @refs << ref
+ @dirty = true
+ end
+
def snippet; @snippet || (chunks && @snippet); end
def is_list_message?; !@list_address.nil?; end
def is_draft?; @source.is_a? DraftLoader; end
k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
k.add :tag_matching, "Tag matching threads", 'g'
k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
+ k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
end
def initialize hidden_labels=[], load_thread_opts={}
regen_text
end
+ def join_threads
+ ## this command has no non-tagged form. as a convenience, allow this
+ ## command to be applied to tagged threads without hitting ';'.
+ @tags.apply_to_tagged :join_threads
+ end
+
+ def multi_join_threads threads
+ @ts.join_threads threads or return
+ @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
+ update
+ end
+
def jump_to_next_new
n = @mutex.synchronize do
((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
def drop_all_tags; @tagged.clear; end
def drop_tag_for o; @tagged.delete o; end
- def apply_to_tagged
+ def apply_to_tagged action=nil
targets = @tagged.select_by_value
num_tagged = targets.size
if num_tagged == 0
end
noun = num_tagged == 1 ? "thread" : "threads"
- c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
- return if c.nil? # user cancelled
- if(action = @mode.resolve_input(c))
+ unless action
+ c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
+ return if c.nil? # user cancelled
+ action = @mode.resolve_input c
+ end
+
+ if action
tagged_sym = "multi_#{action}".intern
if @mode.respond_to? tagged_sym
@mode.send tagged_sym, targets
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}",
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[t.first.id]
+ 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