admin: list nicknamed contacts as "Nickname (Name)"
The paren form is composed in the view because the card holds FN and
NICKNAME as independent facts; other screens keep the plain name.
Assisted-by: GLM-5.3 via pi
diff --git a/lib/pro_tacts/admin/contacts_index.rb b/lib/pro_tacts/admin/contacts_index.rb
index 50b43c3..9e59544 100644
--- a/lib/pro_tacts/admin/contacts_index.rb
+++ b/lib/pro_tacts/admin/contacts_index.rb
@@ -46,6 +46,21 @@ module ProTacts
private
+ # The row's label: "Nickname (Name)" when the card carries a
+ # NICKNAME — the name someone is known by, the formal one kept
+ # beside it — and the plain name otherwise. The composition is
+ # this screen's, not Contact's: the card holds the two facts
+ # independently, and other screens (the detail card, the
+ # birthdays column) show the plain name.
+ #: (Contact contact) -> String
+ def label_of(contact)
+ nickname = contact.nickname
+ name = contact.name
+ return "#{nickname} (#{name})" if nickname && name
+
+ nickname || name || contact.id
+ end
+
#: (Row row, String query) -> bool
def matches?(row, query)
q = query.downcase
@@ -62,7 +77,7 @@ module ProTacts
a(href: "/contacts/#{row.contact.id}") do
render Avatar.new(contact: row.contact, size: "lg")
div(style: "flex: 1; min-width: 0;") do
- div(style: "font-weight: 550;") { row.contact.name || row.contact.id }
+ div(style: "font-weight: 550;") { label_of(row.contact) }
end
span(class: "type-label") { Format.time_ago(row.updated_at) }
end
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 43eabfc..cf26b28 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -160,6 +160,13 @@ module ProTacts
property && components_of(property.value)
end
+ # NICKNAME's value (RFC 2426 section 3.1.3), in text form — the
+ # whole value, as the card spells it, when it comma-lists several.
+ #: () -> String?
+ def nickname
+ text_of(properties.find { it.name.casecmp?("NICKNAME") })
+ end
+
#: () -> Array[Phone]
def phones
of_name("TEL").filter_map do |property|
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index dd65688..2d68126 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -55,6 +55,19 @@ class AdminContactsPagesTest < Minitest::Test
end
end
+ # A card with a NICKNAME lists as "Nickname (Name)" — the name
+ # someone is known by, the formal one kept beside it. The plain-name
+ # row is what every other index test already shows.
+ def test_index_lists_a_nicknamed_contact_as_nickname_and_name
+ red = ADA.sub("FN:Ada Lovelace", "FN:Sarah\r\nNICKNAME:Red").sub("UID:ada", "UID:red")
+
+ with_contacts({"red" => red}) do
+ get "/"
+
+ assert_includes last_response.body, "Red (Sarah)"
+ end
+ end
+
def test_index_with_no_contacts_says_so
with_contacts({}) { get "/" }
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index c0c1954..a1c8fde 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -66,6 +66,13 @@ class ContactTest < Minitest::Test
assert_equal ["Lovelace", "Ada", nil, nil, nil], contact(STRUCTURED).name_components
end
+ def test_reads_the_nickname
+ nicknamed = CARD.sub("FN:Aiden\r\n", "FN:Aiden\r\nNICKNAME:Red\r\n")
+
+ assert_equal "Red", contact(nicknamed).nickname
+ assert_nil contact(CARD).nickname
+ end
+
def test_values_come_back_unescaped
escaped = STRUCTURED.sub("FN:Ada Lovelace", "FN:Ada\\, Countess of Lovelace")
@@ -159,6 +166,7 @@ class ContactTest < Minitest::Test
assert_nil bare.name
assert_nil bare.name_components
+ assert_nil bare.nickname
assert_empty bare.phones
assert_empty bare.emails
assert_empty bare.addresses