]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-index-mode.rb
fixed toggling of starred, new, etc.
[sup] / lib / sup / modes / thread-index-mode.rb
1 require 'thread'
2 module Redwood
3
4 ## subclasses should implement load_threads
5
6 class ThreadIndexMode < LineCursorMode
7   DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
8   FROM_WIDTH = 15
9   LOAD_MORE_THREAD_NUM = 20
10
11   register_keymap do |k|
12     k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
13     k.add :reload, "Discard threads and reload", 'D'
14     k.add :toggle_archived, "Toggle archived status", 'a'
15     k.add :toggle_starred, "Star or unstar all messages in thread", '*'
16     k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
17     k.add :edit_labels, "Edit or add labels for a thread", 'l'
18     k.add :edit_message, "Edit message (drafts only)", 'e'
19     k.add :mark_as_spam, "Mark thread as spam", 'S'
20     k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
21     k.add :save, "Save changes now", '$'
22     k.add :jump_to_next_new, "Jump to next new thread", :tab
23     k.add :reply, "Reply to a thread", 'r'
24     k.add :forward, "Forward a thread", 'f'
25     k.add :toggle_tagged, "Tag/untag current line", 't'
26     k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
27   end
28
29   def initialize required_labels=[], hidden_labels=[]
30     super()
31     @load_thread = nil
32     @required_labels = required_labels
33     @hidden_labels = hidden_labels + LabelManager::HIDDEN_LABELS
34     @date_width = DATE_WIDTH
35     @from_width = FROM_WIDTH
36     @size_width = nil
37
38     @tags = Tagger.new self
39     
40     initialize_threads
41     update
42
43     UpdateManager.register self
44   end
45
46   def lines; @text.length; end
47   def [] i; @text[i]; end
48
49   def reload
50     drop_all_threads
51     BufferManager.draw_screen
52     load_threads :num => buffer.content_height
53   end
54
55   ## open up a thread view window
56   def select t=nil
57     t ||= @threads[curpos]
58
59     ## TODO: don't regen text completely
60     Redwood::reporting_thread do
61       mode = ThreadViewMode.new t, @hidden_labels
62       BufferManager.spawn t.subj, mode
63       BufferManager.draw_screen
64     end
65   end
66
67   def multi_select threads
68     threads.each { |t| select t }
69   end
70   
71   def handle_starred_update m
72     return unless(t = @ts.thread_for m)
73     update_text_for_line @lines[t]
74   end
75
76   def handle_read_update m
77     return unless(t = @ts.thread_for m)
78     update_text_for_line @lines[t]
79   end
80
81   ## overwrite me!
82   def is_relevant? m; false; end
83
84   def handle_add_update m
85     if is_relevant?(m) || @ts.is_relevant?(m)
86       @ts.load_thread_for_message m
87       update
88     end
89   end
90
91   def handle_delete_update mid
92     if @ts.contains_id? mid
93       @ts.remove mid
94       update
95     end
96   end
97
98   def update
99     ## let's see you do THIS in python
100     @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
101     @size_width = (@threads.map { |t| t.size }.max || 0).num_digits
102     regen_text
103   end
104
105   def edit_message
106     return unless(t = @threads[curpos])
107     message, *crap = t.find { |m, *o| m.has_label? :draft }
108     if message
109       mode = ResumeMode.new message
110       BufferManager.spawn "Edit message", mode
111     else
112       BufferManager.flash "Not a draft message!"
113     end
114   end
115
116   ## i always hate people who name things like this...
117   def actually_toggle_starred t=@threads[curpos]
118     if t.has_label? :starred # if ANY message has a star
119       t.remove_label :starred # remove from all
120     else
121       t.first.add_label :starred # add only to first
122     end
123   end  
124
125   def toggle_starred 
126     actually_toggle_starred
127     update_text_for_line curpos
128     cursor_down
129   end
130
131   def multi_toggle_starred threads
132     threads.each { |t| actually_toggle_starred t }
133     regen_text
134   end
135
136   def toggle_archived 
137     t = @threads[curpos] or return
138     t.toggle_label :inbox
139     update_text_for_line curpos
140     cursor_down
141   end
142
143   def multi_toggle_archived threads
144     threads.each { |t| t.toggle_label :inbox }
145     regen_text
146   end
147
148   def toggle_new
149     t = @threads[curpos] or return
150     t.toggle_label :unread
151     update_text_for_line curpos
152     cursor_down
153   end
154
155   def multi_toggle_new threads
156     threads.each { |t| t.toggle_label :unread }
157     regen_text
158   end
159
160   def multi_toggle_tagged threads
161     @tags.drop_all_tags
162     regen_text
163   end
164
165   def jump_to_next_new
166     t = @threads[curpos] or return
167     n = ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread }
168     n = (0 ... curpos).find { |i| @threads[i].has_label? :unread } unless n
169     if n
170       set_cursor_pos n
171     else
172       BufferManager.flash "No new messages"
173     end
174   end
175
176   def mark_as_spam
177     t = @threads[curpos] or return
178     multi_mark_as_spam [t]
179   end
180
181   def multi_mark_as_spam threads
182     threads.each do |t|
183       t.toggle_label :spam
184       hide_thread t
185     end
186     regen_text
187   end
188
189   def kill
190     t = @threads[curpos] or return
191     multi_kill [t]
192   end
193
194   def multi_kill threads
195     threads.each do |t|
196       t.apply_label :killed
197       hide_thread t
198     end
199     regen_text
200   end
201
202   def save
203     threads = @threads + @hidden_threads.keys
204
205     BufferManager.say("Saving threads...") do |say_id|
206       threads.each_with_index do |t, i|
207         next unless t.dirty?
208         BufferManager.say "Saving thread #{i +1} of #{threads.length}...", say_id
209         t.save Index
210       end
211     end
212   end
213
214   def cleanup
215     UpdateManager.unregister self
216
217     if @load_thread
218       @load_thread.kill 
219       BufferManager.clear @mbid if @mbid
220       sleep 0.1 # TODO: necessary?
221       BufferManager.erase_flash
222     end
223     save
224     super
225   end
226
227   def toggle_tagged
228     t = @threads[curpos] or return
229     @tags.toggle_tag_for t
230     update_text_for_line curpos
231     cursor_down
232   end
233
234   def apply_to_tagged; @tags.apply_to_tagged; end
235
236   def edit_labels
237     thread = @threads[curpos]
238     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
239     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
240     label_string = modifyl.join(" ")
241
242     answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
243     return unless answer
244     user_labels = answer.split(/\s+/).map { |l| l.intern }
245     
246     hl = user_labels.select { |l| speciall.member? l }
247     if hl.empty?
248       thread.labels = keepl + user_labels
249       user_labels.each { |l| LabelManager << l }
250     else
251       BufferManager.flash "'#{hl}' is a reserved label!"
252     end
253     update_text_for_line curpos
254   end
255
256   def multi_edit_labels threads
257     answer = BufferManager.ask :add_labels, "add labels: "
258     return unless answer
259     user_labels = answer.split(/\s+/).map { |l| l.intern }
260     
261     hl = user_labels.select { |l| @hidden_labels.member? l }
262     if hl.empty?
263       threads.each { |t| user_labels.each { |l| t.apply_label l } }
264       user_labels.each { |l| LabelManager << l }
265     else
266       BufferManager.flash "'#{hl}' is a reserved label!"
267     end
268     regen_text
269   end
270
271   def reply
272     t = @threads[curpos] or return
273     m = t.latest_message
274     return if m.nil? # probably won't happen
275     mode = ReplyMode.new m
276     BufferManager.spawn "Reply to #{m.subj}", mode
277   end
278
279   def forward
280     t = @threads[curpos] or return
281     m = t.latest_message
282     return if m.nil? # probably won't happen
283     mode = ForwardMode.new m
284     BufferManager.spawn "Forward of #{m.subj}", mode
285     mode.edit
286   end
287
288   def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
289     return if @load_thread
290     @load_thread = Redwood::reporting_thread do 
291       num = load_n_threads n, opts
292       opts[:when_done].call(num) if opts[:when_done]
293       @load_thread = nil
294     end
295   end
296
297   def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
298     @mbid = BufferManager.say "Searching for threads..."
299     orig_size = @ts.size
300     @ts.load_n_threads(@ts.size + n, opts) do |i|
301       BufferManager.say "Loaded #{i} threads...", @mbid
302       if i % 5 == 0
303         update
304         BufferManager.draw_screen
305       end
306     end
307     @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
308
309     update
310     BufferManager.clear @mbid
311     @mbid = nil
312     BufferManager.draw_screen
313     @ts.size - orig_size
314   end
315
316   def status
317     if (l = lines) == 0
318       ""
319     else
320       "line #{curpos + 1} of #{l} #{dirty? ? '*modified*' : ''}"
321     end
322   end
323
324 protected
325
326   def cursor_thread; @threads[curpos]; end
327
328   def drop_all_threads
329     @tags.drop_all_tags
330     initialize_threads
331     update
332   end
333
334   def remove_label_and_hide_thread t, label
335     t.remove_label label
336     hide_thread t
337   end
338
339   def hide_thread t
340     raise "already hidden" if @hidden_threads[t]
341     @hidden_threads[t] = true
342     @threads.delete t
343     @tags.drop_tag_for t
344   end
345
346   def update_text_for_line l
347     return unless l # not sure why this happens, but it does, occasionally
348     @text[l] = text_for_thread @threads[l]
349     buffer.mark_dirty if buffer
350   end
351
352   def regen_text
353     @text = @threads.map_with_index { |t, i| text_for_thread t }
354     @lines = @threads.map_with_index { |t, i| [t, i] }.to_h
355     buffer.mark_dirty if buffer
356   end
357   
358   def author_text_for_thread t
359     if t.authors.size == 1
360       t.authors.first.mediumname
361     else
362       t.authors.map { |p| AccountManager.is_account?(p) ? "me" : p.shortname }.join ", "
363     end
364   end
365
366   def text_for_thread t
367     date = t.date.to_nice_s(Time.now)
368     from = author_text_for_thread(t)
369     if from.length > @from_width
370       from = from[0 ... (@from_width - 1)]
371       from += "." unless from[-1] == ?\s
372     end
373
374     new = t.has_label?(:unread)
375     starred = t.has_label?(:starred)
376
377     dp = t.direct_participants.any? { |p| AccountManager.is_account? p }
378     p = dp || t.participants.any? { |p| AccountManager.is_account? p }
379
380     base_color = (new ? :index_new_color : :index_old_color)
381     [ 
382       [:tagged_color, @tags.tagged?(t) ? ">" : " "],
383       [:none, sprintf("%#{@date_width}s ", date)],
384       [base_color, sprintf("%-#{@from_width}s", from)],
385       [:starred_color, starred ? "*" : " "],
386       [:none, t.size == 1 ? " " * (@size_width + 2) : sprintf("(%#{@size_width}d)", t.size)],
387       [:to_me_color, dp ? " >" : (p ? ' +' : "  ")],
388       [base_color, t.subj + (t.subj.empty? ? "" : " ")],
389     ] +
390       (t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
391       [[:snippet_color, t.snippet]
392     ]
393   end
394
395   def dirty?; (@hidden_threads.keys + @threads).any? { |t| t.dirty? }; end
396
397 private
398
399   def initialize_threads
400     @ts = ThreadSet.new Index.instance
401     @ts_mutex = Mutex.new
402     @hidden_threads = {}
403   end
404 end
405
406 end