Class: Redwood::ThreadViewMode

Inherits:
LineCursorMode show all
Includes:
CanAliasContacts
Defined in:
lib/sup/modes/thread-view-mode.rb

Defined Under Namespace

Classes: ChunkLayout, MessageLayout

Constant Summary collapse

DATE_FORMAT =
"%B %e %Y %l:%M%P"
INDENT_SPACES =

how many spaces to indent child messages

2

Constants inherited from ScrollMode

ScrollMode::COL_JUMP

Instance Attribute Summary

Attributes inherited from LineCursorMode

#curpos

Attributes inherited from ScrollMode

#botline, #leftcol, #status, #topline

Attributes inherited from Mode

#buffer

Instance Method Summary collapse

Methods included from CanAliasContacts

#alias_contact

Methods inherited from LineCursorMode

#draw

Methods inherited from ScrollMode

#at_bottom?, #at_top?, #cancel_search!, #col_left, #col_right, #continue_search_in_buffer, #draw, #ensure_mode_validity, #in_search?, #jump_to_col, #jump_to_end, #jump_to_left, #jump_to_line, #jump_to_start, #line_down, #line_up, #page_down, #page_up, #resize, #rightcol, #search_goto_line, #search_in_buffer, #search_start_line

Methods inherited from Mode

#blur, #cancel_search!, #draw, #focus, #handle_input, #help_text, #in_search?, #killable?, load_all_modes, make_name, #name, register_keymap, #resize, #resolve_input, #save_to_file, #status

Constructor Details

#initialize(thread, hidden_labels = []) ⇒ ThreadViewMode

there are a couple important instance variables we hold to format the thread and to provide line-based functionality. @layout is a map from Messages to MessageLayouts, and @chunk_layout from Chunks to ChunkLayouts. @message_lines is a map from row #s to Message objects. @chunk_lines is a map from row #s to Chunk objects. @person_lines is a map from row #s to Person objects.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/sup/modes/thread-view-mode.rb', line 49

def initialize thread, hidden_labels=[]
  super()
  @thread = thread
  @hidden_labels = hidden_labels

  @layout = SavingHash.new { MessageLayout.new }
  @chunk_layout = SavingHash.new { ChunkLayout.new }
  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? :read
    altcolor = !altcolor
    if latest_date.nil? || m.date > latest_date
      latest_date = m.date
      latest = m
    end
  end

  @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

Instance Method Details

#[](i) ⇒ Object



89
# File 'lib/sup/modes/thread-view-mode.rb', line 89

def [] i; @text[i]; end

#activate_chunkObject

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.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/sup/modes/thread-view-mode.rb', line 193

def activate_chunk
  chunk = @chunk_lines[curpos] or return
  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
end

#aliasObject



134
135
136
137
138
# File 'lib/sup/modes/thread-view-mode.rb', line 134

def alias
  p = @person_lines[curpos] or return
  alias_contact p
  update
end

#archive_and_killObject



322
323
324
325
326
# File 'lib/sup/modes/thread-view-mode.rb', line 322

def archive_and_kill
  @thread.remove_label :inbox
  UpdateManager.relay self, :archived, @thread
  BufferManager.kill_buffer_safely buffer
end

#cleanupObject



318
319
320
# File 'lib/sup/modes/thread-view-mode.rb', line 318

def cleanup
  @layout = @chunk_layout = @text = nil # for good luck
end

#collapse_non_new_messagesObject



303
304
305
306
# File 'lib/sup/modes/thread-view-mode.rb', line 303

def collapse_non_new_messages
  @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
  update
end

#composeObject



147
148
149
150
151
152
153
154
# File 'lib/sup/modes/thread-view-mode.rb', line 147

def compose
  p = @person_lines[curpos]
  if p
    ComposeMode.spawn_nicely :to => [p]
  else
    ComposeMode.spawn_nicely
  end
end

#delete_and_killObject



328
329
330
331
332
# File 'lib/sup/modes/thread-view-mode.rb', line 328

def delete_and_kill
  @thread.apply_label :deleted
  UpdateManager.relay self, :deleted, @thread
  BufferManager.kill_buffer_safely buffer
end

#draw_line(ln, opts = {}) ⇒ Object



81
82
83
84
85
86
87
# File 'lib/sup/modes/thread-view-mode.rb', line 81

def draw_line ln, opts={}
  if ln == curpos
    super ln, :highlight => true
  else
    super
  end
end

#edit_as_newObject



210
211
212
213
214
215
# File 'lib/sup/modes/thread-view-mode.rb', line 210

def edit_as_new
  m = @message_lines[curpos] or return
  mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
  BufferManager.spawn "edit as new", mode
  mode.edit_message
end

#edit_draftObject



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/sup/modes/thread-view-mode.rb', line 233

def edit_draft
  m = @message_lines[curpos] or return
  if m.is_draft?
    mode = ResumeMode.new m
    BufferManager.spawn "Edit message", mode
    BufferManager.kill_buffer self.buffer
    mode.edit_message
  else
    BufferManager.flash "Not a draft message!"
  end
end

#edit_labelsObject



156
157
158
159
160
161
162
163
164
165
# File 'lib/sup/modes/thread-view-mode.rb', line 156

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

#expand_all_messagesObject



296
297
298
299
300
301
# File 'lib/sup/modes/thread-view-mode.rb', line 296

def expand_all_messages
  @global_message_state ||= :closed
  @global_message_state = (@global_message_state == :closed ? :open : :closed)
  @layout.each { |m, l| l.state = @global_message_state }
  update
end

#expand_all_quotesObject



308
309
310
311
312
313
314
315
316
# File 'lib/sup/modes/thread-view-mode.rb', line 308

def expand_all_quotes
  if(m = @message_lines[curpos])
    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 }
    update
  end
end

#forwardObject



128
129
130
131
# File 'lib/sup/modes/thread-view-mode.rb', line 128

def forward
  m = @message_lines[curpos] or return
  ForwardMode.spawn_nicely m
end

#jump_to_first_openObject



245
246
247
248
249
250
251
252
# File 'lib/sup/modes/thread-view-mode.rb', line 245

def jump_to_first_open
  m = @message_lines[0] or return
  if @layout[m].state != :closed
    jump_to_message m
  else
    jump_to_next_open
  end
end

#jump_to_message(m) ⇒ Object



281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/sup/modes/thread-view-mode.rb', line 281

def jump_to_message m
  l = @layout[m]
  left = l.depth * INDENT_SPACES
  right = left + l.width

  ## jump to the top line unless both top and bottom fit in the current view
  jump_to_line l.top unless l.top >= topline && l.top <= botline && l.bot >= topline && l.bot <= botline

  ## jump to the left columns unless both left and right fit in the current view
  jump_to_col left unless left >= leftcol && left <= rightcol && right >= leftcol && right <= rightcol

  ## either way, move the cursor to the first line
  set_cursor_pos l.top
end

#jump_to_next_openObject



254
255
256
257
258
259
260
261
262
# File 'lib/sup/modes/thread-view-mode.rb', line 254

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
    m = nextm
  end
  jump_to_message nextm if nextm
end

#jump_to_prev_openObject



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/sup/modes/thread-view-mode.rb', line 264

def jump_to_prev_open
  m = @message_lines[curpos] or return
  ## jump to the top of the current message if we're in the body;
  ## otherwise, to the previous message
  
  top = @layout[m].top
  if curpos == top
    while(prevm = @layout[m].prev)
      break if @layout[prevm].state != :closed
      m = prevm
    end
    jump_to_message prevm if prevm
  else
    jump_to_message m
  end
end

#linesObject



88
# File 'lib/sup/modes/thread-view-mode.rb', line 88

def lines; @text.length; end

#replyObject



104
105
106
107
108
# File 'lib/sup/modes/thread-view-mode.rb', line 104

def reply
  m = @message_lines[curpos] or return
  mode = ReplyMode.new m
  BufferManager.spawn "Reply to #{m.subj}", mode
end

#save_to_diskObject



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/sup/modes/thread-view-mode.rb', line 217

def save_to_disk
  chunk = @chunk_lines[curpos] or return
  case chunk
  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_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

#searchObject



140
141
142
143
144
145
# File 'lib/sup/modes/thread-view-mode.rb', line 140

def search
  p = @person_lines[curpos] or return
  mode = PersonSearchResultsMode.new [p]
  BufferManager.spawn "Search for #{p.name}", mode
  mode.load_threads :num => mode.buffer.content_height
end

#show_headerObject



91
92
93
94
95
96
# File 'lib/sup/modes/thread-view-mode.rb', line 91

def show_header
  m = @message_lines[curpos] or return
  BufferManager.spawn_unless_exists("Full header") do
    TextMode.new m.raw_header
  end
end

#subscribe_to_listObject



110
111
112
113
114
115
116
117
# File 'lib/sup/modes/thread-view-mode.rb', line 110

def subscribe_to_list
  m = @message_lines[curpos] or return
  if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
    ComposeMode.spawn_nicely :from => AccountManager.(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
  else
    BufferManager.flash "Can't find List-Subscribe header for this message."
  end
end

#toggle_detailed_headerObject



98
99
100
101
102
# File 'lib/sup/modes/thread-view-mode.rb', line 98

def toggle_detailed_header
  m = @message_lines[curpos] or return
  @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
  update
end

#toggle_label(m, label) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
# File 'lib/sup/modes/thread-view-mode.rb', line 177

def toggle_label m, label
  if m.has_label? label
    m.remove_label label
  else
    m.add_label label
  end
  ## TODO: don't recalculate EVERYTHING just to add a stupid little
  ## star to the display
  update
  UpdateManager.relay self, :label, m
end

#toggle_newObject



172
173
174
175
# File 'lib/sup/modes/thread-view-mode.rb', line 172

def toggle_new
  m = @message_lines[curpos] or return
  toggle_label m, :unread
end

#toggle_starredObject



167
168
169
170
# File 'lib/sup/modes/thread-view-mode.rb', line 167

def toggle_starred
  m = @message_lines[curpos] or return
  toggle_label m, :starred
end

#unsubscribe_from_listObject



119
120
121
122
123
124
125
126
# File 'lib/sup/modes/thread-view-mode.rb', line 119

def unsubscribe_from_list
  m = @message_lines[curpos] or return
  if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
    ComposeMode.spawn_nicely :from => AccountManager.(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
  else
    BufferManager.flash "Can't find List-Unsubscribe header for this message."
  end
end