3 class ThreadViewMode < LineCursorMode
4 DATE_FORMAT = "%B %e %Y %l:%M%P"
7 k.add :toggle_detailed_header, "Toggle detailed header", 'd'
8 k.add :show_header, "Show full message header", 'H'
9 k.add :toggle_expanded, "Expand/collapse item", :enter
10 k.add :expand_all_messages, "Expand/collapse all messages", 'E'
11 k.add :edit_message, "Edit message (drafts only)", 'e'
12 k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
13 k.add :jump_to_next_open, "Jump to next open message", 'n'
14 k.add :jump_to_prev_open, "Jump to previous open message", 'p'
15 k.add :toggle_starred, "Star or unstar message", '*'
16 k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
17 k.add :reply, "Reply to a message", 'r'
18 k.add :forward, "Forward a message", 'f'
19 k.add :save_to_disk, "Save message/attachment to disk", 's'
22 def initialize thread, hidden_labels=[]
26 @hidden_labels = hidden_labels
31 @thread.each do |m, d, p|
35 if m.has_label?(:unread) && m == earliest
37 elsif m.has_label?(:starred) || m.has_label?(:unread)
42 if latest_date.nil? || m.date > latest_date
47 @state[latest] = :open if @state[latest] == :closed
49 BufferManager.say "Loading message..." do
55 def draw_line ln, opts={}
57 super ln, :highlight => true
62 def lines; @text.length; end
63 def [] i; @text[i]; end
66 return unless(m = @message_lines[curpos])
67 BufferManager.spawn_unless_exists("Full header") do
68 TextMode.new m.raw_header
72 def toggle_detailed_header
73 return unless(m = @message_lines[curpos])
74 @state[m] = (@state[m] == :detailed ? :open : :detailed)
79 return unless(m = @message_lines[curpos])
80 mode = ReplyMode.new m
81 BufferManager.spawn "Reply to #{m.subj}", mode
85 return unless(m = @message_lines[curpos])
86 mode = ForwardMode.new m
87 BufferManager.spawn "Forward of #{m.subj}", mode
92 return unless(m = @message_lines[curpos])
93 if m.has_label? :starred
94 m.remove_label :starred
98 ## TODO: don't recalculate EVERYTHING just to add a stupid little
99 ## star to the display
101 UpdateManager.relay :starred, m
105 return unless(chunk = @chunk_lines[curpos])
107 when Message, Message::Quote, Message::Signature
108 @state[chunk] = (@state[chunk] != :closed ? :closed : :open)
109 when Message::Attachment
110 view_attachment chunk
117 return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
120 File.open(fn, "w") { |f| yield f }
121 BufferManager.flash "Successfully wrote #{fn}."
122 rescue SystemCallError => e
123 BufferManager.flash "Error writing to file: #{e.message}"
129 return unless(chunk = @chunk_lines[curpos])
131 when Message::Attachment
132 fn = BufferManager.ask :filename, "save attachment to file: ", chunk.filename
133 save(fn) { |f| f.print chunk } if fn
135 m = @message_lines[curpos]
136 fn = BufferManager.ask :filename, "save message to file: "
137 save(fn) { |f| f.print m.raw_full_message } if fn
142 return unless(m = @message_lines[curpos])
144 mode = ResumeMode.new m
145 BufferManager.spawn "Edit message", mode
147 BufferManager.flash "Not a draft message!"
151 def jump_to_next_open
152 return unless(m = @message_lines[curpos])
153 while nextm = @messages[m][3]
154 break if @state[nextm] == :open
157 jump_to_message nextm if nextm
160 def jump_to_prev_open
161 return unless(m = @message_lines[curpos])
162 ## jump to the top of the current message if we're in the body;
163 ## otherwise, to the previous message
164 top = @messages[m][0]
166 while prevm = @messages[m][2]
167 break if @state[prevm] == :open
170 jump_to_message prevm if prevm
176 def jump_to_message m
177 top, bot, prevm, nextm = @messages[m]
178 jump_to_line top unless top >= topline &&
179 top <= botline && bot >= topline && bot <= botline
183 def expand_all_messages
184 @global_message_state ||= :closed
185 @global_message_state = (@global_message_state == :closed ? :open : :closed)
186 @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
191 def collapse_non_new_messages
192 @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
196 def expand_all_quotes
197 if(m = @message_lines[curpos])
198 quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
199 open, closed = quotes.partition { |c| @state[c] == :open }
200 newstate = open.length > closed.length ? :closed : :open
201 Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}"
202 quotes.each { |c| @state[c] = newstate }
207 ## not sure if this is really necessary but we might as well...
209 @thread.each do |m, d, p|
210 if m && m.has_label?(:unread)
211 m.remove_label :unread
212 UpdateManager.relay :read, m
216 Redwood::log "releasing chunks and text from \"#{buffer.title}\""
217 @messages = @chunks = @text = nil
224 buffer.mark_dirty if buffer
229 @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
239 @thread.each do |m, depth, parent|
240 text = chunk_to_lines m, @state[m], @text.length, depth, parent
241 (0 ... text.length).each do |i|
242 @chunk_lines[@text.length + i] = m
243 @message_lines[@text.length + i] = m
246 @messages[m] = [@text.length, @text.length + text.length, prev_m, nil]
247 @messages[prev_m][3] = m if prev_m
251 if @state[m] != :closed && @chunks.member?(m)
252 @chunks[m].each do |c|
253 @state[c] ||= :closed
254 text = chunk_to_lines c, @state[c], @text.length, depth
255 (0 ... text.length).each do |i|
256 @chunk_lines[@text.length + i] = c
257 @message_lines[@text.length + i] = m
261 @messages[m][1] = @text.length
266 def message_patina_lines m, state, parent, prefix
267 prefix_widget = [:message_patina_color, prefix]
271 [:message_patina_color, "+ "]
272 when :open, :detailed
273 [:message_patina_color, "- "]
276 if m.has_label?(:starred)
277 [:starred_patina_color, "* "]
279 [:message_patina_color, " "]
284 [[prefix_widget, widget, imp_widget,
285 [:message_patina_color,
286 "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
287 # (m.to.empty? ? [] : [[[:message_patina_color, prefix + " To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
289 [[prefix_widget, widget, imp_widget,
290 [:message_patina_color,
291 "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
293 labels = m.labels# - @hidden_labels
294 x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
295 ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
296 (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
297 (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
298 [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
299 [" Subject: #{m.subj}"] +
300 [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
301 [labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
302 ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
308 def break_into_lines prefix, list
309 pad = " " * prefix.length
310 [prefix + list.first + (list.length > 1 ? "," : "")] +
311 list[1 .. -1].map_with_index do |e, i|
312 pad + e + (i == list.length - 1 ? "" : ",")
317 def chunk_to_lines chunk, state, start, depth, parent=nil
321 [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
323 [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
325 message_patina_lines(chunk, state, parent, prefix) +
326 (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
328 when Message::Attachment
329 [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
333 t.pop while t[t.length - 2] =~ /^\s*$/
335 t.map { |line| [[:none, "#{prefix}#{line}"]] }
339 [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
342 [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
343 t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
345 when Message::Signature
348 [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
351 [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
352 t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
355 raise "unknown chunk type #{chunk.class.name}"
359 def view_attachment a
360 BufferManager.flash "viewing #{a.content_type} attachment..."
362 BufferManager.erase_flash
363 BufferManager.completely_redraw_screen