lib/sup/modes/thread_index_mode.rb (29521B) - raw
1 require 'set'
2
3 module Redwood
4
5 ## subclasses should implement:
6 ## - is_relevant?
7
8 class ThreadIndexMode < LineCursorMode
9 DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
10 MIN_FROM_WIDTH = 15
11 LOAD_MORE_THREAD_NUM = 20
12
13 HookManager.register "index-mode-size-widget", <<EOS
14 Generates the per-thread size widget for each thread.
15 Variables:
16 thread: The message thread to be formatted.
17 EOS
18
19 HookManager.register "index-mode-date-widget", <<EOS
20 Generates the per-thread date widget for each thread.
21 Variables:
22 thread: The message thread to be formatted.
23 EOS
24
25 HookManager.register "mark-as-spam", <<EOS
26 This hook is run when a thread is marked as spam
27 Variables:
28 thread: The message thread being marked as spam.
29 EOS
30
31 register_keymap do |k|
32 k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
33 k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
34 kk.add :load_all_threads, "Load all threads (may list a _lot_ of threads)", '!'
35 end
36 k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
37 k.add :cancel_search, "Cancel current search", :ctrl_g
38 k.add :reload, "Refresh view", '@'
39 k.add :toggle_archived, "Toggle archived status", 'a'
40 k.add :toggle_starred, "Star or unstar all messages in thread", '*'
41 k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
42 k.add :edit_labels, "Edit or add labels for a thread", 'l'
43 k.add :edit_message, "Edit message (drafts only)", 'e'
44 k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
45 k.add :toggle_deleted, "Delete/undelete thread", 'd'
46 k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
47 k.add :flush_index, "Flush all changes now", '$'
48 k.add :jump_to_next_new, "Jump to next new thread", :tab
49 k.add :reply, "Reply to latest message in a thread", 'r'
50 k.add :reply_all, "Reply to all participants of the latest message in a thread", 'G'
51 k.add :forward, "Forward latest message in a thread", 'f'
52 k.add :toggle_tagged, "Tag/untag selected thread", 't'
53 k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
54 k.add :tag_matching, "Tag matching threads", 'g'
55 k.add :apply_to_tagged, "Apply next command to all tagged threads", '+', '='
56 k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
57 k.add :undo, "Undo the previous action", 'u'
58 end
59
60 def initialize hidden_labels=[], load_thread_opts={}
61 super()
62 @mutex = Mutex.new # covers the following variables:
63 @threads = []
64 @hidden_threads = {}
65 @size_widget_width = nil
66 @size_widgets = []
67 @date_widget_width = nil
68 @date_widgets = []
69 @tags = Tagger.new self
70
71 ## these guys, and @text and @lines, are not covered
72 @load_thread = nil
73 @load_thread_opts = load_thread_opts
74 @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
75 @date_width = DATE_WIDTH
76
77 @interrupt_search = false
78
79 initialize_threads # defines @ts and @ts_mutex
80 update # defines @text and @lines
81
82 UpdateManager.register self
83
84 @save_thread_mutex = Mutex.new
85
86 @last_load_more_size = nil
87 to_load_more do |size|
88 next if @last_load_more_size == 0
89 load_threads :num => size,
90 :when_done => lambda { |num| @last_load_more_size = num }
91 end
92 end
93
94 def unsaved?; dirty? end
95 def lines; @text.length; end
96 def [] i; @text[i]; end
97 def contains_thread? t; @threads.include?(t) end
98
99 def reload
100 drop_all_threads
101 UndoManager.clear
102 BufferManager.draw_screen
103 load_threads :num => buffer.content_height
104 end
105
106 ## open up a thread view window
107 def select t=nil, when_done=nil
108 t ||= cursor_thread or return
109
110 Redwood::reporting_thread("load messages for thread-view-mode") do
111 num = t.size
112 message = "Loading #{num.pluralize 'message body'}..."
113 BufferManager.say(message) do |sid|
114 t.each_with_index do |(m, *_), i|
115 next unless m
116 BufferManager.say "#{message} (#{i}/#{num})", sid if t.size > 1
117 m.load_from_source!
118 end
119 end
120 mode = ThreadViewMode.new t, @hidden_labels, self
121 BufferManager.spawn t.subj, mode
122 BufferManager.draw_screen
123 mode.jump_to_first_open if $config[:jump_to_open_message]
124 BufferManager.draw_screen # lame TODO: make this unnecessary
125 ## the first draw_screen is needed before topline and botline
126 ## are set, and the second to show the cursor having moved
127
128 t.remove_label :unread
129 Index.save_thread t
130
131 update_text_for_line curpos
132 UpdateManager.relay self, :read, t.first
133 when_done.call if when_done
134 end
135 end
136
137 def multi_select threads
138 threads.each { |t| select t }
139 end
140
141 ## these two methods are called by thread-view-modes when the user
142 ## wants to view the previous/next thread without going back to
143 ## index-mode. we update the cursor as a convenience.
144 def launch_next_thread_after thread, &b
145 launch_another_thread thread, 1, &b
146 end
147
148 def launch_prev_thread_before thread, &b
149 launch_another_thread thread, -1, &b
150 end
151
152 def launch_another_thread thread, direction, &b
153 l = @lines[thread] or return
154 target_l = l + direction
155 t = @mutex.synchronize do
156 if target_l >= 0 && target_l < @threads.length
157 @threads[target_l]
158 end
159 end
160
161 if t # there's a next thread
162 set_cursor_pos target_l # move out of mutex?
163 select t, b
164 elsif b # no next thread. call the block anyways
165 b.call
166 end
167 end
168
169 def handle_single_message_labeled_update sender, m
170 ## no need to do anything different here; we don't differentiate
171 ## messages from their containing threads
172 handle_labeled_update sender, m
173 end
174
175 def handle_labeled_update sender, m
176 if(t = thread_containing(m))
177 l = @lines[t] or return
178 update_text_for_line l
179 elsif is_relevant?(m)
180 add_or_unhide m
181 end
182 end
183
184 def handle_simple_update sender, m
185 t = thread_containing(m) or return
186 l = @lines[t] or return
187 update_text_for_line l
188 end
189
190 %w(read unread archived starred unstarred).each do |state|
191 define_method "handle_#{state}_update" do |*a|
192 handle_simple_update(*a)
193 end
194 end
195
196 ## overwrite me!
197 def is_relevant? m; false; end
198
199 def handle_added_update sender, m
200 add_or_unhide m
201 BufferManager.draw_screen
202 end
203
204 def handle_updated_update sender, m
205 t = thread_containing(m) or return
206 l = @lines[t] or return
207 @ts_mutex.synchronize do
208 @ts.delete_message m
209 @ts.add_message m
210 end
211 Index.save_thread t, sync_back = false
212 update_text_for_line l
213 end
214
215 def handle_location_deleted_update sender, m
216 t = thread_containing(m)
217 delete_thread t if t and t.first.id == m.id
218 @ts_mutex.synchronize do
219 @ts.delete_message m if t
220 end
221 update
222 end
223
224 def handle_single_message_deleted_update sender, m
225 @ts_mutex.synchronize do
226 return unless @ts.contains? m
227 @ts.remove_id m.id
228 end
229 update
230 end
231
232 def handle_deleted_update sender, m
233 t = @ts_mutex.synchronize { @ts.thread_for m }
234 return unless t
235 hide_thread t
236 update
237 end
238
239 def handle_killed_update sender, m
240 t = @ts_mutex.synchronize { @ts.thread_for m }
241 return unless t
242 hide_thread t
243 update
244 end
245
246 def handle_spammed_update sender, m
247 t = @ts_mutex.synchronize { @ts.thread_for m }
248 return unless t
249 hide_thread t
250 update
251 end
252
253 def handle_undeleted_update sender, m
254 add_or_unhide m
255 end
256
257 def handle_unkilled_update sender, m
258 add_or_unhide m
259 end
260
261 def undo
262 UndoManager.undo
263 end
264
265 def update
266 old_cursor_thread = cursor_thread
267 @mutex.synchronize do
268 ## let's see you do THIS in python
269 @threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
270 @size_widgets = @threads.map { |t| size_widget_for_thread t }
271 @size_widget_width = @size_widgets.max_of { |w| w.display_length }
272 @date_widgets = @threads.map { |t| date_widget_for_thread t }
273 @date_widget_width = @date_widgets.max_of { |w| w.display_length }
274 end
275 set_cursor_pos @threads.index(old_cursor_thread)||curpos
276
277 regen_text
278 end
279
280 def edit_message
281 return unless(t = cursor_thread)
282 message, *_ = t.find { |m, *o| m.has_label? :draft }
283 if message
284 mode = ResumeMode.new message
285 BufferManager.spawn "Edit message", mode
286 else
287 BufferManager.flash "Not a draft message!"
288 end
289 end
290
291 ## returns an undo lambda
292 def actually_toggle_starred t
293 if t.has_label? :starred # if ANY message has a star
294 t.remove_label :starred # remove from all
295 UpdateManager.relay self, :unstarred, t.first
296 lambda do
297 t.first.add_label :starred
298 UpdateManager.relay self, :starred, t.first
299 regen_text
300 end
301 else
302 t.first.add_label :starred # add only to first
303 UpdateManager.relay self, :starred, t.first
304 lambda do
305 t.remove_label :starred
306 UpdateManager.relay self, :unstarred, t.first
307 regen_text
308 end
309 end
310 end
311
312 def toggle_starred
313 t = cursor_thread or return
314 undo = actually_toggle_starred t
315 UndoManager.register "toggling thread starred status", undo, lambda { Index.save_thread t }
316 update_text_for_line curpos
317 cursor_down
318 Index.save_thread t
319 end
320
321 def multi_toggle_starred threads
322 UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
323 threads.map { |t| actually_toggle_starred t },
324 lambda { threads.each { |t| Index.save_thread t } }
325 regen_text
326 threads.each { |t| Index.save_thread t }
327 end
328
329 ## returns an undo lambda
330 def actually_toggle_archived t
331 thread = t
332 pos = curpos
333 if t.has_label? :inbox
334 t.remove_label :inbox
335 UpdateManager.relay self, :archived, t.first
336 lambda do
337 thread.apply_label :inbox
338 update_text_for_line pos
339 UpdateManager.relay self,:unarchived, thread.first
340 end
341 else
342 t.apply_label :inbox
343 UpdateManager.relay self, :unarchived, t.first
344 lambda do
345 thread.remove_label :inbox
346 update_text_for_line pos
347 UpdateManager.relay self, :unarchived, thread.first
348 end
349 end
350 end
351
352 ## returns an undo lambda
353 def actually_toggle_spammed t
354 thread = t
355 if t.has_label? :spam
356 t.remove_label :spam
357 add_or_unhide t.first
358 UpdateManager.relay self, :unspammed, t.first
359 lambda do
360 thread.apply_label :spam
361 self.hide_thread thread
362 UpdateManager.relay self,:spammed, thread.first
363 end
364 else
365 t.apply_label :spam
366 hide_thread t
367 UpdateManager.relay self, :spammed, t.first
368 lambda do
369 thread.remove_label :spam
370 add_or_unhide thread.first
371 UpdateManager.relay self,:unspammed, thread.first
372 end
373 end
374 end
375
376 ## returns an undo lambda
377 def actually_toggle_deleted t
378 if t.has_label? :deleted
379 t.remove_label :deleted
380 add_or_unhide t.first
381 UpdateManager.relay self, :undeleted, t.first
382 lambda do
383 t.apply_label :deleted
384 hide_thread t
385 UpdateManager.relay self, :deleted, t.first
386 end
387 else
388 t.apply_label :deleted
389 hide_thread t
390 UpdateManager.relay self, :deleted, t.first
391 lambda do
392 t.remove_label :deleted
393 add_or_unhide t.first
394 UpdateManager.relay self, :undeleted, t.first
395 end
396 end
397 end
398
399 def toggle_archived
400 t = cursor_thread or return
401 undo = actually_toggle_archived t
402 UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos },
403 lambda { Index.save_thread t }
404 update_text_for_line curpos
405 Index.save_thread t
406 end
407
408 def multi_toggle_archived threads
409 undos = threads.map { |t| actually_toggle_archived t }
410 UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text },
411 lambda { threads.each { |t| Index.save_thread t } }
412 regen_text
413 threads.each { |t| Index.save_thread t }
414 end
415
416 def toggle_new
417 t = cursor_thread or return
418 t.toggle_label :unread
419 update_text_for_line curpos
420 cursor_down
421 Index.save_thread t
422 end
423
424 def multi_toggle_new threads
425 threads.each { |t| t.toggle_label :unread }
426 regen_text
427 threads.each { |t| Index.save_thread t }
428 end
429
430 def multi_toggle_tagged threads
431 @mutex.synchronize { @tags.drop_all_tags }
432 regen_text
433 end
434
435 def join_threads
436 ## this command has no non-tagged form. as a convenience, allow this
437 ## command to be applied to tagged threads without hitting ';'.
438 @tags.apply_to_tagged :join_threads
439 end
440
441 def multi_join_threads threads
442 @ts.join_threads threads or return
443 threads.each { |t| Index.save_thread t }
444 @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
445 update
446 end
447
448 def jump_to_next_new
449 n = @mutex.synchronize do
450 ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
451 (0 ... curpos).find { |i| @threads[i].has_label? :unread }
452 end
453 if n
454 ## jump there if necessary
455 jump_to_line n unless n >= topline && n < botline
456 set_cursor_pos n
457 else
458 BufferManager.flash "No new messages."
459 end
460 end
461
462 def toggle_spam
463 t = cursor_thread or return
464 multi_toggle_spam [t]
465 end
466
467 ## both spam and deleted have the curious characteristic that you
468 ## always want to hide the thread after either applying or removing
469 ## that label. in all thread-index-views except for
470 ## label-search-results-mode, when you mark a message as spam or
471 ## deleted, you want it to disappear immediately; in LSRM, you only
472 ## see deleted or spam emails, and when you undelete or unspam them
473 ## you also want them to disappear immediately.
474 def multi_toggle_spam threads
475 undos = threads.map { |t| actually_toggle_spammed t }
476 threads.each { |t| HookManager.run("mark-as-spam", :thread => t) }
477 UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
478 undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
479 regen_text
480 threads.each { |t| Index.save_thread t }
481 end
482
483 def toggle_deleted
484 t = cursor_thread or return
485 multi_toggle_deleted [t]
486 end
487
488 ## see comment for multi_toggle_spam
489 def multi_toggle_deleted threads
490 undos = threads.map { |t| actually_toggle_deleted t }
491 UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
492 undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
493 regen_text
494 threads.each { |t| Index.save_thread t }
495 end
496
497 def kill
498 t = cursor_thread or return
499 multi_kill [t]
500 end
501
502 def flush_index
503 @flush_id = BufferManager.say "Flushing index..."
504 Index.save_index
505 BufferManager.clear @flush_id
506 end
507
508 ## m-m-m-m-MULTI-KILL
509 def multi_kill threads
510 UndoManager.register "killing/unkilling #{threads.size.pluralize 'threads'}" do
511 threads.each do |t|
512 if t.toggle_label :killed
513 add_or_unhide t.first
514 else
515 hide_thread t
516 end
517 end.each do |t|
518 UpdateManager.relay self, :labeled, t.first
519 Index.save_thread t
520 end
521 regen_text
522 end
523
524 threads.each do |t|
525 if t.toggle_label :killed
526 hide_thread t
527 else
528 add_or_unhide t.first
529 end
530 end.each do |t|
531 # send 'labeled'... this might be more specific
532 UpdateManager.relay self, :labeled, t.first
533 Index.save_thread t
534 end
535
536 killed, unkilled = threads.partition { |t| t.has_label? :killed }.map(&:size)
537 BufferManager.flash "#{killed.pluralize 'thread'} killed, #{unkilled} unkilled"
538 regen_text
539 end
540
541 def cleanup
542 UpdateManager.unregister self
543
544 if @load_thread
545 @load_thread.kill
546 BufferManager.clear @mbid if @mbid
547 sleep 0.1 # TODO: necessary?
548 BufferManager.erase_flash
549 end
550 dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
551 fail "dirty threads remain" unless dirty_threads.empty?
552 super
553 end
554
555 def toggle_tagged
556 t = cursor_thread or return
557 @mutex.synchronize { @tags.toggle_tag_for t }
558 update_text_for_line curpos
559 cursor_down
560 end
561
562 def toggle_tagged_all
563 @mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } }
564 regen_text
565 end
566
567 def tag_matching
568 query = BufferManager.ask :search, "tag threads matching (regex): "
569 return if query.nil? || query.empty?
570 query = begin
571 /#{query}/i
572 rescue RegexpError => e
573 BufferManager.flash "error interpreting '#{query}': #{e.message}"
574 return
575 end
576 @mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } }
577 regen_text
578 end
579
580 def apply_to_tagged; @tags.apply_to_tagged; end
581
582 def edit_labels
583 thread = cursor_thread or return
584 speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
585
586 old_labels = thread.labels
587 pos = curpos
588
589 keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
590
591 user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl.sort_by {|x| x.to_s}, @hidden_labels
592 return unless user_labels
593
594 thread.labels = Set.new(keepl) + user_labels
595 user_labels.each { |l| LabelManager << l }
596 update_text_for_line curpos
597
598 UndoManager.register "labeling thread" do
599 thread.labels = old_labels
600 update_text_for_line pos
601 UpdateManager.relay self, :labeled, thread.first
602 Index.save_thread thread
603 end
604
605 UpdateManager.relay self, :labeled, thread.first
606 Index.save_thread thread
607 end
608
609 def multi_edit_labels threads
610 user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
611 return unless user_labels
612
613 user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
614 hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
615 unless hl.empty?
616 BufferManager.flash "'#{hl}' is a reserved label!"
617 return
618 end
619
620 old_labels = threads.map { |t| t.labels.dup }
621
622 threads.each do |t|
623 user_labels.each do |(l, to_remove)|
624 if to_remove
625 t.remove_label l
626 else
627 t.apply_label l
628 LabelManager << l
629 end
630 end
631 UpdateManager.relay self, :labeled, t.first
632 end
633
634 regen_text
635
636 UndoManager.register "labeling #{threads.size.pluralize 'thread'}" do
637 threads.zip(old_labels).map do |t, old_labels|
638 t.labels = old_labels
639 UpdateManager.relay self, :labeled, t.first
640 Index.save_thread t
641 end
642 regen_text
643 end
644
645 threads.each { |t| Index.save_thread t }
646 end
647
648 def reply type_arg=nil
649 t = cursor_thread or return
650 m = t.latest_message
651 return if m.nil? # probably won't happen
652 m.load_from_source!
653 mode = ReplyMode.new m, type_arg
654 BufferManager.spawn "Reply to #{m.subj}", mode
655 end
656
657 def reply_all; reply :all; end
658
659 def forward
660 t = cursor_thread or return
661 m = t.latest_message
662 return if m.nil? # probably won't happen
663 m.load_from_source!
664 ForwardMode.spawn_nicely :message => m
665 end
666
667 def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
668 return if @load_thread # todo: wrap in mutex
669 @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
670 num = load_n_threads n, opts
671 opts[:when_done].call(num) if opts[:when_done]
672 @load_thread = nil
673 end
674 end
675
676 ## TODO: figure out @ts_mutex in this method
677 def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
678 @interrupt_search = false
679 @mbid = BufferManager.say "Searching for threads..."
680
681 ts_to_load = n
682 ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads
683
684 orig_size = @ts.size
685 last_update = Time.now
686 @ts.load_n_threads(ts_to_load, opts) do |i|
687 if (Time.now - last_update) >= 0.25
688 BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
689 update
690 BufferManager.draw_screen
691 last_update = Time.now
692 end
693 ::Thread.pass
694 break if @interrupt_search
695 end
696 @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
697
698 update
699 BufferManager.clear @mbid if @mbid
700 @mbid = nil
701 BufferManager.draw_screen
702 @ts.size - orig_size
703 end
704 ignore_concurrent_calls :load_n_threads
705
706 def status
707 if (l = lines) == 0
708 "line 0 of 0"
709 else
710 "line #{curpos + 1} of #{l}"
711 end
712 end
713
714 def cancel_search
715 @interrupt_search = true
716 end
717
718 def load_all_threads
719 load_threads :num => -1
720 end
721
722 def load_threads opts={}
723 if opts[:num].nil?
724 n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
725 else
726 n = opts[:num]
727 end
728
729 myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
730 opts[:when_done].call(num) if opts[:when_done]
731
732 if num > 0
733 BufferManager.flash "Found #{num.pluralize 'thread'}."
734 else
735 BufferManager.flash "No matches."
736 end
737 end)})
738
739 if opts[:background] || opts[:background].nil?
740 load_n_threads_background n, myopts
741 else
742 load_n_threads n, myopts
743 end
744 end
745 ignore_concurrent_calls :load_threads
746
747 def read_and_archive
748 return unless cursor_thread
749 thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
750
751 was_unread = thread.labels.member? :unread
752 UndoManager.register "reading and archiving thread" do
753 thread.apply_label :inbox
754 thread.apply_label :unread if was_unread
755 add_or_unhide thread.first
756 Index.save_thread thread
757 end
758
759 cursor_thread.remove_label :unread
760 cursor_thread.remove_label :inbox
761 hide_thread cursor_thread
762 regen_text
763 Index.save_thread thread
764 end
765
766 def multi_read_and_archive threads
767 old_labels = threads.map { |t| t.labels.dup }
768
769 threads.each do |t|
770 t.remove_label :unread
771 t.remove_label :inbox
772 hide_thread t
773 end
774 regen_text
775
776 UndoManager.register "reading and archiving #{threads.size.pluralize 'thread'}" do
777 threads.zip(old_labels).each do |t, l|
778 t.labels = l
779 add_or_unhide t.first
780 Index.save_thread t
781 end
782 regen_text
783 end
784
785 threads.each { |t| Index.save_thread t }
786 end
787
788 def resize rows, cols
789 regen_text
790 super
791 end
792
793 protected
794
795 def add_or_unhide m
796 @ts_mutex.synchronize do
797 if (is_relevant?(m) || @ts.is_relevant?(m)) && !@ts.contains?(m)
798 @ts.load_thread_for_message m, @load_thread_opts
799 end
800
801 @hidden_threads.delete @ts.thread_for(m)
802 end
803
804 update
805 end
806
807 def thread_containing m; @ts_mutex.synchronize { @ts.thread_for m } end
808
809 ## used to tag threads by query. this can be made a lot more sophisticated,
810 ## but for right now we'll do the obvious this.
811 def thread_matches? t, query
812 t.subj =~ query || t.snippet =~ query || t.participants.any? { |x| x.longname =~ query }
813 end
814
815 def size_widget_for_thread t
816 HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
817 end
818
819 def date_widget_for_thread t
820 HookManager.run("index-mode-date-widget", :thread => t) || default_date_widget_for(t)
821 end
822
823 def cursor_thread; @mutex.synchronize { @threads[curpos] }; end
824
825 def drop_all_threads
826 @tags.drop_all_tags
827 initialize_threads
828 update
829 end
830
831 def delete_thread t
832 @mutex.synchronize do
833 i = @threads.index(t) or return
834 @threads.delete_at i
835 @size_widgets.delete_at i
836 @date_widgets.delete_at i
837 @tags.drop_tag_for t
838 end
839 end
840
841 def hide_thread t
842 @mutex.synchronize do
843 i = @threads.index(t) or return
844 raise "already hidden" if @hidden_threads[t]
845 @hidden_threads[t] = true
846 @threads.delete_at i
847 @size_widgets.delete_at i
848 @date_widgets.delete_at i
849 @tags.drop_tag_for t
850 end
851 end
852
853 def update_text_for_line l
854 return unless l # not sure why this happens, but it does, occasionally
855
856 need_update = false
857
858 @mutex.synchronize do
859 # and certainly not sure why this happens..
860 #
861 # probably a race condition between thread modification and updating
862 # going on.
863 return if @threads[l].empty?
864
865 @size_widgets[l] = size_widget_for_thread @threads[l]
866 @date_widgets[l] = date_widget_for_thread @threads[l]
867
868 ## if a widget size has increased, we need to redraw everyone
869 need_update =
870 (@size_widgets[l].size > @size_widget_width) or
871 (@date_widgets[l].size > @date_widget_width)
872 end
873
874 if need_update
875 update
876 else
877 @text[l] = text_for_thread_at l
878 buffer.mark_dirty if buffer
879 end
880 end
881
882 def regen_text
883 threads = @mutex.synchronize { @threads }
884 @text = threads.map_with_index { |t, i| text_for_thread_at i }
885 @lines = threads.map_with_index { |t, i| [t, i] }.to_h
886 buffer.mark_dirty if buffer
887 end
888
889 def authors; map { |m, *o| m.from if m }.compact.uniq; end
890
891 ## preserve author order from the thread
892 def author_names_and_newness_for_thread t, limit=nil
893 new = {}
894 seen = {}
895 authors = t.map do |m, *o|
896 next unless m && m.from
897 new[m.from] ||= m.has_label?(:unread)
898 next if seen[m.from]
899 seen[m.from] = true
900 m.from
901 end.compact
902
903 result = []
904 authors.each do |a|
905 break if limit && result.size >= limit
906 name = if AccountManager.is_account?(a)
907 "me"
908 elsif t.authors.size == 1
909 a.mediumname
910 else
911 a.shortname
912 end
913
914 result << [name, new[a]]
915 end
916
917 if result.size == 1 && (author_and_newness = result.assoc("me"))
918 unless (recipients = t.participants - t.authors).empty?
919 result = recipients.collect do |r|
920 break if limit && result.size >= limit
921 name = (recipients.size == 1) ? r.mediumname : r.shortname
922 ["(#{name})", author_and_newness[1]]
923 end
924 end
925 end
926
927 result
928 end
929
930 AUTHOR_LIMIT = 5
931 def text_for_thread_at line
932 t, size_widget, date_widget = @mutex.synchronize do
933 [@threads[line], @size_widgets[line], @date_widgets[line]]
934 end
935
936 starred = t.has_label? :starred
937
938 ## format the from column
939 cur_width = 0
940 ann = author_names_and_newness_for_thread t, AUTHOR_LIMIT
941 from = []
942 ann.each_with_index do |(name, newness), i|
943 break if cur_width >= from_width
944 last = i == ann.length - 1
945
946 abbrev =
947 if cur_width + name.display_length > from_width
948 name.slice_by_display_length(from_width - cur_width - 1) + "."
949 elsif cur_width + name.display_length == from_width
950 name.slice_by_display_length(from_width - cur_width)
951 else
952 if last
953 name.slice_by_display_length(from_width - cur_width)
954 else
955 name.slice_by_display_length(from_width - cur_width - 1) + ","
956 end
957 end
958
959 cur_width += abbrev.display_length
960
961 if last && from_width > cur_width
962 abbrev += " " * (from_width - cur_width)
963 end
964
965 from << [(newness ? :index_new_color : (starred ? :index_starred_color : :index_old_color)), abbrev]
966 end
967
968 is_me = AccountManager.method(:is_account?)
969 directly_participated = t.direct_participants.any?(&is_me)
970 participated = directly_participated || t.participants.any?(&is_me)
971
972 subj_color =
973 if t.has_label?(:draft)
974 :index_draft_color
975 elsif t.has_label?(:unread)
976 :index_new_color
977 elsif starred
978 :index_starred_color
979 elsif Colormap.sym_is_defined(:index_subject_color)
980 :index_subject_color
981 else
982 :index_old_color
983 end
984
985 size_padding = @size_widget_width - size_widget.display_length
986 size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
987
988 date_padding = @date_widget_width - date_widget.display_length
989 date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
990
991 [
992 [:tagged_color, @tags.tagged?(t) ? ">" : " "],
993 [:date_color, date_widget_text],
994 [:starred_color, (starred ? "*" : " ")],
995 ] +
996 from +
997 [
998 [:size_widget_color, size_widget_text],
999 [:with_attachment_color , t.labels.member?(:attachment) ? "@" : " "],
1000 [:to_me_color, directly_participated ? ">" : (participated ? '+' : " ")],
1001 ] +
1002 (t.labels - @hidden_labels).sort_by {|x| x.to_s}.map {
1003 |label| [Colormap.sym_is_defined("label_#{label}_color".to_sym) || :label_color, "#{label} "]
1004 } +
1005 [
1006 [subj_color, t.subj + (t.subj.empty? ? "" : " ")],
1007 [:snippet_color, t.snippet],
1008 ]
1009 end
1010
1011 def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
1012
1013 private
1014
1015 def default_size_widget_for t
1016 case t.size
1017 when 1
1018 ""
1019 else
1020 "(#{t.size})"
1021 end
1022 end
1023
1024 def default_date_widget_for t
1025 t.date.getlocal.to_nice_s
1026 end
1027
1028 def from_width
1029 if buffer
1030 [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
1031 else
1032 MIN_FROM_WIDTH # not sure why the buffer is gone
1033 end
1034 end
1035
1036 def initialize_threads
1037 @ts = ThreadSet.new Index.instance, $config[:thread_by_subject]
1038 @ts_mutex = Mutex.new
1039 @hidden_threads = {}
1040 end
1041 end
1042
1043 end