X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=lib%2Fsup%2Fmodes%2Fthread-view-mode.rb;h=88c512529f148bb9a2b9e5c4e3635cc587b997e1;hb=fe56e8fe02b2be19b455652ace77959ef13feeb7;hp=6693f68052790fcfd9a5e509e72d69f3ecc4ab4c;hpb=bfc7b47b610da5103c1f95f480a80d952b30a2a7;p=sup diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index 6693f68..88c5125 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -1,6 +1,9 @@ module Redwood class ThreadViewMode < LineCursorMode + include CanSpawnComposeMode + include CanSpawnForwardMode + ## this holds all info we need to lay out a message class MessageLayout attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new @@ -14,24 +17,29 @@ class ThreadViewMode < LineCursorMode INDENT_SPACES = 2 # how many spaces to indent child messages register_keymap do |k| - k.add :toggle_detailed_header, "Toggle detailed header", 'd' + k.add :toggle_detailed_header, "Toggle detailed header", 'h' k.add :show_header, "Show full message header", 'H' - k.add :toggle_expanded, "Expand/collapse item", :enter + k.add :activate_chunk, "Expand/collapse or activate item", :enter k.add :expand_all_messages, "Expand/collapse all messages", 'E' - k.add :edit_message, "Edit message (drafts only)", 'e' + k.add :edit_draft, "Edit draft", 'e' + k.add :edit_labels, "Edit or add labels for a thread", 'l' 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 :toggle_new, "Toggle new/read status of message", 'N' +# 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' - k.add :alias, "Edit alias/nickname for a person", 'a' + k.add :alias, "Edit alias/nickname for a person", 'i' k.add :edit_as_new, "Edit message as new", 'D' k.add :save_to_disk, "Save message/attachment to disk", 's' k.add :search, "Search for messages from particular people", 'S' k.add :compose, "Compose message to person", 'm' - k.add :archive_and_kill, "Archive thread and kill buffer", 'A' + k.add :archive_and_kill, "Archive thread and kill buffer", 'a' + k.add :delete_and_kill, "Delete thread and kill buffer", 'd' + k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "(" + k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")" end ## there are a couple important instance variables we hold to format @@ -51,13 +59,14 @@ class ThreadViewMode < LineCursorMode earliest, latest = nil, nil latest_date = nil altcolor = false + @thread.each do |m, d, p| next unless m earliest ||= m @layout[m].state = initial_state_for m @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color - @layout[m].orig_new = m.has_label? :unread + @layout[m].orig_new = m.has_label? :read altcolor = !altcolor if latest_date.nil? || m.date > latest_date latest_date = m.date @@ -68,6 +77,7 @@ class ThreadViewMode < LineCursorMode @layout[latest].state = :open if @layout[latest].state == :closed @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1 + @thread.remove_label :unread regen_text end @@ -100,11 +110,27 @@ class ThreadViewMode < LineCursorMode BufferManager.spawn "Reply to #{m.subj}", mode end + def subscribe_to_list + m = @message_lines[curpos] or return + if m.list_subscribe && m.list_subscribe =~ // + spawn_compose_mode :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 + else + BufferManager.flash "Can't find List-Subscribe header for this message." + end + end + + def unsubscribe_from_list + m = @message_lines[curpos] or return + if m.list_unsubscribe && m.list_unsubscribe =~ // + spawn_compose_mode :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 + else + BufferManager.flash "Can't find List-Unsubscribe header for this message." + end + end + def forward m = @message_lines[curpos] or return - mode = ForwardMode.new m - BufferManager.spawn "Forward of #{m.subj}", mode - mode.edit + spawn_forward_mode m end include CanAliasContacts @@ -123,73 +149,97 @@ class ThreadViewMode < LineCursorMode def compose p = @person_lines[curpos] - mode = - if p - ComposeMode.new :to => [p] - else - ComposeMode.new - end - BufferManager.spawn "Compose message", mode - mode.edit + if p + spawn_compose_mode :to => [p] + else + spawn_compose_mode + end end + def edit_labels + reserved_labels = @thread.labels.select { |l| LabelManager::RESERVED_LABELS.include? l } + new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels + + return unless new_labels + @thread.labels = (reserved_labels + new_labels).uniq + new_labels.each { |l| LabelManager << l } + update + UpdateManager.relay self, :label_thread, @thread + end + def toggle_starred m = @message_lines[curpos] or return - if m.has_label? :starred - m.remove_label :starred + toggle_label m, :starred + end + + def toggle_new + m = @message_lines[curpos] or return + toggle_label m, :unread + end + + def toggle_label m, label + if m.has_label? label + m.remove_label label else - m.add_label :starred + m.add_label label end ## TODO: don't recalculate EVERYTHING just to add a stupid little ## star to the display update - UpdateManager.relay self, :starred, m + UpdateManager.relay self, :label, m end - def toggle_expanded + ## called when someone presses enter when the cursor is highlighting + ## a chunk. for expandable chunks (including messages) we toggle + ## open/closed state; for viewable chunks (like attachments) we + ## view. + def activate_chunk chunk = @chunk_lines[curpos] or return - case chunk - when Message - l = @layout[chunk] - l.state = (l.state != :closed ? :closed : :open) - cursor_down if l.state == :closed - when Message::Quote, Message::Signature - return if chunk.lines.length == 1 - l = @chunk_layout[chunk] - l.state = (l.state != :closed ? :closed : :open) - cursor_down if l.state == :closed - when Message::Attachment - view_attachment chunk + layout = + if chunk.is_a?(Message) + @layout[chunk] + elsif chunk.expandable? + @chunk_layout[chunk] + end + if layout + layout.state = (layout.state != :closed ? :closed : :open) + #cursor_down if layout.state == :closed # too annoying + update + elsif chunk.viewable? + view chunk end - update end def edit_as_new m = @message_lines[curpos] or return mode = ComposeMode.new(:body => m.basic_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc) BufferManager.spawn "edit as new", mode - mode.edit + mode.edit_message end def save_to_disk chunk = @chunk_lines[curpos] or return case chunk - when Message::Attachment - fn = BufferManager.ask :filename, "Save attachment to file: ", chunk.filename - save_to_file(fn) { |f| f.print chunk } if fn + when Chunk::Attachment + fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", chunk.filename + save_to_file(fn) { |f| f.print chunk.raw_content } if fn else m = @message_lines[curpos] - fn = BufferManager.ask :filename, "Save message to file: " - save_to_file(fn) { |f| f.print m.raw_full_message } if fn + fn = BufferManager.ask_for_filename :filename, "Save message to file: " + return unless fn + save_to_file(fn) do |f| + m.each_raw_message_line { |l| f.print l } + end end end - def edit_message + def edit_draft m = @message_lines[curpos] or return if m.is_draft? mode = ResumeMode.new m BufferManager.spawn "Edit message", mode - mode.edit + BufferManager.kill_buffer self.buffer + mode.edit_message else BufferManager.flash "Not a draft message!" end @@ -205,6 +255,7 @@ class ThreadViewMode < LineCursorMode end def jump_to_next_open + return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations m = @message_lines[curpos] or return while nextm = @layout[m].next break if @layout[nextm].state != :closed @@ -259,7 +310,7 @@ class ThreadViewMode < LineCursorMode def expand_all_quotes if(m = @message_lines[curpos]) - quotes = m.chunks.select { |c| (c.is_a?(Message::Quote) || c.is_a?(Message::Signature)) && c.lines.length > 1 } + quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 } numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) } newstate = numopen > quotes.length / 2 ? :closed : :open quotes.each { |c| @chunk_layout[c].state = newstate } @@ -277,7 +328,13 @@ class ThreadViewMode < LineCursorMode BufferManager.kill_buffer_safely buffer end -private + def delete_and_kill + @thread.apply_label :deleted + UpdateManager.relay self, :deleted, @thread + BufferManager.kill_buffer_safely buffer + end + +private def initial_state_for m if m.has_label?(:starred) || m.has_label?(:unread) @@ -309,6 +366,9 @@ private end l = @layout[m] + ## is this still necessary? + next unless @layout[m].state # skip discarded drafts + ## build the patina text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color @@ -332,7 +392,15 @@ private if l.state != :closed m.chunks.each do |c| cl = @chunk_layout[c] - cl.state ||= :closed + + ## set the default state for chunks + cl.state ||= + if c.expandable? && c.respond_to?(:initial_state) + c.initial_state + else + :closed + end + text = chunk_to_lines c, cl.state, @text.length, depth (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = c @@ -349,14 +417,10 @@ private def message_patina_lines m, state, start, parent, prefix, color, star_color prefix_widget = [color, prefix] - widget = - case state - when :closed - [color, "+ "] - when :open, :detailed - [color, "- "] - end - imp_widget = + + open_widget = [color, (state == :closed ? "+ " : "- ")] + new_widget = [color, (m.has_label?(:unread) ? "N" : " ")] + starred_widget = if m.has_label?(:starred) [star_color, "* "] else @@ -366,39 +430,41 @@ private case state when :open @person_lines[start] = m.from - [[prefix_widget, widget, imp_widget, + [[prefix_widget, open_widget, new_widget, starred_widget, [color, "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]] when :closed @person_lines[start] = m.from - [[prefix_widget, widget, imp_widget, + [[prefix_widget, open_widget, new_widget, starred_widget, [color, "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]] when :detailed @person_lines[start] = m.from - from = [[prefix_widget, widget, imp_widget, [color, "From: #{m.from ? format_person(m.from) : '?'}"]]] + from = [[prefix_widget, open_widget, new_widget, starred_widget, + [color, "From: #{m.from ? format_person(m.from) : '?'}"]]] rest = [] unless m.to.empty? m.to.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p } - rest += format_person_list " To: ", m.to + rest += format_person_list " To: ", m.to end unless m.cc.empty? m.cc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p } - rest += format_person_list " Cc: ", m.cc + rest += format_person_list " Cc: ", m.cc end unless m.bcc.empty? m.bcc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p } - rest += format_person_list " Bcc: ", m.bcc + rest += format_person_list " Bcc: ", m.bcc end + show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS rest += [ - " 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), - m.labels.empty? ? nil : " Labels: #{m.labels.join(', ')}", + " 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), + show_labels.empty? ? nil : " Labels: #{show_labels.join(', ')}", ].compact from + rest.map { |l| [[color, prefix + " " + l]] } @@ -418,6 +484,7 @@ private p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "") end + ## todo: check arguments on this overly complex function def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil prefix = " " * INDENT_SPACES * depth case chunk @@ -428,42 +495,31 @@ private when Message message_patina_lines(chunk, state, start, parent, prefix, color, star_color) + (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.length > 1 - t.pop while t[-2] =~ /^\s*$/ # pop until only one file empty line - end - t.map { |line| [[:none, "#{prefix}#{line}"]] } - when Message::Quote - return [[[:quote_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1 - case state - when :closed - [[[:quote_patina_color, "#{prefix}+ (#{chunk.lines.length} quoted lines)"]]] - when :open - [[[:quote_patina_color, "#{prefix}- (#{chunk.lines.length} quoted lines)"]]] + chunk.lines.map { |line| [[:quote_color, "#{prefix}#{line}"]] } - end - when Message::Signature - return [[[:sig_patina_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1 - case state - when :closed - [[[:sig_patina_color, "#{prefix}+ (#{chunk.lines.length}-line signature)"]]] - when :open - [[[:sig_patina_color, "#{prefix}- (#{chunk.lines.length}-line signature)"]]] + chunk.lines.map { |line| [[:sig_color, "#{prefix}#{line}"]] } - end + else - raise "unknown chunk type #{chunk.class.name}" + raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging + if chunk.inlineable? + chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] } + elsif chunk.expandable? + case state + when :closed + [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]] + when :open + [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] } + end + else + [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]] + end end end - def view_attachment a - BufferManager.flash "viewing #{a.content_type} attachment..." - success = a.view! + def view chunk + BufferManager.flash "viewing #{chunk.content_type} attachment..." + success = chunk.view! BufferManager.erase_flash BufferManager.completely_redraw_screen unless success - BufferManager.spawn "Attachment: #{a.filename}", TextMode.new(a.to_s) + BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s) BufferManager.flash "Couldn't execute view command, viewing as text." end end