module Redwood class ThreadViewMode < LineCursorMode DATE_FORMAT = "%B %e %Y %l:%M%P" register_keymap do |k| k.add :toggle_detailed_header, "Toggle detailed header", 'd' k.add :show_header, "Show full message header", 'H' k.add :toggle_expanded, "Expand/collapse item", :enter k.add :expand_all_messages, "Expand/collapse all messages", 'E' k.add :edit_message, "Edit message (drafts only)", 'e' k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o' k.add :jump_to_next_open, "Jump to next open message", 'n' k.add :jump_to_prev_open, "Jump to previous open message", 'p' k.add :toggle_starred, "Star or unstar message", '*' k.add :collapse_non_new_messages, "Collapse all but new messages", 'N' k.add :reply, "Reply to a message", 'r' k.add :forward, "Forward a message", 'f' end def initialize thread, hidden_labels=[] super() @thread = thread @state = {} @hidden_labels = hidden_labels earliest = nil latest = nil latest_date = nil @thread.each do |m, d, p| next unless m earliest ||= m @state[m] = if m.has_label?(:unread) && m == earliest :detailed elsif m.has_label?(:starred) || m.has_label?(:unread) :open else :closed end if latest_date.nil? || m.date > latest_date latest_date = m.date latest = m end end @state[latest] = :open if @state[latest] == :closed regen_chunks regen_text end def draw_line ln, opts={} if ln == curpos super ln, :highlight => true else super end end def lines; @text.length; end def [] i; @text[i]; end def show_header return unless(m = @message_lines[curpos]) BufferManager.spawn_unless_exists("Full header") do TextMode.new m.content #m.header_text end end def toggle_detailed_header return unless(m = @message_lines[curpos]) @state[m] = (@state[m] == :detailed ? :open : :detailed) update end def reply return unless(m = @message_lines[curpos]) mode = ReplyMode.new m BufferManager.spawn "Reply to #{m.subj}", mode end def forward return unless(m = @message_lines[curpos]) mode = ForwardMode.new m BufferManager.spawn "Forward of #{m.subj}", mode mode.edit end def toggle_starred return unless(m = @message_lines[curpos]) if m.has_label? :starred m.remove_label :starred else m.add_label :starred end ## TODO: don't recalculate EVERYTHING just to add a stupid little ## star to the display update UpdateManager.relay :starred, m end def toggle_expanded return unless(chunk = @chunk_lines[curpos]) case chunk when Message, Message::Quote, Message::Signature @state[chunk] = (@state[chunk] != :closed ? :closed : :open) when Message::Attachment view_attachment chunk end update end def edit_message return unless(m = @message_lines[curpos]) if m.is_draft? mode = ResumeMode.new m BufferManager.spawn "Edit message", mode else BufferManager.flash "Not a draft message!" end end def jump_to_next_open return unless(m = @message_lines[curpos]) while nextm = @messages[m][3] break if @state[nextm] == :open m = nextm end jump_to_message nextm if nextm end def jump_to_prev_open return unless(m = @message_lines[curpos]) ## jump to the top of the current message if we're in the body; ## otherwise, to the previous message top = @messages[m][0] if curpos == top while prevm = @messages[m][2] break if @state[prevm] == :open m = prevm end jump_to_message prevm if prevm else jump_to_message m end end def jump_to_message m top, bot, prevm, nextm = @messages[m] jump_to_line top unless top >= topline && top <= botline && bot >= topline && bot <= botline set_cursor_pos top end def expand_all_messages @global_message_state ||= :closed @global_message_state = (@global_message_state == :closed ? :open : :closed) @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message } update end def collapse_non_new_messages @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed } update end def expand_all_quotes if(m = @message_lines[curpos]) quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) } open, closed = quotes.partition { |c| @state[c] == :open } newstate = open.length > closed.length ? :closed : :open Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}" quotes.each { |c| @state[c] = newstate } update end end ## not sure if this is really necessary but we might as well... def cleanup @thread.each do |m, d, p| if m.has_label? :unread m.remove_label :unread UpdateManager.relay :read, m end end Redwood::log "releasing chunks and text from \"#{buffer.title}\"" @messages = @chunks = @text = nil end private def update regen_text buffer.mark_dirty if buffer end def regen_chunks @chunks = {} @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) } end def regen_text @text = [] @chunk_lines = [] @message_lines = [] @messages = {} prev_m = nil @thread.each do |m, depth, parent| text = chunk_to_lines m, @state[m], @text.length, depth, parent (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = m @message_lines[@text.length + i] = m end @messages[m] = [@text.length, @text.length + text.length, prev_m, nil] @messages[prev_m][3] = m if prev_m prev_m = m @text += text if @state[m] != :closed && @chunks.member?(m) @chunks[m].each do |c| @state[c] ||= :closed text = chunk_to_lines c, @state[c], @text.length, depth (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = c @message_lines[@text.length + i] = m end @text += text end @messages[m][1] = @text.length end end end def message_patina_lines m, state, parent, prefix prefix_widget = [:message_patina_color, prefix] widget = case state when :closed [:message_patina_color, "+ "] when :open, :detailed [:message_patina_color, "- "] end imp_widget = if m.has_label?(:starred) [:starred_patina_color, "* "] else [:message_patina_color, " "] end case state when :open [[prefix_widget, widget, imp_widget, [:message_patina_color, "#{m.from ? m.from.mediumname : '?'} to #{m.to.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]] # (m.to.empty? ? [] : [[[:message_patina_color, prefix + " To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) + when :closed [[prefix_widget, widget, imp_widget, [:message_patina_color, "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]] when :detailed labels = m.labels# - @hidden_labels x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] + ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) + (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) + (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) + [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] + [" Subject: #{m.subj}"] + [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] + [labels.empty? ? nil : " Labels: #{labels.join(', ')}"] ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] } #raise x.inspect x end end def break_into_lines prefix, list pad = " " * prefix.length [prefix + list.first + (list.length > 1 ? "," : "")] + list[1 .. -1].map_with_index do |e, i| pad + e + (i == list.length - 1 ? "" : ",") end end def chunk_to_lines chunk, state, start, depth, parent=nil prefix = " " * depth case chunk when :fake_root [[[:message_patina_color, "#{prefix}"]]] when nil [[[:message_patina_color, "#{prefix}"]]] when Message message_patina_lines(chunk, state, parent, prefix) + (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : []) when Message::Attachment [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]] when Message::Text t = chunk.lines if t.last =~ /^\s*$/ t.pop while t[t.length - 2] =~ /^\s*$/ end t.map { |line| [[:none, "#{prefix}#{line}"]] } when Message::Quote case state when :closed [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]] when :open t = chunk.lines [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] + t.map { |line| [[:quote_color, "#{prefix}#{line}"]] } end when Message::Signature case state when :closed [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]] when :open t = chunk.lines [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] + t.map { |line| [[:sig_color, "#{prefix}#{line}"]] } end else raise "unknown chunk type #{chunk.class.name}" end end def view_attachment a BufferManager.flash "viewing #{a.content_type} attachment..." a.view! BufferManager.erase_flash end end end