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
53 def draw_line ln, opts={}
55 super ln, :highlight => true
60 def lines; @text.length; end
61 def [] i; @text[i]; end
64 return unless(m = @message_lines[curpos])
65 BufferManager.spawn_unless_exists("Full header") do
66 TextMode.new m.raw_header
70 def toggle_detailed_header
71 return unless(m = @message_lines[curpos])
72 @state[m] = (@state[m] == :detailed ? :open : :detailed)
77 return unless(m = @message_lines[curpos])
78 mode = ReplyMode.new m
79 BufferManager.spawn "Reply to #{m.subj}", mode
83 return unless(m = @message_lines[curpos])
84 mode = ForwardMode.new m
85 BufferManager.spawn "Forward of #{m.subj}", mode
90 return unless(m = @message_lines[curpos])
91 if m.has_label? :starred
92 m.remove_label :starred
96 ## TODO: don't recalculate EVERYTHING just to add a stupid little
97 ## star to the display
99 UpdateManager.relay :starred, m
103 return unless(chunk = @chunk_lines[curpos])
105 when Message, Message::Quote, Message::Signature
106 @state[chunk] = (@state[chunk] != :closed ? :closed : :open)
107 when Message::Attachment
108 view_attachment chunk
115 return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
118 File.open(fn, "w") { |f| yield f }
119 BufferManager.flash "Successfully wrote #{fn}."
120 rescue SystemCallError => e
121 BufferManager.flash "Error writing to file: #{e.message}"
127 return unless(chunk = @chunk_lines[curpos])
129 when Message::Attachment
130 fn = BufferManager.ask :filename, "save attachment to file: ", chunk.filename
131 save(fn) { |f| f.print chunk } if fn
133 m = @message_lines[curpos]
134 fn = BufferManager.ask :filename, "save message to file: "
135 save(fn) { |f| f.print m.raw_full_message } if fn
140 return unless(m = @message_lines[curpos])
142 mode = ResumeMode.new m
143 BufferManager.spawn "Edit message", mode
145 BufferManager.flash "Not a draft message!"
149 def jump_to_next_open
150 return unless(m = @message_lines[curpos])
151 while nextm = @messages[m][3]
152 break if @state[nextm] == :open
155 jump_to_message nextm if nextm
158 def jump_to_prev_open
159 return unless(m = @message_lines[curpos])
160 ## jump to the top of the current message if we're in the body;
161 ## otherwise, to the previous message
162 top = @messages[m][0]
164 while prevm = @messages[m][2]
165 break if @state[prevm] == :open
168 jump_to_message prevm if prevm
174 def jump_to_message m
175 top, bot, prevm, nextm = @messages[m]
176 jump_to_line top unless top >= topline &&
177 top <= botline && bot >= topline && bot <= botline
181 def expand_all_messages
182 @global_message_state ||= :closed
183 @global_message_state = (@global_message_state == :closed ? :open : :closed)
184 @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
189 def collapse_non_new_messages
190 @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
194 def expand_all_quotes
195 if(m = @message_lines[curpos])
196 quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
197 open, closed = quotes.partition { |c| @state[c] == :open }
198 newstate = open.length > closed.length ? :closed : :open
199 Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}"
200 quotes.each { |c| @state[c] = newstate }
205 ## not sure if this is really necessary but we might as well...
207 @thread.each do |m, d, p|
208 if m && m.has_label?(:unread)
209 m.remove_label :unread
210 UpdateManager.relay :read, m
214 Redwood::log "releasing chunks and text from \"#{buffer.title}\""
215 @messages = @chunks = @text = nil
222 buffer.mark_dirty if buffer
227 @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
237 @thread.each do |m, depth, parent|
238 text = chunk_to_lines m, @state[m], @text.length, depth, parent
239 (0 ... text.length).each do |i|
240 @chunk_lines[@text.length + i] = m
241 @message_lines[@text.length + i] = m
244 @messages[m] = [@text.length, @text.length + text.length, prev_m, nil]
245 @messages[prev_m][3] = m if prev_m
249 if @state[m] != :closed && @chunks.member?(m)
250 @chunks[m].each do |c|
251 @state[c] ||= :closed
252 text = chunk_to_lines c, @state[c], @text.length, depth
253 (0 ... text.length).each do |i|
254 @chunk_lines[@text.length + i] = c
255 @message_lines[@text.length + i] = m
259 @messages[m][1] = @text.length
264 def message_patina_lines m, state, parent, prefix
265 prefix_widget = [:message_patina_color, prefix]
269 [:message_patina_color, "+ "]
270 when :open, :detailed
271 [:message_patina_color, "- "]
274 if m.has_label?(:starred)
275 [:starred_patina_color, "* "]
277 [:message_patina_color, " "]
282 [[prefix_widget, widget, imp_widget,
283 [:message_patina_color,
284 "#{m.from ? m.from.mediumname : '?'} to #{m.to.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
285 # (m.to.empty? ? [] : [[[:message_patina_color, prefix + " To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
287 [[prefix_widget, widget, imp_widget,
288 [:message_patina_color,
289 "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
291 labels = m.labels# - @hidden_labels
292 x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
293 ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
294 (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
295 (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
296 [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
297 [" Subject: #{m.subj}"] +
298 [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
299 [labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
300 ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
306 def break_into_lines prefix, list
307 pad = " " * prefix.length
308 [prefix + list.first + (list.length > 1 ? "," : "")] +
309 list[1 .. -1].map_with_index do |e, i|
310 pad + e + (i == list.length - 1 ? "" : ",")
315 def chunk_to_lines chunk, state, start, depth, parent=nil
319 [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
321 [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
323 message_patina_lines(chunk, state, parent, prefix) +
324 (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
326 when Message::Attachment
327 [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
331 t.pop while t[t.length - 2] =~ /^\s*$/
333 t.map { |line| [[:none, "#{prefix}#{line}"]] }
337 [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
340 [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
341 t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
343 when Message::Signature
346 [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
349 [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
350 t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
353 raise "unknown chunk type #{chunk.class.name}"
357 def view_attachment a
358 BufferManager.flash "viewing #{a.content_type} attachment..."
360 BufferManager.erase_flash
361 BufferManager.completely_redraw_screen