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'
21 def initialize thread, hidden_labels=[]
25 @hidden_labels = hidden_labels
30 @thread.each do |m, d, p|
34 if m.has_label?(:unread) && m == earliest
36 elsif m.has_label?(:starred) || m.has_label?(:unread)
41 if latest_date.nil? || m.date > latest_date
46 @state[latest] = :open if @state[latest] == :closed
52 def draw_line ln, opts={}
54 super ln, :highlight => true
59 def lines; @text.length; end
60 def [] i; @text[i]; end
63 return unless(m = @message_lines[curpos])
64 BufferManager.spawn_unless_exists("Full header") do
65 TextMode.new m.content #m.header_text
69 def toggle_detailed_header
70 return unless(m = @message_lines[curpos])
71 @state[m] = (@state[m] == :detailed ? :open : :detailed)
76 return unless(m = @message_lines[curpos])
77 mode = ReplyMode.new m
78 BufferManager.spawn "Reply to #{m.subj}", mode
82 return unless(m = @message_lines[curpos])
83 mode = ForwardMode.new m
84 BufferManager.spawn "Forward of #{m.subj}", mode
89 return unless(m = @message_lines[curpos])
90 if m.has_label? :starred
91 m.remove_label :starred
95 ## TODO: don't recalculate EVERYTHING just to add a stupid little
96 ## star to the display
98 UpdateManager.relay :starred, m
102 return unless(chunk = @chunk_lines[curpos])
104 when Message, Message::Quote, Message::Signature
105 @state[chunk] = (@state[chunk] != :closed ? :closed : :open)
106 when Message::Attachment
107 view_attachment chunk
113 return unless(m = @message_lines[curpos])
115 mode = ResumeMode.new m
116 BufferManager.spawn "Edit message", mode
118 BufferManager.flash "Not a draft message!"
122 def jump_to_next_open
123 return unless(m = @message_lines[curpos])
124 while nextm = @messages[m][3]
125 break if @state[nextm] == :open
128 jump_to_message nextm if nextm
131 def jump_to_prev_open
132 return unless(m = @message_lines[curpos])
133 ## jump to the top of the current message if we're in the body;
134 ## otherwise, to the previous message
135 top = @messages[m][0]
137 while prevm = @messages[m][2]
138 break if @state[prevm] == :open
141 jump_to_message prevm if prevm
147 def jump_to_message m
148 top, bot, prevm, nextm = @messages[m]
149 jump_to_line top unless top >= topline &&
150 top <= botline && bot >= topline && bot <= botline
154 def expand_all_messages
155 @global_message_state ||= :closed
156 @global_message_state = (@global_message_state == :closed ? :open : :closed)
157 @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
162 def collapse_non_new_messages
163 @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
167 def expand_all_quotes
168 if(m = @message_lines[curpos])
169 quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
170 open, closed = quotes.partition { |c| @state[c] == :open }
171 newstate = open.length > closed.length ? :closed : :open
172 Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}"
173 quotes.each { |c| @state[c] = newstate }
178 ## not sure if this is really necessary but we might as well...
180 @thread.each do |m, d, p|
181 if m.has_label? :unread
182 m.remove_label :unread
183 UpdateManager.relay :read, m
187 Redwood::log "releasing chunks and text from \"#{buffer.title}\""
188 @messages = @chunks = @text = nil
195 buffer.mark_dirty if buffer
200 @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
210 @thread.each do |m, depth, parent|
211 text = chunk_to_lines m, @state[m], @text.length, depth, parent
212 (0 ... text.length).each do |i|
213 @chunk_lines[@text.length + i] = m
214 @message_lines[@text.length + i] = m
217 @messages[m] = [@text.length, @text.length + text.length, prev_m, nil]
218 @messages[prev_m][3] = m if prev_m
222 if @state[m] != :closed && @chunks.member?(m)
223 @chunks[m].each do |c|
224 @state[c] ||= :closed
225 text = chunk_to_lines c, @state[c], @text.length, depth
226 (0 ... text.length).each do |i|
227 @chunk_lines[@text.length + i] = c
228 @message_lines[@text.length + i] = m
232 @messages[m][1] = @text.length
237 def message_patina_lines m, state, parent, prefix
238 prefix_widget = [:message_patina_color, prefix]
242 [:message_patina_color, "+ "]
243 when :open, :detailed
244 [:message_patina_color, "- "]
247 if m.has_label?(:starred)
248 [:starred_patina_color, "* "]
250 [:message_patina_color, " "]
255 [[prefix_widget, widget, imp_widget,
256 [:message_patina_color,
257 "#{m.from ? m.from.mediumname : '?'} to #{m.to.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
258 # (m.to.empty? ? [] : [[[:message_patina_color, prefix + " To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
260 [[prefix_widget, widget, imp_widget,
261 [:message_patina_color,
262 "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
264 labels = m.labels# - @hidden_labels
265 x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
266 ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
267 (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
268 (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
269 [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
270 [" Subject: #{m.subj}"] +
271 [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
272 [labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
273 ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
279 def break_into_lines prefix, list
280 pad = " " * prefix.length
281 [prefix + list.first + (list.length > 1 ? "," : "")] +
282 list[1 .. -1].map_with_index do |e, i|
283 pad + e + (i == list.length - 1 ? "" : ",")
288 def chunk_to_lines chunk, state, start, depth, parent=nil
292 [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
294 [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
296 message_patina_lines(chunk, state, parent, prefix) +
297 (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
299 when Message::Attachment
300 [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
304 t.pop while t[t.length - 2] =~ /^\s*$/
306 t.map { |line| [[:none, "#{prefix}#{line}"]] }
310 [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
313 [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
314 t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
316 when Message::Signature
319 [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
322 [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
323 t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
326 raise "unknown chunk type #{chunk.class.name}"
330 def view_attachment a
331 BufferManager.flash "viewing #{a.content_type} attachment..."
333 BufferManager.erase_flash