mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-10-18 07:33:46 +00:00
Merge 778752ce17a77e9a2d1376733556861da9ff8b16 into 9b84b361225facd8e623f25d76f889d3dc54a879
This commit is contained in:
commit
9151353fac
@ -19396,7 +19396,7 @@
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"%@ active" : {
|
||||
"comment" : "A label at the bottom of the people list sheet showing the number of active users.",
|
||||
"comment" : "A label at the bottom of the people list sheet showing the number of active people.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
@ -35151,6 +35151,746 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"common.clear" : {
|
||||
"comment" : "Clear the current text field",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "مسح"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "মুছুন"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "löschen"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "clear"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "borrar"
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "پاک کردن"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "i-clear"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "effacer"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "נקה"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "साफ़ करें"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "hapus"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cancella"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "クリア"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "지우기"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kosongkan"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "खाली"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "wissen"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "wyczyść"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "limpar"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "limpar"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "очистить"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "rensa"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "அழி"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ล้าง"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "temizle"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "очистити"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "صاف کریں"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "xóa"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "清除"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "清除"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.people.search.accessibility" : {
|
||||
"comment" : "Accessibility label for the people-sheet nickname search field",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "تصفية الأشخاص حسب اللقب"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ডাকনাম দিয়ে মানুষ ফিল্টার"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personen nach Spitznamen filtern"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filter people by nickname"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtrar personas por apodo"
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "فیلتر افراد بر اساس نام مستعار"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "I-filter ang mga tao ayon sa palayaw"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtrer les personnes par surnom"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "סינון אנשים לפי כינוי"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "उपनाम से लोग फ़िल्टर करें"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filter orang berdasarkan nama panggilan"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtra persone per soprannome"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ニックネームで人を絞り込む"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "닉네임으로 사람 필터"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tapis orang mengikut nama samaran"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "उपनामबाट मानिस फिल्टर"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personen filteren op bijnaam"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtruj osoby po pseudonimie"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtrar pessoas por alcunha"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtrar pessoas por apelido"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Фильтровать людей по нику"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtrera personer efter smeknamn"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "புனைப்பெயரால் நபர்களை வடிகட்டு"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "กรองผู้คนตามชื่อเล่น"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kişileri takma ada göre filtrele"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Фільтрувати людей за нікнеймом"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "عرفی نام سے لوگ فلٹر کریں"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Lọc mọi người theo biệt danh"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "按昵称筛选联系人"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "依暱稱篩選聯絡人"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.people.search.no_matches" : {
|
||||
"comment" : "Empty state when people-sheet search matches no nicknames",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "لا نتائج"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "কোনো মিল নেই"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "keine treffer"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no matches"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "sin coincidencias"
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "موردی نیست"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "walang tugma"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "aucune correspondance"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "אין התאמות"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "कोई मेल नहीं"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tidak ada hasil"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "nessuna corrispondenza"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "一致なし"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "일치 없음"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tiada padanan"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "मिल्ने छैन"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "geen resultaten"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "brak wyników"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "sem resultados"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "sem resultados"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "нет совпадений"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "inga träffar"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "பொருத்தம் இல்லை"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ไม่พบรายการ"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "eş yok"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "немає збігів"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "کوئی مماثلت نہیں"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "không khớp"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无匹配"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無相符項目"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.people.search.placeholder" : {
|
||||
"comment" : "Placeholder for the people-sheet nickname search field",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "تصفية الأشخاص"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "মানুষ ফিল্টার"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "personen filtern"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filter people"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtrar personas"
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "فیلتر افراد"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "i-filter ang mga tao"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtrer les personnes"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "סינון אנשים"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "लोग फ़िल्टर करें"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filter orang"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtra persone"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "人を絞り込む"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "사람 필터"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tapis orang"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "मानिस फिल्टर"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "personen filteren"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtruj osoby"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtrar pessoas"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtrar pessoas"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "фильтр людей"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "filtrera personer"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "நபர்களை வடிகட்டு"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "กรองผู้คน"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kişileri filtrele"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "фільтр людей"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "لوگ فلٹر کریں"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "lọc mọi người"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "筛选联系人"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "篩選聯絡人"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common.close" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
22
bitchat/Utils/PeopleNameFilter.swift
Normal file
22
bitchat/Utils/PeopleNameFilter.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// PeopleNameFilter.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Case-insensitive nickname / display-name match for the people sheet search field.
|
||||
///
|
||||
/// Filtering is display-only: callers never mutate `PeerListModel` row order.
|
||||
/// Clearing the query restores the full list with existing sticky ordering.
|
||||
enum PeopleNameFilter {
|
||||
/// Empty / whitespace-only queries match everything (no filter active).
|
||||
static func matches(_ displayName: String, query: String) -> Bool {
|
||||
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return true }
|
||||
return displayName.localizedCaseInsensitiveContains(trimmed)
|
||||
}
|
||||
}
|
||||
@ -39,6 +39,8 @@ struct PeopleSectionHeader: View {
|
||||
struct BridgePeopleList: View {
|
||||
@ObservedObject private var bridgeService = BridgeService.shared
|
||||
@ThemedPalette private var palette
|
||||
/// People-sheet search query; empty means show every row.
|
||||
var nameFilter: String = ""
|
||||
|
||||
private enum Strings {
|
||||
static let sectionTitle = String(localized: "bridge_people.section_title", defaultValue: "across the bridge", comment: "Section header in the people sheet for participants reachable via the mesh bridge")
|
||||
@ -50,7 +52,10 @@ struct BridgePeopleList: View {
|
||||
// (a serving neighbor's carriers) even while this device's own
|
||||
// bridge is off — whoever is visible in the timeline belongs in the
|
||||
// sheet.
|
||||
if !bridgeService.bridgedParticipants.isEmpty {
|
||||
let people = bridgeService.bridgedParticipants.filter {
|
||||
PeopleNameFilter.matches($0.displayName, query: nameFilter)
|
||||
}
|
||||
if !people.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
PeopleSectionHeader(
|
||||
icon: "network",
|
||||
@ -58,7 +63,7 @@ struct BridgePeopleList: View {
|
||||
title: Strings.sectionTitle
|
||||
)
|
||||
|
||||
ForEach(bridgeService.bridgedParticipants) { person in
|
||||
ForEach(people) { person in
|
||||
HStack(spacing: 4) {
|
||||
Text(person.displayName)
|
||||
.bitchatFont(size: 14)
|
||||
|
||||
@ -321,6 +321,18 @@ private struct ContentPeopleListView: View {
|
||||
|
||||
@Binding var showSidebar: Bool
|
||||
@Binding var showVerifySheet: Bool
|
||||
@State private var nameFilter = ""
|
||||
|
||||
private enum Strings {
|
||||
static let searchPlaceholder = String(
|
||||
localized: "content.people.search.placeholder",
|
||||
comment: "Placeholder for the people-sheet nickname search field"
|
||||
)
|
||||
static let searchAccessibility = String(
|
||||
localized: "content.people.search.accessibility",
|
||||
comment: "Accessibility label for the people-sheet nickname search field"
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@ -363,6 +375,35 @@ private struct ContentPeopleListView: View {
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(palette.locationAccent)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(palette.secondary)
|
||||
TextField(Strings.searchPlaceholder, text: $nameFilter)
|
||||
.bitchatFont(size: 14)
|
||||
.textFieldStyle(.plain)
|
||||
.autocorrectionDisabled()
|
||||
#if os(iOS)
|
||||
.textInputAutocapitalization(.never)
|
||||
#endif
|
||||
.accessibilityLabel(Strings.searchAccessibility)
|
||||
if !nameFilter.isEmpty {
|
||||
Button {
|
||||
nameFilter = ""
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(String(localized: "common.clear", comment: "Clear the current text field"))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 7)
|
||||
.background(palette.secondary.opacity(0.12))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 16)
|
||||
@ -378,7 +419,8 @@ private struct ContentPeopleListView: View {
|
||||
GeohashPeopleList(
|
||||
onTapPerson: {
|
||||
showSidebar = true
|
||||
}
|
||||
},
|
||||
nameFilter: nameFilter
|
||||
)
|
||||
// Direct conversations survive channel switches; the
|
||||
// geoDM someone opened from another cell must stay
|
||||
@ -413,17 +455,19 @@ private struct ContentPeopleListView: View {
|
||||
} else {
|
||||
conversationUIModel.block(peerID: peer.peerID, displayName: peer.displayName)
|
||||
}
|
||||
}
|
||||
},
|
||||
nameFilter: nameFilter
|
||||
)
|
||||
// People in this area but beyond radio range, and
|
||||
// private groups: one sheet for the whole room.
|
||||
BridgePeopleList()
|
||||
BridgePeopleList(nameFilter: nameFilter)
|
||||
GroupChatList(
|
||||
groups: peerListModel.groupRows,
|
||||
onTapGroup: { peerID in
|
||||
peerListModel.startConversation(with: peerID)
|
||||
showSidebar = true
|
||||
}
|
||||
},
|
||||
nameFilter: nameFilter
|
||||
)
|
||||
// Conversations with people no roster above lists
|
||||
// anymore — without this, a read DM from an offline
|
||||
|
||||
@ -4,13 +4,16 @@ struct GeohashPeopleList: View {
|
||||
@EnvironmentObject private var peerListModel: PeerListModel
|
||||
@ThemedPalette private var palette
|
||||
let onTapPerson: () -> Void
|
||||
/// People-sheet search query; empty means show every row.
|
||||
var nameFilter: String = ""
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var orderedIDs: [String] = []
|
||||
|
||||
private enum Strings {
|
||||
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
|
||||
static let noMatches: LocalizedStringKey = "content.people.search.no_matches"
|
||||
static let youSuffix: LocalizedStringKey = "geohash_people.you_suffix"
|
||||
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to users blocked in geohash channels")
|
||||
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to people blocked in geohash channels")
|
||||
static let unblock: LocalizedStringKey = "geohash_people.action.unblock"
|
||||
static let block: LocalizedStringKey = "geohash_people.action.block"
|
||||
static let unblockText = String(localized: "geohash_people.action.unblock", comment: "Context menu action to unblock a person")
|
||||
@ -32,14 +35,42 @@ struct GeohashPeopleList: View {
|
||||
.padding(.top, 12)
|
||||
}
|
||||
} else {
|
||||
let people = peerListModel.geohashPeople
|
||||
let currentIDs = people.map(\.id)
|
||||
let allPeople = peerListModel.geohashPeople
|
||||
// Sticky order must track the unfiltered roster — filtering before
|
||||
// `currentIDs` would prune orderedIDs and reshuffle on clear (#1589 Codex).
|
||||
let allIDs = allPeople.map(\.id)
|
||||
let filterActive = !nameFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let people = allPeople.filter {
|
||||
PeopleNameFilter.matches($0.displayName, query: nameFilter)
|
||||
}
|
||||
if people.isEmpty && filterActive {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(Strings.noMatches)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.secondary)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.onAppear {
|
||||
orderedIDs = allIDs
|
||||
}
|
||||
.onChange(of: allIDs) { ids in
|
||||
var newOrder = orderedIDs
|
||||
newOrder.removeAll { !ids.contains($0) }
|
||||
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
||||
if newOrder != orderedIDs { orderedIDs = newOrder }
|
||||
}
|
||||
} else {
|
||||
let currentIDs = allIDs
|
||||
|
||||
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
|
||||
let nonTele = displayIDs.filter { id in
|
||||
let visibleIDs = displayIDs.filter { id in
|
||||
people.contains(where: { $0.id == id })
|
||||
}
|
||||
let nonTele = visibleIDs.filter { id in
|
||||
!(people.first(where: { $0.id == id })?.isTeleported ?? false)
|
||||
}
|
||||
let tele = displayIDs.filter { id in
|
||||
let tele = visibleIDs.filter { id in
|
||||
people.first(where: { $0.id == id })?.isTeleported ?? false
|
||||
}
|
||||
let finalOrder: [String] = nonTele + tele
|
||||
@ -142,7 +173,8 @@ struct GeohashPeopleList: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Seed and update order outside result builder
|
||||
// Seed and update order outside result builder — always from the
|
||||
// full unfiltered ID list so search does not reshuffle sticky order.
|
||||
.onAppear {
|
||||
orderedIDs = currentIDs
|
||||
}
|
||||
@ -152,6 +184,7 @@ struct GeohashPeopleList: View {
|
||||
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
||||
if newOrder != orderedIDs { orderedIDs = newOrder }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ struct GroupChatList: View {
|
||||
|
||||
let groups: [GroupChatRow]
|
||||
let onTapGroup: (PeerID) -> Void
|
||||
/// People-sheet search query; empty means show every row.
|
||||
var nameFilter: String = ""
|
||||
|
||||
private enum Strings {
|
||||
static let header = String(localized: "groups.section.header", comment: "Section header above the private groups list")
|
||||
@ -19,7 +21,8 @@ struct GroupChatList: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !groups.isEmpty {
|
||||
let visible = groups.filter { PeopleNameFilter.matches($0.name, query: nameFilter) }
|
||||
if !visible.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Same glyph+label header shape as #mesh / across the bridge.
|
||||
PeopleSectionHeader(
|
||||
@ -28,7 +31,7 @@ struct GroupChatList: View {
|
||||
title: Strings.header
|
||||
)
|
||||
|
||||
ForEach(groups) { group in
|
||||
ForEach(visible) { group in
|
||||
HStack(spacing: 4) {
|
||||
Text("#\(group.name)")
|
||||
.bitchatFont(size: 14)
|
||||
|
||||
@ -10,12 +10,15 @@ struct MeshPeerList: View {
|
||||
/// Optional so existing call sites (and previews/tests) keep compiling;
|
||||
/// when absent the block/unblock context-menu entry is hidden.
|
||||
var onToggleBlock: ((MeshPeerRow) -> Void)? = nil
|
||||
/// People-sheet search query; empty means show every row.
|
||||
var nameFilter: String = ""
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@State private var orderedIDs: [String] = []
|
||||
|
||||
private enum Strings {
|
||||
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
|
||||
static let noMatches: LocalizedStringKey = "content.people.search.no_matches"
|
||||
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to a blocked peer indicator")
|
||||
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
|
||||
static let connected = String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator")
|
||||
@ -41,7 +44,8 @@ struct MeshPeerList: View {
|
||||
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
|
||||
let peers: [MeshPeerRow] = displayIDs.compactMap { id in
|
||||
peerListModel.meshRows.first(where: { $0.id == id })
|
||||
}
|
||||
}.filter { PeopleNameFilter.matches($0.displayName, query: nameFilter) }
|
||||
let filterActive = !nameFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|
||||
if peerListModel.meshRows.isEmpty {
|
||||
// Match the section's row rhythm (same size, indent, and vertical
|
||||
@ -52,6 +56,21 @@ struct MeshPeerList: View {
|
||||
.foregroundColor(palette.secondary)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 4)
|
||||
} else if peers.isEmpty && filterActive {
|
||||
Text(Strings.noMatches)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.secondary)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 4)
|
||||
.onAppear {
|
||||
orderedIDs = currentIDs
|
||||
}
|
||||
.onChange(of: currentIDs) { ids in
|
||||
var newOrder = orderedIDs
|
||||
newOrder.removeAll { !ids.contains($0) }
|
||||
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
||||
if newOrder != orderedIDs { orderedIDs = newOrder }
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(0..<peers.count, id: \.self) { idx in
|
||||
|
||||
22
bitchatTests/Utils/PeopleNameFilterTests.swift
Normal file
22
bitchatTests/Utils/PeopleNameFilterTests.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct PeopleNameFilterTests {
|
||||
@Test func emptyQueryMatchesEverything() {
|
||||
#expect(PeopleNameFilter.matches("alice", query: ""))
|
||||
#expect(PeopleNameFilter.matches("alice", query: " "))
|
||||
}
|
||||
|
||||
@Test func matchesAreCaseInsensitive() {
|
||||
#expect(PeopleNameFilter.matches("Alice#abcd", query: "ali"))
|
||||
#expect(PeopleNameFilter.matches("Bob", query: "BOB"))
|
||||
}
|
||||
|
||||
@Test func nonMatchingQueryRejects() {
|
||||
#expect(!PeopleNameFilter.matches("alice", query: "bob"))
|
||||
}
|
||||
|
||||
@Test func matchesSubstringInsideNickname() {
|
||||
#expect(PeopleNameFilter.matches("cool_alice", query: "ali"))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user