def initialize fn
@fn = fn
- @people = {}
+ @p2a = {} # person to alias map
+ @a2p = {} # alias to person map
if File.exists? fn
IO.foreach(fn) do |l|
l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
aalias, addr = $1, $2
- @people[aalias] = Person.for addr
+ p = Person.for addr
+ @p2a[p] = aalias
+ @a2p[aalias] = p
end
end
self.class.i_am_the_instance self
end
- def contacts; @people; end
+ def contacts; @p2a.keys; end
def set_contact person, aalias
- oldentry = @people.find { |a, p| p == person }
- @people.delete oldentry.first if oldentry
- @people[aalias] = person
+ if(pold = @a2p[aalias]) && (pold != person)
+ drop_contact pold
+ end
+ @p2a[person] = aalias
+ @a2p[aalias] = person
end
- def drop_contact person; @people.find { |a, p| @people.delete a if p == person }; end
- def delete t; @people.delete t; end
- def resolve aalias; @people[aalias]; end
-
+ def drop_contact person
+ if(aalias = @p2a[person])
+ @p2a.delete person
+ @a2p.delete aalias
+ end
+ end
+ def person_with aalias; @a2p[aalias]; end
+ def alias_for person; @p2a[person]; end
+ def is_contact? person; @p2a.member? person; end
def save
File.open(@fn, "w") do |f|
- @people.keys.sort.each do |aalias|
- f.puts "#{aalias}: #{@people[aalias].full_address}"
+ @p2a.each do |p, a|
+ f.puts "#{a}: #{p.full_address}"
end
end
end
module Redwood
+module CanAliasContacts
+ def alias_contact p
+ a = BufferManager.ask(:alias, "Nickname for #{p.longname}: ", ContactManager.alias_for(p)) or return
+ if a.empty?
+ ContactManager.drop_contact p
+ else
+ ContactManager.set_contact p, a
+ end
+ end
+end
+
class ContactListMode < LineCursorMode
LOAD_MORE_CONTACTS_NUM = 10
register_keymap do |k|
k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
k.add :reload, "Drop contact list and reload", 'D'
- k.add :alias, "Edit alias for contact", 'a'
+ k.add :alias, "Edit nickname/alias for contact", 'a'
k.add :toggle_tagged, "Tag/untag current line", 't'
k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
k.add :search, "Search for messages from particular people", 'S'
super()
end
+ include CanAliasContacts
+ def alias
+ p = @contacts[curpos] or return
+ alias_contact p
+ regen_text
+ end
+
def lines; @text.length; end
def [] i; @text[i]; end
load
end
- def alias
- p = @contacts[curpos] or return
- a = BufferManager.ask(:alias, "alias for #{p.longname}: ", @user_contacts[p]) or return
- if a.empty?
- ContactManager.drop_contact p
- @user_contacts.delete p
- else
- ContactManager.set_contact p, a
- @user_contacts[p] = a
- end
- regen_text # in case columns need to be shifted
- end
-
def load_in_background
Redwood::reporting_thread do
load
def load
@num ||= buffer.content_height
- @user_contacts = ContactManager.contacts.invert
+ @user_contacts = ContactManager.contacts
num = [@num - @user_contacts.length, 0].max
BufferManager.say("Loading #{num} contacts from index...") do
recentc = Index.load_contacts AccountManager.user_emails, :num => num
- @contacts = (@user_contacts.keys + recentc).sort_by { |p| p.sort_by_me }.uniq
+ @contacts = (@user_contacts + recentc).sort_by { |p| p.sort_by_me }.uniq
end
end
end
def text_for_contact p
- aalias = @user_contacts[p] || ""
+ aalias = ContactManager.alias_for(p) || ""
[[:tagged_color, @tags.tagged?(p) ? ">" : " "],
[:none, sprintf("%-#{@awidth}s %-#{@nwidth}s %s", aalias, p.name, p.email)]]
end
def regen_text
@awidth, @nwidth = 0, 0
@contacts.each do |p|
- aalias = @user_contacts[p]
+ aalias = ContactManager.alias_for_person(p)
@awidth = aalias.length if aalias && aalias.length > @awidth
@nwidth = p.name.length if p.name && p.name.length > @nwidth
end
k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
k.add :reply, "Reply to a message", 'r'
k.add :forward, "Forward a message", 'f'
+ k.add :alias, "Edit alias/nickname for a person", 'a'
k.add :save_to_disk, "Save message/attachment to disk", 's'
end
- ## there are three important instance variables we hold to lay out
- ## the thread. @layout is a map from Message and Chunk objects to
- ## Layout objects. (for chunks, we only use the state field right
- ## now.) @message_lines is a map from row #s to Message objects.
- ## @chunk_lines is a map from row #s to Chunk objects.
+ ## there are a couple important instance variables we hold to lay
+ ## out the thread and to provide line-based functionality. @layout
+ ## is a map from Message and Chunk objects to Layout objects. (for
+ ## chunks, we only use the state field right now.) @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.
def initialize thread, hidden_labels=[]
super()
mode.edit
end
+ include CanAliasContacts
+ def alias
+ p = @person_lines[curpos] or return
+ alias_contact p
+ regen_text
+ end
+
def toggle_starred
return unless(m = @message_lines[curpos])
if m.has_label? :starred
@text = []
@chunk_lines = []
@message_lines = []
+ @person_lines = []
prevm = nil
@thread.each do |m, depth, parent|
end
end
- def message_patina_lines m, state, parent, prefix
+ def message_patina_lines m, state, start, parent, prefix
prefix_widget = [:message_patina_color, prefix]
widget =
case state
case state
when :open
+ @person_lines[start] = m.from
[[prefix_widget, widget, imp_widget,
[:message_patina_color,
"#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
+
when :closed
+ @person_lines[start] = m.from
[[prefix_widget, widget, imp_widget,
[:message_patina_color,
"#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
+
when :detailed
- labels = m.labels# - @hidden_labels
- x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
- ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
- (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
- (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
- [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
- [" Subject: #{m.subj}"] +
- [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
- [labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
- ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
- #raise x.inspect
- x
+ @person_lines[start] = m.from
+ from = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
+
+ rest = []
+ unless m.to.empty?
+ m.to.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+ rest += format_person_list " To: ", m.to
+ end
+ unless m.cc.empty?
+ m.cc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+ rest += format_person_list " Cc: ", m.cc
+ end
+ unless m.bcc.empty?
+ m.bcc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+ rest += format_person_list " Bcc: ", m.bcc
+ end
+
+ rest += [
+ " Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})",
+ " Subject: #{m.subj}",
+ (parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil),
+ m.labels.empty? ? nil : " Labels: #{m.labels.join(', ')}",
+ ].compact
+
+ from + rest.map { |l| [[:message_patina_color, prefix + " " + l]] }
end
end
- def break_into_lines prefix, list
+ def format_person_list prefix, people
+ ptext = people.map { |p| format_person p }
pad = " " * prefix.length
- [prefix + list.first + (list.length > 1 ? "," : "")] +
- list[1 .. -1].map_with_index do |e, i|
- pad + e + (i == list.length - 1 ? "" : ",")
+ [prefix + ptext.first + (ptext.length > 1 ? "," : "")] +
+ ptext[1 .. -1].map_with_index do |e, i|
+ pad + e + (i == ptext.length - 1 ? "" : ",")
end
end
+ def format_person p
+ p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
+ end
def chunk_to_lines chunk, state, start, depth, parent=nil
prefix = " " * INDENT_SPACES * depth
when nil
[[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
when Message
- message_patina_lines(chunk, state, parent, prefix) +
+ message_patina_lines(chunk, state, start, parent, prefix) +
(chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
when Message::Attachment
[[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]