Merge pull request #7445 from vector-im/release/1.10.6/release

Release 1.10.6
This commit is contained in:
Mauro
2023-03-21 18:29:51 +01:00
committed by GitHub
66 changed files with 4104 additions and 893 deletions
+18
View File
@@ -1,3 +1,21 @@
## Changes in 1.10.6 (2023-03-21)
🙌 Improvements
- Encryption: Refactor user / room encryption trust level ([#7430](https://github.com/vector-im/element-ios/pull/7430))
- Crypto: Increase local rust crypto rollout to 20% of all users ([#7434](https://github.com/vector-im/element-ios/pull/7434))
- Upgrade MatrixSDK version ([v0.26.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.2)).
- Permalinks to a room/space are pillified ([#7409](https://github.com/vector-im/element-ios/issues/7409))
- Permalinks to a matrix user are pillified ([#7411](https://github.com/vector-im/element-ios/issues/7411))
- Permalinks to messages are pillified ([#7412](https://github.com/vector-im/element-ios/issues/7412))
- Loading: Update startup progress UX ([#7417](https://github.com/vector-im/element-ios/issues/7417))
🐛 Bugfixes
- Room list: increase tappability area of the avatar button. ([#7427](https://github.com/vector-im/element-ios/pull/7427))
- Manage bad m.file attachment format. ([#7406](https://github.com/vector-im/element-ios/issues/7406))
## Changes in 1.10.5 (2023-03-13)
🙌 Improvements
+2 -2
View File
@@ -15,5 +15,5 @@
//
// Version
MARKETING_VERSION = 1.10.5
CURRENT_PROJECT_VERSION = 1.10.5
MARKETING_VERSION = 1.10.6
CURRENT_PROJECT_VERSION = 1.10.6
+1 -1
View File
@@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
$matrixSDKVersion = '= 0.26.1'
$matrixSDKVersion = '= 0.26.2'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "pill_user.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pill_user@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pill_user@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+16 -10
View File
@@ -2661,12 +2661,6 @@
// Unverified sessions
"key_verification_alert_title" = "Du hast nicht verifizierte Sitzungen";
"launch_loading_processing_response" = "Verarbeite Daten\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Synchronisiere mit dem Server\n(%@ Versuch)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Synchronisiere mit dem Server";
"user_other_session_permanently_unverified_additional_info" = "Diese Sitzung unterstützt keine Verschlüsselung und kann deshalb nicht verifiziert werden.";
"voice_broadcast_time_left" = "%@ übrig";
"voice_broadcast_buffering" = "Puffere …";
@@ -2716,10 +2710,6 @@
"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten";
"voice_broadcast_recorder_connection_error" = "Verbindungsfehler Aufnahme pausiert";
"poll_timeline_reply_ended_poll" = "Beendete Umfrage";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migriere Daten\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung (zum Deaktivieren abmelden)";
"settings_labs_confirm_crypto_sdk" = "Bitte beachte, dass diese Funktion noch experimentell ist, womöglich nicht wie erwartet funktioniert und unerwünschte Nebeneffekte haben kann. Melde dich zum deaktivieren einfach ab und erneut an. Nutze diese Funktion nach eigenem Ermessen und mit Vorsicht.";
"settings_labs_enable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung";
@@ -2736,3 +2726,19 @@
"settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten.";
"poll_history_detail_view_in_timeline" = "Umfrage im Verlauf anzeigen";
"authentication_qr_login_failure_device_not_supported" = "Die Verbindung mit diesem Gerät wird nicht unterstützt.";
"room_waiting_other_participants_message" = "Sobald eingeladene Benutzer %@ beigetreten sind, werdet ihr euch unterhalten können und der Raum Ende-zu-Ende-verschlüsselt sein";
"room_waiting_other_participants_title" = "Warte darauf, dass Benutzer %@ beitreten";
"key_verification_scan_qr_code_information_new_session" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um deine neue Sitzung zu verifizieren";
"key_verification_scan_qr_code_information_other_session" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um deine Sitzung zu verifizieren";
"key_verification_scan_qr_code_information_other_device" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um diese Sitzung zu verifizieren";
"key_verification_scan_qr_code_information_other_user" = "Richte deine Kamera auf den QR-Code des anderen Gerätes, um die Sitzung der anderen Person zu verifizieren";
"key_verification_scan_qr_code_title" = "QR-Code einlesen";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Du hast keinen Zugriff auf eine bestehende %@-Sitzung?";
"device_verification_self_verify_open_on_other_device_information" = "Du musst diese Sitzung verifizieren, um deinen verschlüsselten Nachrichtenverlauf lesen zu können.\n\nÖffne Element auf einem deiner anderen Geräte und folge den Anweisungen.";
"device_verification_self_verify_open_on_other_device_title" = "Öffne %@ auf deinem anderen Gerät";
"room_creation_only_one_email_invite" = "Du kannst E-Mail-Einladung nur nacheinander verschicken";
"launch_loading_delay_warning" = "Das könnte eine Weile dauern.\nDanke für deine Geduld.";
// MARK: - Launch loading
"launch_loading_generic" = "Synchronisiere deine Unterhaltungen";
+8 -4
View File
@@ -1991,10 +1991,8 @@ Tap the + to start adding people.";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrating data\n%@ %%";
"launch_loading_server_syncing" = "Syncing with the server";
"launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)";
"launch_loading_processing_response" = "Processing data\n%@ %%";
"launch_loading_generic" = "Syncing your conversations";
"launch_loading_delay_warning" = "This may take a little longer.\nThanks for your patience.";
// MARK: - Home
@@ -3158,3 +3156,9 @@ To enable access, tap Settings> Location and select Always";
"ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate.";
"ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.";
"ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";
// Pills
"pill_room_fallback_display_name" = "Space/Room";
"pill_message" = "Message";
"pill_message_from" = "Message from %@";
"pill_message_in" = "Message in %@";
+17 -11
View File
@@ -2601,12 +2601,6 @@
"key_verification_alert_title" = "Sul on verifitseerimata sessioone";
"user_other_session_permanently_unverified_additional_info" = "Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi.";
"voice_broadcast_time_left" = "aega jäänud %@";
"launch_loading_processing_response" = "Töötleme andmeid\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Sünkroniseerime andmeid serveriga\n(katse: %@)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Sünkroniseerimine serveriga";
"voice_broadcast_buffering" = "Andmed on puhverdamisel…";
"voice_broadcast_stop_alert_agree_button" = "Jah, lõpetame";
"voice_broadcast_stop_alert_description" = "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.";
@@ -2629,7 +2623,7 @@
"notice_voice_broadcast_ended" = "%@ lõpetas ringhäälingukõne.";
"notice_voice_broadcast_live" = "Ringhäälingukõne on eetris";
"user_other_session_security_recommendation_title" = "Muud sessioonid";
"poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata";
"poll_timeline_decryption_error" = "Dekrüptimisvigade tõttu jääb osa hääli lugemata";
"voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu";
"voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne";
"poll_timeline_ended_text" = "Küsitlus on lõppenud";
@@ -2654,10 +2648,6 @@
"wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja";
"voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud";
"poll_timeline_reply_ended_poll" = "Lõppenud küsitlus";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine (väljalülitamiseks pead välja logima)";
"settings_labs_confirm_crypto_sdk" = "Palun arvesta, et see funktsionaalsus on alles katseline ja ei pruugi toimida eesmärgipäraselt. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Jätka ettevaatlikult ja omal äranägemisel.";
"settings_labs_enable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine";
@@ -2674,3 +2664,19 @@
"settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada.";
"poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel";
"authentication_qr_login_failure_device_not_supported" = "Sidumine selle seadmega ei ole toetatud.";
"room_waiting_other_participants_message" = "Kui kutse saanud kasutajad on liitunud jututoaga %@, siis saad sa nendega suhelda ja jututuba on läbivalt krüptitud";
"room_waiting_other_participants_title" = "Kasutajate liitumise ootel jututoaga %@";
"key_verification_scan_qr_code_information_new_session" = "Suuna oma nutiseadme kaamera oma seadmes kuvatavale QR-koodile ja verifitseeri oma uus sessioon";
"key_verification_scan_qr_code_information_other_user" = "Suuna oma nutiseadme kaamera teise kasutaja seadmes kuvatavale QR-koodile ja verifitseeri tema sessioon";
"key_verification_scan_qr_code_information_other_device" = "Suuna kaamera oma teises seadmes kuvatavale QR-koodile ja verifitseeri see sessioon";
"key_verification_scan_qr_code_information_other_session" = "Suuna oma seadme kaamera teises seadmes kuvatavale QR-koodile ja verifitseeri oma sessioon";
"key_verification_scan_qr_code_title" = "Loe QR-koodi";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Sul puudub ligipääs olemasolevale %@ sessioonile?";
"device_verification_self_verify_open_on_other_device_information" = "Oma krüptitud sõnumite ajaloo lugemiseks pead selle seadme verifitseerima.\n\nAva Element või mõni muu ühilduv Matrixi klient on teises seadmes ja järgi juhendit.";
"device_verification_self_verify_open_on_other_device_title" = "Ava %@ oma teises seadmes";
"room_creation_only_one_email_invite" = "E-posti teel saad saata kutseid vaid ükshaaval";
"launch_loading_delay_warning" = "Selleks võib natuke rohkem aega kuluda.\nTänud ootamast.";
// MARK: - Launch loading
"launch_loading_generic" = "Sinu vestlused on sünkroniseerimisel";
-6
View File
@@ -2616,7 +2616,6 @@
"poll_history_loading_text" = "Afficher les sondages";
"voice_message_broadcast_in_progress_title" = "Impossible de démarrer l'enregistrement vocal";
"home_context_menu_mark_as_unread" = "Marquer comme non lu";
"launch_loading_processing_response" = "Traitement des données\n%@ %%";
"notice_voice_broadcast_ended_by_you" = "Vous avez terminé une diffusion vocale.";
"notice_voice_broadcast_ended" = "%@ a terminé une diffusion vocale.";
"notice_voice_broadcast_live" = "Diffusion en direct";
@@ -2721,12 +2720,7 @@
// MARK: - Voice Broadcast
"voice_broadcast_unauthorized_title" = "Impossible de démarrer une nouvelle diffusion vocale";
"voice_message_broadcast_in_progress_message" = "Vous ne pouvez pas démarrer d'enregistrement vocal car vous diffusez en direct. Veuillez interrompre votre diffusion pour démarrer l'enregistrement vocal";
"launch_loading_server_syncing_nth_attempt" = "Synchronisation avec le serveur\n(%@ tentatives)";
"launch_loading_server_syncing" = "Synchronisation avec le serveur";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migration des données\n%@ %%";
"key_backup_recover_from_private_key_progress" = "%@%% Fini";
"room_details_polls" = "Historique des sondages";
"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout avec Rust (se déconnecter pour désactiver)";
+6 -8
View File
@@ -2654,12 +2654,7 @@
"voice_broadcast_stop_alert_title" = "Megszakítod az élő közvetítést?";
"voice_broadcast_buffering" = "Pufferelés…";
"voice_broadcast_time_left" = "%@ van vissza";
"launch_loading_processing_response" = "Adat feldolgozása\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Szinkronizálás a szerverrel\n(%@ próbálkozás)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Szinkronizálás a szerverrel";
"password_policy_pwd_in_dict_error" = "Ez a jelszó megtalálható a szótárban ezért nem engedélyezett.";
"password_policy_weak_pwd_error" = "Ez a jelszó túl gyenge. Legalább 8 karakternek kell lennie és minden típusból legalább egy: nagybetű, kisbetű, szám és speciális karakter.";
@@ -2707,9 +2702,6 @@
"poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez";
"poll_history_loading_text" = "Szavazások megjelenítése";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Adatok migrálása\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Rust végpontok közötti titkosítás (kikapcsoláshoz kijelentkezés szükséges)";
"settings_labs_confirm_crypto_sdk" = "Ez a funkció még kísérleti fázisban van. Lehet, hogy nem az elvártnak megfelelően fog működni és előre nem látható következménye lehet. A funkció kikapcsolásához egyszerű ki-, és bejelentkezés szükséges. Használata csak saját felelősségre.";
"settings_labs_enable_crypto_sdk" = "Rust végpontok közötti titkosítás";
@@ -2722,3 +2714,9 @@
"poll_history_detail_view_in_timeline" = "Szavazás megjelenítése az idővonalon";
"settings_push_rules_error" = "Hiba történt az értesítések beállításának frissítésekor. Próbáld meg az beállítást újra átkapcsolni.";
"authentication_qr_login_failure_device_not_supported" = "Ezzel az eszközzel való összeköttetés nem támogatott.";
"room_waiting_other_participants_message" = "Miután a meghívott felhasználók csatlakoztak a(z) %@ alkalmazáshoz beszélhet velük és a szoba végpontok között titkosítva lesz";
"room_waiting_other_participants_title" = "%@ alkalmazáshoz csatlakozó felhasználókra várakozás";
"key_verification_scan_qr_code_title" = "QR kód beolvasása";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Nem férsz hozzá létező munkamenethez, %@?";
"device_verification_self_verify_open_on_other_device_title" = "Nyisd meg ezt: %@ a másik eszközön";
"room_creation_only_one_email_invite" = "E-mail meghívóból egyszerre csak egy küldhető";
+16 -10
View File
@@ -2856,12 +2856,6 @@
"key_verification_alert_title" = "Anda punya sesi yang belum diverifikasi";
"user_other_session_permanently_unverified_additional_info" = "Sesi ini tidak mendukung enkripsi jadi tidak dapat diverifikasi.";
"voice_broadcast_time_left" = "Tersisa %@";
"launch_loading_processing_response" = "Memroses data\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Menyinkron dengan server\n(%@ percobaan)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Menyinkron dengan server";
"voice_broadcast_buffering" = "Memuat…";
"voice_broadcast_stop_alert_agree_button" = "Ya, batalkan";
"voice_broadcast_stop_alert_description" = "Apakah Anda ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siarannya, dan rekamanan lengkap akan tersedia dalam ruangan.";
@@ -2909,10 +2903,6 @@
"voice_broadcast_connection_error_title" = "Kesalahan koneksi";
"voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda";
"poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung Rust (keluar dari akun untuk menonaktifkan)";
"settings_labs_confirm_crypto_sdk" = "Ketahui bahwa fitur ini masih dalam masa eksperimental, ini mungkin tidak berfungsi seperti yang diharapkan dan dapat memiliki konsekuensi yang tidak terduga. Untuk mengembalikan fitur, cukup keluar dari akun dan masuk kembali ke akun. Gunakan dengan pengetahuan dan risiko Anda.";
"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung Rust";
@@ -2929,3 +2919,19 @@
"poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa";
"settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda.";
"authentication_qr_login_failure_device_not_supported" = "Penautan dengan perangkat ini tidak didukung.";
"room_waiting_other_participants_message" = "Setelah pengguna yang diundang telah bergabung dengan %@, Anda akan dapat mengobrol dan ruangannya akan terenkripsi secara ujung ke ujung";
"room_waiting_other_participants_title" = "Menunggu pengguna untuk bergabung dengan %@";
"launch_loading_delay_warning" = "Ini mungkin membutuhkan waktu yang lebih lama.\nTerima kasih atas kesabaran Anda.";
// MARK: - Launch loading
"launch_loading_generic" = "Menyinkronkan percakapan Anda";
"key_verification_scan_qr_code_information_new_session" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi Anda yang baru";
"key_verification_scan_qr_code_information_other_session" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi Anda";
"key_verification_scan_qr_code_information_other_device" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi ini";
"key_verification_scan_qr_code_information_other_user" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkatnya untuk memverifikasi sesi";
"key_verification_scan_qr_code_title" = "Pindai kode QR";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Tidak dapat mengakses sesi %@ yang sudah ada?";
"device_verification_self_verify_open_on_other_device_information" = "Anda harus memverifikasi sesi ini supaya dapat membaca riwayat pesan aman Anda.\n\nBuka Element di salah satu perangkat Anda yang lain dan ikuti petunjuknya.";
"device_verification_self_verify_open_on_other_device_title" = "Buka %@ di perangkat Anda yang lain";
"room_creation_only_one_email_invite" = "Amda hanya dapat mengundang satu surel satu-satu";
+16 -10
View File
@@ -2630,12 +2630,6 @@
"user_other_session_permanently_unverified_additional_info" = "Questa sessione non supporta la crittografia, perciò non può essere verificata.";
"voice_broadcast_buffering" = "Buffer...";
"voice_broadcast_time_left" = "%@ rimasti";
"launch_loading_processing_response" = "Elaborazione dati\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Sincronizzazione con il server\n(%@ tentativo)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Sincronizzazione con il server";
"voice_broadcast_stop_alert_agree_button" = "Sì, ferma";
"voice_broadcast_stop_alert_description" = "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.";
"voice_broadcast_stop_alert_title" = "Fermare la trasmissione in diretta?";
@@ -2686,10 +2680,6 @@
"poll_history_no_past_poll_period_text" = "Non ci sono sondaggi passati negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
"poll_history_no_active_poll_period_text" = "Non ci sono sondaggi attivi negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
"poll_history_loading_text" = "Visualizzazione sondaggi";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrazione dati\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end Rust (disconnettiti per disattivarla)";
"settings_labs_confirm_crypto_sdk" = "Si noti che questa funzione, essendo ancora in fase sperimentale, potrebbe non funzionare come previsto e potrebbe avere conseguenze indesiderate. Per disattivare la funzione, è sufficiente disconnettersi e riaccedere. Utilizzare a propria discrezione e con cautela.";
"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end Rust";
@@ -2702,3 +2692,19 @@
"poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale";
"settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione.";
"authentication_qr_login_failure_device_not_supported" = "Il collegamento con questo dispositivo non è supportato.";
"room_waiting_other_participants_message" = "Una volta che gli utenti si saranno uniti a %@, potrete scrivervi e la stanza sarà crittografata end-to-end";
"room_waiting_other_participants_title" = "In attesa che gli utenti si uniscano a %@";
"key_verification_scan_qr_code_information_new_session" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare la tua nuova sessione";
"key_verification_scan_qr_code_information_other_session" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare la tua sessione";
"key_verification_scan_qr_code_information_other_device" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare questa sessione";
"key_verification_scan_qr_code_information_other_user" = "Punta la fotocamera verso il codice QR mostrato sul suo dispositivo per verificare sua la sessione";
"key_verification_scan_qr_code_title" = "Scansiona codice QR";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Non riesci ad accedere a una sessione esistente di %@?";
"device_verification_self_verify_open_on_other_device_information" = "Devi verificare questa sessione per potere leggere la cronologia dei messaggi sicuri.\n\nApri Element su uno degli altri tuoi dispositivi e segui le istruzioni.";
"device_verification_self_verify_open_on_other_device_title" = "Apri %@ sull'altro tuo dispositivo";
"room_creation_only_one_email_invite" = "Puoi invitare una sola email alla volta";
"launch_loading_delay_warning" = "Potrebbe volerci un po' più tempo.\nGrazie per la pazienza.";
// MARK: - Launch loading
"launch_loading_generic" = "Sincronizzazione delle tue conversazioni";
-6
View File
@@ -2591,8 +2591,6 @@
"user_session_unverified_session_description" = "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。";
"user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。";
"user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。";
"launch_loading_server_syncing" = "サーバーと同期しています";
"launch_loading_processing_response" = "データを処理しています\n%@ %%";
"wysiwyg_composer_format_action_link" = "リンクの装飾を適用";
"wysiwyg_composer_format_action_inline_code" = "インラインコードの装飾を適用";
"wysiwyg_composer_format_action_unordered_list" = "箇条書きリストの表示を切り替える";
@@ -2603,9 +2601,6 @@
"settings_labs_enable_crypto_sdk" = "Rust エンドツーエンド暗号化";
"settings_labs_disable_crypto_sdk" = "Rust エンドツーエンド暗号化(無効にするにはログアウトしてください)";
// MARK: - Launch loading
"launch_loading_migrating_data" = "データを移行しています\n%@ %%";
"poll_history_load_more" = "他のアンケートを読み込む";
"key_backup_recover_from_private_key_progress" = "%@%%完了";
"voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。";
@@ -2624,7 +2619,6 @@
"analytics_prompt_title" = "%@の改善を手伝う";
"event_formatter_call_active_video" = "実施中のビデオ通話";
"event_formatter_call_active_voice" = "実施中の音声通話";
"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)";
"create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。";
"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索し、参加できます。";
"searchable_directory_x_network" = "%@ネットワーク";
+5
View File
@@ -2572,3 +2572,8 @@
// User sessions management
"user_sessions_settings" = "Zarządzaj sesjami";
"invite_to" = "Zaproś do %@";
"authentication_qr_login_start_step1" = "Otwórz Element na innym urządzeniu";
"authentication_qr_login_start_subtitle" = "Użyj aparatu tego urządzenia, aby zeskanować kod QR widoczny na innym urządzeniu:";
"authentication_qr_login_start_title" = "Zeskanuj kod QR";
"authentication_login_with_qr" = "Zaloguj się za pomocą kodu QR";
"accessibility_selected" = "wybrane";
-5
View File
@@ -2626,12 +2626,7 @@
"user_session_got_it" = "Entendido";
"user_other_session_permanently_unverified_additional_info" = "Esta sessão não suporta encriptação e assim não pode ser verificada.";
"voice_broadcast_time_left" = "%@ restando";
"launch_loading_processing_response" = "Processando dados\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Sincando com o servidor\n(%@ tentativa)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Sincando com o servidor";
"key_verification_alert_body" = "Revise para assegurar que sua conta está segura.";
// Unverified sessions
+3
View File
@@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ поделились своим местоположением";
/* New voice broadcast from a specific person, not referencing a room. */
"VOICE_BROADCAST_FROM_USER" = "%@ начал голосовую трансляцию";
+24
View File
@@ -2200,3 +2200,27 @@
// MARK: Password policy errors
"password_policy_too_short_pwd_error" = "Очень короткий пароль";
"settings_enable_room_message_bubbles" = "Сообщения пузырями";
"notice_voice_broadcast_ended" = "%@ закончил(а) голосовую трансляцию.";
"notice_voice_broadcast_ended_by_you" = "Вы закончили голосовую трансляцию.";
"room_displayname_more_than_two_members" = "%@ и %@ другие";
"room_details_access_row_title" = "Доступ";
"room_details_polls" = "История опроса";
// User sessions management
"user_sessions_settings" = "Управление сеансами";
"manage_session_sign_out_other_sessions" = "Выйти из всех остальных сеансов";
"manage_session_rename" = "Переименовать сеанс";
"settings_presence_offline_mode_description" = "Если включено, вы будете всегда оффлайн для остальных пользователей, даже при использовании приложения.";
"settings_presence" = "Присутствие";
"settings_discovery_accept_terms" = "Примите Правила Идентификации Сервера";
"settings_labs_enable_voice_broadcast" = "Голосовая трансляция";
"settings_labs_enable_wysiwyg_composer" = "Попробуйте редактор текста";
"settings_labs_enable_new_app_layout" = "Новый Слой Приложения";
"settings_labs_enable_new_session_manager" = "Новый менеджер сессии";
"settings_labs_enable_auto_report_decryption_errors" = "Авто Отчет Ошибок Расшифровки";
"settings_push_rules_error" = "Произошла ошибка при обновлении настроек уведомлений. Пожалуйста, попробуйте переключить свой вариант еще раз.";
"threads_beta_information" = "Держите обсуждения организованными с помощью потоков.\n\nПотоки помогают вести ваши разговоры по теме и их легко отслеживать. ";
"room_creation_only_one_email_invite" = "Вы можете пригласить только один адрес email за раз";
"threads_notice_title" = "Потоки больше не экспериментальная функция 🎉";
"threads_notice_information" = "Все потоки созданные во время экспериментального периода теперь <b>отображаются как обычные ответы</b>.<br/><br/>Это разовый переход, так как потоки теперь часть спецификации Matrix.";
"authentication_qr_login_failure_device_not_supported" = "Связь с этим устройством не поддерживается.";
"accessibility_selected" = "выбранный";
+16 -10
View File
@@ -2852,12 +2852,6 @@
"key_verification_alert_title" = "Máte neoverené relácie";
"user_other_session_permanently_unverified_additional_info" = "Táto relácia nepodporuje šifrovanie, a preto ju nemožno overiť.";
"voice_broadcast_time_left" = "%@ ostáva";
"launch_loading_processing_response" = "Spracovanie údajov\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Synchronizácia so serverom\n(%@ pokus)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Synchronizácia so serverom";
"voice_broadcast_buffering" = "Načítavanie do vyrovnávacej pamäte…";
"voice_broadcast_stop_alert_agree_button" = "Áno, zastaviť";
"voice_broadcast_stop_alert_description" = "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.";
@@ -2905,10 +2899,6 @@
"wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami";
"voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené";
"poll_timeline_reply_ended_poll" = "Ukončená anketa";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Rust end-to-end šifrovanie (odhláste sa, aby ste ho vypli)";
"settings_labs_confirm_crypto_sdk" = "Upozorňujeme, že táto funkcia je stále v experimentálnej fáze, preto nemusí fungovať podľa očakávaní a môže mať potenciálne nezamýšľané dôsledky. Ak chcete funkciu vrátiť späť, jednoducho sa odhláste a znova prihláste. Používajte ju podľa vlastného uváženia a s opatrnosťou.";
"settings_labs_enable_crypto_sdk" = "Rust end-to-end šifrovanie";
@@ -2925,3 +2915,19 @@
"poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi";
"settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova.";
"authentication_qr_login_failure_device_not_supported" = "Prepojenie s týmto zariadením nie je podporované.";
"room_waiting_other_participants_message" = "Keď sa pozvaní používatelia pripoja k aplikácii %@, budete môcť konverzovať a miestnosť bude end-to-end šifrovaná";
"room_waiting_other_participants_title" = "Čaká sa na používateľov, kým sa pripoja k aplikácii %@";
"key_verification_scan_qr_code_information_new_session" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte vašu novú reláciu";
"key_verification_scan_qr_code_information_other_session" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte vašu reláciu";
"key_verification_scan_qr_code_information_other_device" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte túto reláciu";
"key_verification_scan_qr_code_information_other_user" = "Nasmerujte kameru na QR kód zobrazený na ich zariadení a overte ich reláciu";
"key_verification_scan_qr_code_title" = "Skenovať QR kód";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Nemôžete získať prístup k existujúcej relácii %@?";
"device_verification_self_verify_open_on_other_device_information" = "Ak si chcete prečítať históriu zabezpečených správ, musíte túto reláciu overiť.\n\nOtvorte aplikáciu Element na jednom z vašich iných zariadení a postupujte podľa pokynov.";
"device_verification_self_verify_open_on_other_device_title" = "Otvorte %@ na vašom druhom zariadení";
"room_creation_only_one_email_invite" = "Naraz môžete pozvať len jeden e-mail";
"launch_loading_delay_warning" = "Môže to trvať trochu dlhšie.\nĎakujeme za vašu trpezlivosť.";
// MARK: - Launch loading
"launch_loading_generic" = "Synchronizácia vašich konverzácií";
+16 -10
View File
@@ -2625,7 +2625,6 @@
"user_session_inactive_session_description" = "Sesione jo aktive janë sesione që keni ca kohë që si keni përdorur, por që vazhdojnë të marrin kyçe fshehtëzimi.\n\nHeqja e sesioneve jo aktive përmirëson sigurinë dhe punimin dhe e bëjnë të lehtë për ju të identifikoni, nëse një sesion i ri është i dyshimtë.";
"user_session_unverified_session_description" = "Sesione të paverifikuar janë sesione ku keni bërë hyrjen me kredencialet tuaja, por që nuk janë ndër-verifikuar.\n\nDuhet ta bëni veçanërisht të qartë se i njihni këto sesione, ngaqë mund të përfaqësojnë përdorim të paautorizuar të llogarisë tuaj.";
"user_session_verified_session_description" = "Sesione të verifikuar janë ata kudo që përdorni Element-in pasi të keni dhënë frazëkalimin tuaj, ose pasi të keni ripohuar identitetin tuaj përmes një tjetër sesioni të verifikuar.\n\nKjo do të thotë se zotëroni krejt kyçet e nevojshëm për të shkyçur mesazhet tuaj të fshehtëzuar dhe ripohuar përdoruesve të tjerë se e besoni këtë sesion.";
"launch_loading_server_syncing_nth_attempt" = "Po njëkohësohet me shërbyesin\n(Përpjekja e %@)";
"user_session_rename_session_title" = "Riemërtim sesionesh";
"user_session_inactive_session_title" = "Sesione jo aktive";
"user_session_unverified_session_title" = "Sesione të paverifikuar";
@@ -2636,11 +2635,6 @@
"user_sessions_show_location_info" = "Shfaq adresë IP";
"voice_broadcast_time_left" = "Edhe %@";
"voice_broadcast_tile" = "Transmetim zanor";
"launch_loading_processing_response" = "Po përpunohen të dhëna\n%@ %%";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Po njëkohësohet me shërbyesin";
"key_verification_alert_body" = "Shqyrtojini, që të siguroheni se llogaria juaj është e parrezik.";
// Unverified sessions
@@ -2698,10 +2692,6 @@
"voice_broadcast_connection_error_message" = "Mjerisht, sjemi në gjendje të nisim një incizim mu tani. Ju lutemi, riprovoni më vonë.";
"voice_broadcast_connection_error_title" = "Gabim lidhjeje";
"home_context_menu_mark_as_unread" = "Vëri shenjë si i palexuar";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%";
"key_backup_recover_from_private_key_progress" = "Plotësuar %@%%";
"room_details_polls" = "Historik pyetësorësh";
"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust (që ta çaktivizoni, dilni)";
@@ -2712,3 +2702,19 @@
"wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe";
"poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore";
"authentication_qr_login_failure_device_not_supported" = "Nuk mbulohet lidhja me këtë pajisje.";
"room_waiting_other_participants_message" = "Pasi përdoruesit e ftuar të kenë hyrë në %@, do të jeni në gjendje të bisedoni dhe dhoma do të jetë e fshehtëzuar skaj-më-skaj";
"room_waiting_other_participants_title" = "Po pritet që përdoruesit të hyjnë në %@";
"launch_loading_delay_warning" = "Kjo mund të zgjasë pak.\nFaleminderit për durimin.";
// MARK: - Launch loading
"launch_loading_generic" = "Po njëkohësohen bisedat tuaja";
"key_verification_scan_qr_code_information_new_session" = "Që të verifikoni sesionin tuaj të ri, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
"key_verification_scan_qr_code_information_other_session" = "Që të verifikoni sesionin tuaj, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
"key_verification_scan_qr_code_information_other_device" = "Që të verifikoni këtë sesion, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
"key_verification_scan_qr_code_information_other_user" = "Që të verifikoni sesionin e tij, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen e tjetrit";
"key_verification_scan_qr_code_title" = "Skanoni kodin QR";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Shyni dot te një sesion %@ ekzistues?";
"device_verification_self_verify_open_on_other_device_information" = "Lypset të verifikoni këtë sesion, që të mund të lexoni historikun e mesazheve tuaj të siguruar.\n\nHapeni Element-in në një nga pajisjet tuaja të tjera dhe ndiqni udhëzimet.";
"device_verification_self_verify_open_on_other_device_title" = "Hapeni %@ në pajisjen tuaj tjetër";
"room_creation_only_one_email_invite" = "Mund të ftoni vetëm një email në herë";
-8
View File
@@ -2409,12 +2409,7 @@
"spaces_explore_rooms_format" = "Utforska %@";
"spaces_create_subspace_title" = "Skapa ett underutrymme";
"spaces_add_subspace_title" = "Skapa utrymme inuti %@";
"launch_loading_processing_response" = "Hanterar data\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Synkar med servern\n(%@ försök)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Synkar med servern";
"key_verification_alert_body" = "Granska för att försäkra att ditt konto är säkert.";
// Unverified sessions
@@ -2653,9 +2648,6 @@
"voice_broadcast_connection_error_title" = "Anslutningsfel";
"voice_broadcast_playback_lock_screen_placeholder" = "Röstsändning";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrerar data\n%@ %%";
"room_details_polls" = "Omröstningshistorik";
"settings_labs_disable_crypto_sdk" = "Totalsträckskryptering i Rust (logga ut för att stänga av)";
"settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk.";
+16 -10
View File
@@ -2854,12 +2854,6 @@
"key_verification_alert_title" = "У вас є не звірені сеанси";
"user_other_session_permanently_unverified_additional_info" = "Цей сеанс не підтримує шифрування, і його не можна звірити.";
"voice_broadcast_time_left" = "Залишилося %@";
"launch_loading_processing_response" = "Обробка даних\n%@ %%";
"launch_loading_server_syncing_nth_attempt" = "Синхронізація з сервером\n(%@ спроба)";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Синхронізація з сервером";
"voice_broadcast_buffering" = "Буферизація...";
"voice_broadcast_stop_alert_agree_button" = "Так, припинити";
"voice_broadcast_stop_alert_description" = "Ви впевнені, що хочете припинити голосову трансляцію? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.";
@@ -2907,10 +2901,6 @@
"wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список";
"voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено";
"poll_timeline_reply_ended_poll" = "Завершене опитування";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Перенесення даних\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування Rust (вийдіть, щоб вимкнути)";
"settings_labs_confirm_crypto_sdk" = "Зауважте, що оскільки ця функція досі перебуває на стадії експерименту, вона може працювати не так, як очікується, і може мати непередбачувані наслідки. Щоб вимкнути цю функцію, просто вийдіть з системи та увійдіть знову. Використовуйте на власний розсуд і з обережністю.";
"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування Rust";
@@ -2927,3 +2917,19 @@
"settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз.";
"poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці";
"authentication_qr_login_failure_device_not_supported" = "Пов'язування з цим пристроєм не підтримується.";
"room_waiting_other_participants_message" = "Після того, як запрошені користувачі приєднаються до %@, ви зможете спілкуватися з ними, а кімната буде захищена наскрізним шифруванням";
"room_waiting_other_participants_title" = "Очікування коли користувачі приєднаються до %@";
"key_verification_scan_qr_code_information_new_session" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
"key_verification_scan_qr_code_information_other_session" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
"key_verification_scan_qr_code_information_other_device" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
"key_verification_scan_qr_code_information_other_user" = "Наведіть камеру на QR-код, що показаний на їхньому пристрої, щоб звірити сеанс";
"key_verification_scan_qr_code_title" = "Сканувати QR-код";
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Не можете отримати доступ до наявного сеансу %@?";
"device_verification_self_verify_open_on_other_device_information" = "Вам потрібно звірити цей сеанс, щоб прочитати історію захищених повідомлень.\n\nВідкрийте Element на одному з інших пристроїв і дотримуйтесь інструкцій.";
"device_verification_self_verify_open_on_other_device_title" = "Відкрийте %@ на іншому своєму пристрої";
"room_creation_only_one_email_invite" = "Ви можете запросити лише одну адресу електронної пошти за раз";
"launch_loading_delay_warning" = "Це може тривати трохи довше.\nДякуємо за ваше терпіння.";
// MARK: - Launch loading
"launch_loading_generic" = "Синхронізація ваших розмов";
+8 -8
View File
@@ -1,9 +1,9 @@
// Permissions usage explanations
"NSCameraUsageDescription" = "相機權限會用來拍攝照片影片,與進行視訊通話。";
"NSPhotoLibraryUsageDescription" = "允許讀取照片圖庫權限用來傳送照片與影片。";
"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來進行語音通話、視訊通話與錄製語音訊息。";
"NSContactsUsageDescription" = "這將會分享給身份伺服器以便在 Matrix 尋找您的聯絡人。";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "當您分享您的位置給其他人時Element 需要權限顯示地圖。";
"NSLocationWhenInUseUsageDescription" = "當您分享您的位置給其他人時Element 需要權限顯示地圖。";
"NSFaceIDUsageDescription" = "已啟用 Face ID 來使用您的應用程式。";
"NSCalendarsUsageDescription" = "檢視您已排定的會議。";
"NSCameraUsageDescription" = "給予相機權限會用來進行視訊通話或是拍攝並上傳照片影片。";
"NSPhotoLibraryUsageDescription" = "同意使用圖片的權限用來上傳您圖庫的照片與影片。";
"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來接受通話、拍攝影片以及錄製語音訊息。";
"NSContactsUsageDescription" = "他們會與您的身分伺服器共享以找到您在Matrix的聯絡人。";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示地圖。";
"NSLocationWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示地圖。";
"NSFaceIDUsageDescription" = "您可以使用 Face ID 來登入您的應用程式。";
"NSCalendarsUsageDescription" = "在應用程式中查看您已預約的會議。";
+30 -27
View File
@@ -1,11 +1,11 @@
/* New message from a specific person, not referencing a room */
"MSG_FROM_USER" = "%@ 傳來的訊息";
"MSG_FROM_USER" = "來自 %@ 的訊息";
/* New message from a specific person in a named room */
"MSG_FROM_USER_IN_ROOM" = "%@ 在 %@ 貼文";
/* New message from a specific person, not referencing a room. Content included. */
"MSG_FROM_USER_WITH_CONTENT" = "%@%@";
/* New message from a specific person in a named room. Content included. */
"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ %@%@";
"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ 來自 %@%@";
/* New action message from a specific person, not referencing a room. */
"ACTION_FROM_USER" = "* %@ %@";
/* New action message from a specific person in a named room. */
@@ -26,13 +26,13 @@
/* Multiple messages in two rooms */
"MSGS_IN_TWO_ROOMS" = "%@ 個新訊息來自 %@ 與 %@";
/* Look, stuff's happened, alright? Just open the app. */
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 個新訊息來自 %@、%@ 與其他";
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 個新訊息來自 %@、%@ 與其他";
/* A user has invited you to a chat */
"USER_INVITE_TO_CHAT" = "%@ 已經邀請您來聊天";
"USER_INVITE_TO_CHAT" = "%@ 邀請您來聊天";
/* A user has invited you to an (unamed) group chat */
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ 已經邀請您到群組聊天中";
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ 邀請您到群組聊天中";
/* A user has invited you to a named room */
"USER_INVITE_TO_NAMED_ROOM" = "%@ 已經邀請您加入 %@";
"USER_INVITE_TO_NAMED_ROOM" = "%@ 邀請您加入 %@";
/* Incoming one-to-one voice call */
"VOICE_CALL_FROM_USER" = "來自 %@ 的通話";
/* Incoming one-to-one video call */
@@ -46,14 +46,14 @@
/* Incoming named video conference invite from a specific person */
"VIDEO_CONF_NAMED_FROM_USER" = "來自 %@ 的視訊群組通話:'%@'";
/* A single unread message in a room */
"SINGLE_UNREAD_IN_ROOM" = "您在 %@ 中收到一則訊息";
"SINGLE_UNREAD_IN_ROOM" = "您在 %@ 中收到一則訊息";
/* A single unread message */
"SINGLE_UNREAD" = "您收到一則訊息";
"SINGLE_UNREAD" = "您收到一則訊息";
/* Message title for a specific person in a named room */
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ %@";
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ 來自 %@";
/* New message reply from a specific person in a named room. */
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ %@ 回覆";
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ 已從 %@ 回覆";
/* New message reply from a specific person, not referencing a room. */
"REPLY_FROM_USER_TITLE" = "%@ 已回覆";
@@ -63,41 +63,41 @@
/** Key verification **/
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ 請求驗證";
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ 希望驗證";
/* Group call from user, CallKit caller name */
"GROUP_CALL_FROM_USER" = "%@ 群組通話)";
"GROUP_CALL_FROM_USER" = "%@ (群組通話)";
/* A user added a Jitsi call to a room */
"GROUP_CALL_STARTED" = "群組通話開始";
"GROUP_CALL_STARTED" = "群組對話已開始";
/* A user's membership has updated in an unknown way */
"USER_MEMBERSHIP_UPDATED" = "%@ 更新了簡介";
"USER_MEMBERSHIP_UPDATED" = "%@ 更新了個人資料";
/* A user has change their name to a new name which we don't know */
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱";
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更了名字";
/** Membership Updates **/
/* A user has change their name to a new name */
"USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱為 %@";
"USER_UPDATED_DISPLAYNAME" = "%@ 把名稱變更為 %@";
/* A user has change their avatar */
"USER_UPDATED_AVATAR" = "%@ 變更頭像";
"USER_UPDATED_AVATAR" = "%@ 變更了他們的頭像";
/* A user has reacted to a message, but the reaction content is unknown */
"GENERIC_REACTION_FROM_USER" = "%@ 送出一個反應";
"GENERIC_REACTION_FROM_USER" = "%@ 送出一個反應";
/** Reactions **/
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
"REACTION_FROM_USER" = "%@ 覺得 %@";
"REACTION_FROM_USER" = "%@ %@ 做出了反應";
/* New message with hidden content due to PIN enabled */
"MESSAGE_PROTECTED" = "新訊息";
/* New message indicator on a room */
"MESSAGE_IN_X" = " %@ 的訊息";
"MESSAGE_IN_X" = "來自 %@ 的訊息";
/* New message indicator from a DM */
"MESSAGE_FROM_X" = "來自 %@ 的訊息";
@@ -108,24 +108,27 @@
"MESSAGE" = "訊息";
/* Sticker from a specific person, not referencing a room. */
"STICKER_FROM_USER" = "%@ 戳你一下";
"STICKER_FROM_USER" = "%@ 傳了一張貼圖";
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ 分享的位置";
"LOCATION_FROM_USER" = "%@ 分享了他們的位置";
/* New file message from a specific person, not referencing a room. */
"FILE_FROM_USER" = "%@ 送一個檔案 %@";
"FILE_FROM_USER" = "%@ 送一個檔案 %@";
/* New voice message from a specific person, not referencing a room. */
"VOICE_MESSAGE_FROM_USER" = "%@ 傳送一個語音訊息";
"VOICE_MESSAGE_FROM_USER" = "%@ 送出一段語音訊息";
/* New audio message from a specific person, not referencing a room. */
"AUDIO_FROM_USER" = "%@ 傳送一個音訊檔案 %@";
"AUDIO_FROM_USER" = "%@ 送出一個語音檔案 %@";
/* New video message from a specific person, not referencing a room. */
"VIDEO_FROM_USER" = "%@ 傳送一個影片";
"VIDEO_FROM_USER" = "%@ 送出一段影片";
/** Media Messages **/
/* New image message from a specific person, not referencing a room. */
"PICTURE_FROM_USER" = "%@ 送一張圖片";
"PICTURE_FROM_USER" = "%@ 送一張圖片";
/* New voice broadcast from a specific person, not referencing a room. */
"VOICE_BROADCAST_FROM_USER" = "%@ 開始語音廣播";
File diff suppressed because it is too large Load Diff
+5 -25
View File
@@ -20,7 +20,7 @@
#import "AvatarGenerator.h"
#import "MatrixKit.h"
#import "GeneratedInterface-Swift.h"
#import <objc/runtime.h>
@implementation MXRoom (Riot)
@@ -331,30 +331,10 @@
{
[self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) {
UserEncryptionTrustLevel userEncryptionTrustLevel;
double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted;
if (trustedDevicesPercentage >= 1.0)
{
userEncryptionTrustLevel = UserEncryptionTrustLevelTrusted;
}
else if (trustedDevicesPercentage == 0.0)
{
// Verify if the user has the user has cross-signing enabled
if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId])
{
userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified;
}
else
{
userEncryptionTrustLevel = UserEncryptionTrustLevelNoCrossSigning;
}
}
else
{
userEncryptionTrustLevel = UserEncryptionTrustLevelWarning;
}
MXCrossSigningInfo *crossSigningInfo = [self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId];
EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
UserEncryptionTrustLevel userEncryptionTrustLevel = [encryption userTrustLevelWithCrossSigning:crossSigningInfo
devicesTrust:usersTrustLevelSummary.devicesTrust];
onComplete(userEncryptionTrustLevel);
} failure:^(NSError *error) {
+1 -11
View File
@@ -15,17 +15,7 @@
*/
#import "MatrixKit.h"
/**
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
*/
typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
RoomEncryptionTrustLevelTrusted,
RoomEncryptionTrustLevelWarning,
RoomEncryptionTrustLevelNormal,
RoomEncryptionTrustLevelUnknown
};
#import "RoomEncryptionTrustLevel.h"
/**
Define a `MXRoomSummary` category at Riot level.
+6 -23
View File
@@ -33,32 +33,15 @@
- (RoomEncryptionTrustLevel)roomEncryptionTrustLevel
{
RoomEncryptionTrustLevel roomEncryptionTrustLevel = RoomEncryptionTrustLevelUnknown;
if (self.trust)
MXUsersTrustLevelSummary *trust = self.trust;
if (!trust)
{
double trustedUsersPercentage = self.trust.trustedUsersProgress.fractionCompleted;
double trustedDevicesPercentage = self.trust.trustedDevicesProgress.fractionCompleted;
if (trustedUsersPercentage >= 1.0)
{
if (trustedDevicesPercentage >= 1.0)
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelTrusted;
}
else
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelWarning;
}
}
else
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelNormal;
}
roomEncryptionTrustLevel = roomEncryptionTrustLevel;
MXLogError(@"[MXRoomSummary] roomEncryptionTrustLevel: trust is missing");
return RoomEncryptionTrustLevelUnknown;
}
return roomEncryptionTrustLevel;
EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
return [encryption roomTrustLevelWithSummary:trust];
}
- (BOOL)isJoined
+11 -5
View File
@@ -15,6 +15,7 @@
//
import Foundation
import MatrixSDKCrypto
/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status
/// of `CryptoSDK`, and which uses feature flags to control rollout availability.
@@ -30,6 +31,11 @@ import Foundation
@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature {
@objc static let shared = CryptoSDKFeature()
var version: String {
// Will be moved into the olm machine as API
Bundle(for: OlmMachine.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
}
var isEnabled: Bool {
RiotSettings.shared.enableCryptoSDK
}
@@ -38,14 +44,14 @@ import Foundation
private let remoteFeature: RemoteFeaturesClientProtocol
private let localFeature: PhasedRolloutFeature
init(remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared) {
init(
remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared,
localTargetPercentage: Double = 0.2
) {
self.remoteFeature = remoteFeature
self.localFeature = PhasedRolloutFeature(
name: Self.FeatureName,
// Local feature is currently set to 0% target, and all availability is fully controlled
// by the remote feature. Once the remote is fully rolled out, target for local feature will
// be gradually increased.
targetPercentage: 0.0
targetPercentage: localTargetPercentage
)
}
+1
View File
@@ -244,6 +244,7 @@ internal class Asset: NSObject {
internal static let locationPinIcon = ImageAsset(name: "location_pin_icon")
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
internal static let pillUser = ImageAsset(name: "pill_user")
internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default")
internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected")
internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon")
+22 -14
View File
@@ -3191,21 +3191,13 @@ public class VectorL10n: NSObject {
public static var later: String {
return VectorL10n.tr("Vector", "later")
}
/// Migrating data\n%@ %%
public static func launchLoadingMigratingData(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_migrating_data", p1)
/// This may take a little longer.\nThanks for your patience.
public static var launchLoadingDelayWarning: String {
return VectorL10n.tr("Vector", "launch_loading_delay_warning")
}
/// Processing data\n%@ %%
public static func launchLoadingProcessingResponse(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_processing_response", p1)
}
/// Syncing with the server
public static var launchLoadingServerSyncing: String {
return VectorL10n.tr("Vector", "launch_loading_server_syncing")
}
/// Syncing with the server\n(%@ attempt)
public static func launchLoadingServerSyncingNthAttempt(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_server_syncing_nth_attempt", p1)
/// Syncing your conversations
public static var launchLoadingGeneric: String {
return VectorL10n.tr("Vector", "launch_loading_generic")
}
/// Leave
public static var leave: String {
@@ -4699,6 +4691,22 @@ public class VectorL10n: NSObject {
public static func photoLibraryAccessNotGranted(_ p1: String) -> String {
return VectorL10n.tr("Vector", "photo_library_access_not_granted", p1)
}
/// Message
public static var pillMessage: String {
return VectorL10n.tr("Vector", "pill_message")
}
/// Message from %@
public static func pillMessageFrom(_ p1: String) -> String {
return VectorL10n.tr("Vector", "pill_message_from", p1)
}
/// Message in %@
public static func pillMessageIn(_ p1: String) -> String {
return VectorL10n.tr("Vector", "pill_message_in", p1)
}
/// Space/Room
public static var pillRoomFallbackDisplayName: String {
return VectorL10n.tr("Vector", "pill_room_fallback_display_name")
}
/// Create a PIN for security
public static var pinProtectionChoosePin: String {
return VectorL10n.tr("Vector", "pin_protection_choose_pin")
@@ -46,6 +46,9 @@ struct SentryMonitoringClient {
if let message = event.message?.formatted {
event.fingerprint = [message]
}
event.tags = [
"crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId
]
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
return event
}
@@ -0,0 +1,49 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Object responsible for calculating user and room trust level
@objc class EncryptionTrustLevel: NSObject {
/// Calculate trust level for a single user given their cross-signing info
@objc func userTrustLevel(
crossSigning: MXCrossSigningInfo?,
devicesTrust: MXTrustSummary
) -> UserEncryptionTrustLevel {
// If we could cross-sign but we haven't, the user is simply not verified
if let crossSigning, !crossSigning.isVerified {
return .notVerified
// If we cannot cross-sign the user (legacy behaviour) and have not signed
// any devices manually, the user is not verified
} else if crossSigning == nil && devicesTrust.trustedCount == 0 {
return .notVerified
}
// In all other cases we check devices for trust level
return devicesTrust.areAllTrusted ? .trusted : .warning
}
/// Calculate trust level for a room given trust level of users and their devices
@objc func roomTrustLevel(summary: MXUsersTrustLevelSummary) -> RoomEncryptionTrustLevel {
guard summary.usersTrust.totalCount > 0 && summary.usersTrust.areAllTrusted else {
return .normal
}
return summary.devicesTrust.areAllTrusted ? .trusted : .warning
}
}
@@ -0,0 +1,25 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
/**
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
*/
typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
RoomEncryptionTrustLevelTrusted,
RoomEncryptionTrustLevelWarning,
RoomEncryptionTrustLevelNormal,
RoomEncryptionTrustLevelUnknown
};
@@ -366,7 +366,8 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
view.backgroundColor = .clear
let avatarInsets: UIEdgeInsets = .init(top: 7, left: 7, bottom: 7, right: 7)
let button: UIButton = .init(frame: view.bounds.inset(by: avatarInsets))
let button: UIButton = .init(frame: view.bounds)
button.imageEdgeInsets = avatarInsets
button.setImage(Asset.Images.tabPeople.image, for: .normal)
button.menu = avatarMenu
button.showsMenuAsPrimaryAction = true
@@ -386,12 +387,12 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
}
private func updateAvatarButtonItem() {
guard let avatarView = avatarMenuView, let button = avatarMenuButton, let avatar = userAvatarViewData(from: currentMatrixSession) else {
return
if let avatar = userAvatarViewData(from: currentMatrixSession) {
avatarMenuView?.fill(with: avatar)
avatarMenuButton?.setImage(nil, for: .normal)
} else {
avatarMenuButton?.setImage(Asset.Images.tabPeople.image, for: .normal)
}
button.setImage(nil, for: .normal)
avatarView.fill(with: avatar)
}
private func showRoom(withId roomId: String, eventId: String? = nil) {
@@ -30,6 +30,8 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
// MARK: - Properties
@IBOutlet private weak var animationView: ElementView!
@IBOutlet private weak var progressContainer: UIStackView!
@IBOutlet private weak var progressView: UIProgressView!
@IBOutlet private weak var statusLabel: UILabel!
private var animationTimeline: Timeline_1!
@@ -54,7 +56,7 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
animationTimeline.play()
self.animationTimeline = animationTimeline
self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableStartupProgress
progressContainer.isHidden = true
}
// MARK: - Public
@@ -66,18 +68,18 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
}
extension LaunchLoadingView: MXSessionStartupProgressDelegate {
func sessionDidUpdateStartupStage(_ stage: MXSessionStartupStage) {
func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) {
guard MXSDKOptions.sharedInstance().enableStartupProgress else {
return
}
updateStatusText(for: stage)
update(with: state)
}
private func updateStatusText(for stage: MXSessionStartupStage) {
private func update(with state: MXSessionStartupProgress.State) {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.updateStatusText(for: stage)
self?.update(with: state)
}
return
}
@@ -85,24 +87,9 @@ extension LaunchLoadingView: MXSessionStartupProgressDelegate {
// Sync may be doing a lot of heavy work on the main thread and the status text
// does not update reliably enough without explicitly refreshing
CATransaction.begin()
statusLabel.text = statusText(for: stage)
progressContainer.isHidden = false
progressView.progress = Float(state.progress)
statusLabel.text = state.showDelayWarning ? VectorL10n.launchLoadingDelayWarning : VectorL10n.launchLoadingGeneric
CATransaction.commit()
}
private func statusText(for stage: MXSessionStartupStage) -> String {
switch stage {
case .migratingData(let progress):
let percent = Int(floor(progress * 100))
return VectorL10n.launchLoadingMigratingData("\(percent)")
case .serverSyncing(let attempts):
if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) {
return VectorL10n.launchLoadingServerSyncingNthAttempt(nth)
} else {
return VectorL10n.launchLoadingServerSyncing
}
case .processingResponse(let progress):
let percent = Int(floor(progress * 100))
return VectorL10n.launchLoadingProcessingResponse("\(percent)")
}
}
}
@@ -14,32 +14,52 @@
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleAspectFit" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Element" customModuleProvider="target">
<view contentMode="scaleAspectFit" placeholderIntrinsicWidth="130" placeholderIntrinsicHeight="130" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="95" y="219" width="130" height="130"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wzS-bN-Pht">
<rect key="frame" x="20" y="528" width="280" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" systemColor="systemGrayColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="hme-ss-nFP">
<rect key="frame" x="20" y="472" width="280" height="36"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Syncing" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wzS-bN-Pht">
<rect key="frame" x="0.0" y="0.0" width="280" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" systemColor="systemGrayColor"/>
<nil key="highlightedColor"/>
</label>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DEk-Va-xdT">
<rect key="frame" x="0.0" y="32" width="280" height="4"/>
<color key="tintColor" red="0.050980392156862744" green="0.74117647058823533" blue="0.54509803921568623" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</progressView>
</subviews>
<constraints>
<constraint firstAttribute="width" priority="750" constant="350" id="hle-Py-f0J"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="wzS-bN-Pht" secondAttribute="trailing" constant="20" id="Naf-Cc-qLq"/>
<constraint firstAttribute="bottom" secondItem="wzS-bN-Pht" secondAttribute="bottom" constant="40" id="cnE-Pn-Wb2"/>
<constraint firstAttribute="bottom" secondItem="hme-ss-nFP" secondAttribute="bottom" constant="60" id="Ayr-Nh-R8M"/>
<constraint firstItem="hme-ss-nFP" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="Ecw-cR-GaE"/>
<constraint firstItem="hme-ss-nFP" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="KSs-HF-BlI"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="hme-ss-nFP" secondAttribute="trailing" constant="20" id="U6l-o1-IL3"/>
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ig4-YX-FoT"/>
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="r9K-7c-fjh"/>
<constraint firstItem="wzS-bN-Pht" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="uZP-JW-dVR"/>
</constraints>
<connections>
<outlet property="animationView" destination="3KG-IR-FPV" id="Are-fn-laY"/>
<outlet property="progressContainer" destination="hme-ss-nFP" id="XC1-4m-OD7"/>
<outlet property="progressView" destination="DEk-Va-xdT" id="PCI-Vi-Zyg"/>
<outlet property="statusLabel" destination="wzS-bN-Pht" id="Mj2-rn-i5x"/>
</connections>
<point key="canvasLocation" x="136.875" y="132.5"/>
</view>
</objects>
<designables>
<designable name="3KG-IR-FPV">
<size key="intrinsicContentSize" width="130" height="130"/>
</designable>
</designables>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -206,10 +206,10 @@
// Only show a warning badge if there are trust issues.
if (event.sender)
{
MXUserTrustLevel *userTrustLevel = [session.crypto trustLevelForUser:event.sender];
BOOL isUserVerified = [session.crypto isUserVerified:event.sender];
MXDeviceInfo *deviceInfo = [session.crypto eventDeviceInfo:event];
if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified)
if (isUserVerified && !deviceInfo.trustLevel.isVerified)
{
return EventEncryptionDecorationUntrustedDevice;
}
@@ -1362,14 +1362,21 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
{
body = body? body : [VectorL10n noticeFileAttachment];
NSDictionary *fileInfo = contentToUse[@"info"];
NSDictionary *fileInfo;
MXJSONModelSetDictionary(fileInfo, contentToUse[@"info"]);
if (fileInfo)
{
NSNumber *fileSize = fileInfo[@"size"];
NSNumber *fileSize;
MXJSONModelSetNumber(fileSize, fileInfo[@"size"])
if (fileSize)
{
body = [NSString stringWithFormat:@"%@ (%@)", body, [MXTools fileSizeToString: fileSize.longValue]];
}
else
{
MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported m.file format: %@", event.description);
*error = MXKEventFormatterErrorUnsupported;
}
}
}
else
+29 -1
View File
@@ -36,6 +36,10 @@
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
// Regex expression for permalink detection
NSString *const kMXKToolsRegexStringForPermalink = @"\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";
#pragma mark - MXKTools static private members
// The regex used to find matrix ids.
static NSRegularExpression *userIdRegex;
@@ -47,6 +51,8 @@ static NSRegularExpression *httpLinksRegex;
// A regex to find all HTML tags
static NSRegularExpression *htmlTagsRegex;
static NSDataDetector *linkDetector;
// A regex to detect permalinks
static NSRegularExpression* permalinkRegex;
@implementation MXKTools
@@ -63,6 +69,9 @@ static NSDataDetector *linkDetector;
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSString *permalinkPattern = [NSString stringWithFormat:@"%@%@", BuildSettings.clientPermalinkBaseUrl ?: kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink];
permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil];
});
}
@@ -1039,10 +1048,29 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
{
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
}
// Permalinks
NSArray* matches = [httpLinksRegex matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
if (matches) {
for (NSTextCheckingResult *match in matches)
{
NSRange matchRange = [match range];
NSString *link = [mutableAttributedString.string substringWithRange:matchRange];
// Handle potential permalinks
if ([permalinkRegex numberOfMatchesInString:link options:0 range:NSMakeRange(0, link.length)]) {
NSURLComponents *url = [[NSURLComponents new] initWithString:link];
if (url.URL)
{
[mutableAttributedString addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
}
}
}
}
// This allows to check for normal url based links (like https://element.io)
// And set back the default link color
NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
if (matches)
{
for (NSTextCheckingResult *match in matches)
+97 -31
View File
@@ -25,7 +25,9 @@ class PillAttachmentView: UIView {
struct Sizes {
var verticalMargin: CGFloat
var horizontalMargin: CGFloat
var avatarLeading: CGFloat
var avatarSideLength: CGFloat
var itemSpacing: CGFloat
var pillBackgroundHeight: CGFloat {
return avatarSideLength + 2 * verticalMargin
@@ -33,11 +35,8 @@ class PillAttachmentView: UIView {
var pillHeight: CGFloat {
return pillBackgroundHeight + 2 * verticalMargin
}
var displaynameLabelLeading: CGFloat {
return avatarSideLength + 2 * horizontalMargin
}
var totalWidthWithoutLabel: CGFloat {
return displaynameLabelLeading + 2 * horizontalMargin
return avatarSideLength + 2 * horizontalMargin
}
}
@@ -56,44 +55,111 @@ class PillAttachmentView: UIView {
mediaManager: MXMediaManager?,
andPillData pillData: PillTextAttachmentData) {
self.init(frame: frame)
let label = UILabel(frame: .zero)
label.text = pillData.displayText
label.font = pillData.font
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
height: sizes.pillBackgroundHeight))
label.frame = CGRect(x: sizes.displaynameLabelLeading,
y: 0,
width: labelSize.width,
height: sizes.pillBackgroundHeight)
let stack = UIStackView(frame: frame)
stack.axis = .horizontal
stack.alignment = .center
stack.spacing = sizes.itemSpacing
var computedWidth: CGFloat = 0
for item in pillData.items {
switch item {
case .text(let string):
let label = UILabel(frame: .zero)
label.text = string
label.font = pillData.font
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
stack.addArrangedSubview(label)
computedWidth += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: sizes.pillBackgroundHeight)).width
case .avatar(let url, let alt, let matrixId):
let avatarView = UserAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
displayName: alt,
avatarUrl: url,
mediaManager: mediaManager,
fallbackImage: .matrixItem(matrixId, alt)))
avatarView.isUserInteractionEnabled = false
avatarView.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(avatarView)
NSLayoutConstraint.activate([
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
])
computedWidth += sizes.avatarSideLength
case .spaceAvatar(let url, let alt, let matrixId):
let avatarView = SpaceAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
displayName: alt,
avatarUrl: url,
mediaManager: mediaManager,
fallbackImage: .matrixItem(matrixId, alt)))
avatarView.isUserInteractionEnabled = false
avatarView.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(avatarView)
NSLayoutConstraint.activate([
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
])
computedWidth += sizes.avatarSideLength
case .asset(let name, let parameters):
let assetView = UIView(frame: CGRect(x: 0, y: 0, width: sizes.avatarSideLength, height: sizes.avatarSideLength))
assetView.backgroundColor = parameters.backgroundColor?.uiColor
assetView.layer.cornerRadius = sizes.avatarSideLength / 2
assetView.isUserInteractionEnabled = false
assetView.translatesAutoresizingMaskIntoConstraints = false
let imageView = UIImageView(frame: .zero)
imageView.image = ImageAsset(name: name).image.withRenderingMode(UIImage.RenderingMode(rawValue: parameters.rawRenderingMode) ?? .automatic)
imageView.tintColor = parameters.tintColor?.uiColor ?? theme.baseIconPrimaryColor
imageView.contentMode = .scaleAspectFit
assetView.vc_addSubViewMatchingParent(imageView, withInsets: UIEdgeInsets(top: parameters.padding, left: parameters.padding, bottom: -parameters.padding, right: -parameters.padding))
stack.addArrangedSubview(assetView)
NSLayoutConstraint.activate([
assetView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
assetView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
])
computedWidth += sizes.avatarSideLength
}
}
computedWidth += max(0, CGFloat(stack.arrangedSubviews.count - 1) * stack.spacing)
let leadingStackMargin: CGFloat
switch pillData.items.first {
case .asset, .avatar:
leadingStackMargin = sizes.avatarLeading
computedWidth += sizes.avatarLeading + sizes.horizontalMargin
default:
leadingStackMargin = sizes.horizontalMargin
computedWidth += 2 * sizes.horizontalMargin
}
let pillBackgroundView = UIView(frame: CGRect(x: 0,
y: sizes.verticalMargin,
width: labelSize.width + sizes.totalWidthWithoutLabel,
width: computedWidth,
height: sizes.pillBackgroundHeight))
let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
y: sizes.verticalMargin,
width: sizes.avatarSideLength,
height: sizes.avatarSideLength))
avatarView.fill(with: AvatarViewData(matrixItemId: pillData.matrixItemId,
displayName: pillData.displayName,
avatarUrl: pillData.avatarUrl,
mediaManager: mediaManager,
fallbackImage: .matrixItem(pillData.matrixItemId, pillData.displayName)))
avatarView.isUserInteractionEnabled = false
pillBackgroundView.addSubview(avatarView)
pillBackgroundView.addSubview(label)
pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -sizes.horizontalMargin))
pillBackgroundView.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
self.addSubview(pillBackgroundView)
self.alpha = pillData.alpha
}
// MARK: - Override
override var isHidden: Bool {
get {
@@ -20,9 +20,11 @@ import UIKit
@available(iOS 15.0, *)
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
// MARK: - Properties
private static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
horizontalMargin: 4.0,
avatarSideLength: 16.0)
static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
horizontalMargin: 6.0,
avatarLeading: 2.0,
avatarSideLength: 16.0,
itemSpacing: 4)
private weak var messageTextView: MXKMessageTextView?
// MARK: - Override
@@ -47,8 +49,7 @@ import UIKit
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: Self.size(forDisplayText: pillData.displayText,
andFont: pillData.font)),
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)),
sizes: Self.pillAttachmentViewSizes,
theme: ThemeService.shared().theme,
mediaManager: mainSession?.mediaManager,
@@ -57,23 +58,3 @@ import UIKit
messageTextView?.registerPillView(pillView)
}
}
@available(iOS 15.0, *)
extension PillAttachmentViewProvider {
/// Computes size required to display a pill for given display text.
///
/// - Parameters:
/// - displayText: display text for the pill
/// - font: the text font
/// - Returns: required size for pill
static func size(forDisplayText displayText: String, andFont font: UIFont) -> CGSize {
let label = UILabel(frame: .zero)
label.text = displayText
label.font = font
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
height: pillAttachmentViewSizes.pillBackgroundHeight))
return CGSize(width: labelSize.width + pillAttachmentViewSizes.totalWidthWithoutLabel,
height: pillAttachmentViewSizes.pillHeight)
}
}
+296
View File
@@ -0,0 +1,296 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@available (iOS 15.0, *)
private enum PillAttachmentKind {
case attachment(PillTextAttachment)
case string(NSAttributedString)
}
@available (iOS 15.0, *)
struct PillProvider {
private let session: MXSession
private let eventFormatter: MXKEventFormatter
private let event: MXEvent
private let roomState: MXRoomState
private let latestRoomState: MXRoomState?
private let isEditMode: Bool
init(withSession session: MXSession,
eventFormatter: MXKEventFormatter,
event: MXEvent,
roomState: MXRoomState,
andLatestRoomState latestRoomState: MXRoomState?,
isEditMode: Bool) {
self.session = session
self.eventFormatter = eventFormatter
self.event = event
self.roomState = roomState
self.latestRoomState = latestRoomState
self.isEditMode = isEditMode
}
func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? {
// Try to get a pill from this url
guard let pillType = PillType.from(url: url) else {
return nil
}
// Do not pillify an url if it is a markdown or an http link (except for user and room) with a custom text
// First, we need to handle the case where the label can contains more than one # (room alias)
var urlFromLabel = URL(string: label)?.absoluteURL
if urlFromLabel == nil, label.filter({ $0 == "#" }).count > 1 {
if let escapedLabel = label.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedLabel) {
urlFromLabel = Tools.fixURL(withSeveralHashKeys: url)
}
}
let fixedUrl = Tools.fixURL(withSeveralHashKeys: url)
let isUrlMarkDownLink = urlFromLabel != fixedUrl
let result: PillAttachmentKind
switch pillType {
case .user(let userId):
var userFound = false
result = pillTextAttachment(forUserId: userId, userFound: &userFound)
// if it is a markdown link and we didn't found the user, don't pillify it
if isUrlMarkDownLink && !userFound {
return nil
}
case .room(let roomId):
var roomFound = false
result = pillTextAttachment(forRoomId: roomId, roomFound: &roomFound)
// if it is a markdown link and we didn't found the room, don't pillify it
if isUrlMarkDownLink && !roomFound {
return nil
}
case .message(let roomId, let messageId):
// if it is a markdown link, don't pillify it
if isUrlMarkDownLink {
return nil
}
result = pillTextAttachment(forMessageId: messageId, inRoomId: roomId)
}
switch result {
case .attachment(let pillTextAttachment):
return PillsFormatter.attributedStringWithAttachment(pillTextAttachment, link: isEditMode ? nil : url, font: eventFormatter.defaultTextFont)
case .string(let attributedString):
// if we don't have an attachment, use the fallback attributed string
let newAttrString = NSMutableAttributedString(attributedString: attributedString)
if let font = eventFormatter.defaultTextFont {
newAttrString.addAttribute(.font, value: font, range: .init(location: 0, length: newAttrString.length))
}
newAttrString.addAttribute(.foregroundColor, value: ThemeService.shared().theme.colors.links, range: .init(location: 0, length: newAttrString.length))
newAttrString.addAttribute(.link, value: url, range: .init(location: 0, length: newAttrString.length))
return newAttrString
}
}
/// Retrieve the latest available `MXRoomMember` from given data.
///
/// - Parameters:
/// - userId: the id of the user
/// - Returns: the room member, if available
private func roomMember(withUserId userId: String) -> MXRoomMember? {
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
}
/// Create a pill representation for a given user
/// - Parameters:
/// - userId: the user MatrixID
/// - userFound: this flag will be set to true if a user is found locally with this userId
/// - Returns: a pill attachment
private func pillTextAttachment(forUserId userId: String, userFound: inout Bool) -> PillAttachmentKind {
// Search for a room member matching this user id
let roomMember = self.roomMember(withUserId: userId)
var user: MXUser?
if roomMember == nil {
// fallback on getting the user from the session's store
user = session.user(withUserId: userId)
}
let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl
let displayName = roomMember?.displayname ?? user?.displayName ?? userId
let isHighlighted = userId == session.myUserId
let avatar: PillTextAttachmentItem
if roomMember == nil && user == nil {
avatar = .asset(named: "pill_user",
parameters: .init(tintColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.secondaryContent),
rawRenderingMode: UIImage.RenderingMode.alwaysOriginal.rawValue,
padding: 0.0))
} else {
avatar = .avatar(url: avatarUrl,
string: displayName,
matrixId: userId)
}
let data = PillTextAttachmentData(pillType: .user(userId: userId),
items: [
avatar,
.text(displayName)
],
isHighlighted: isHighlighted,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
userFound = roomMember != nil || user != nil
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayName))
}
/// Create a pill representation for a given room
/// - Parameters:
/// - roomId: the room MXID or alias
/// - roomFound: this flag will be set to true if a room is found locally with this roomId
/// - Returns: a pill attachment
private func pillTextAttachment(forRoomId roomId: String, roomFound: inout Bool) -> PillAttachmentKind {
// Get the room matching this roomId
let room = roomId.starts(with: "#") ? session.room(withAlias: roomId) : session.room(withRoomId: roomId)
let displayName = room?.displayName ?? VectorL10n.pillRoomFallbackDisplayName
let avatar: PillTextAttachmentItem
if let room {
if session.spaceService.getSpace(withId: roomId) != nil {
avatar = .spaceAvatar(url: room.avatarData.mxContentUri,
string: displayName,
matrixId: roomId)
} else {
avatar = .avatar(url: room.avatarData.mxContentUri,
string: displayName,
matrixId: roomId)
}
} else {
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let data = PillTextAttachmentData(pillType: .room(roomId: roomId),
items: [
avatar,
.text(displayName)
],
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
roomFound = room != nil
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayName))
}
/// Create a pill representation for a message in a room
/// - Parameters:
/// - messageId: message eventId
/// - roomId: roomId of the message
/// - Returns: a pill attachment
private func pillTextAttachment(forMessageId messageId: String, inRoomId roomId: String) -> PillAttachmentKind {
// Check if this is the current room
if roomId == roomState.roomId {
return pillTextAttachment(inCurrentRoomForMessageId: messageId)
}
let room = session.room(withRoomId: roomId)
let avatar: PillTextAttachmentItem
if let room {
avatar = .avatar(url: room.avatarData.mxContentUri,
string: room.displayName,
matrixId: roomId)
} else {
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let displayText = room?.displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage
let data = PillTextAttachmentData(pillType: .message(roomId: roomId, eventId: messageId),
items: [
avatar,
.text(displayText)
],
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayText))
}
/// Create a pill representation for a message in the current room
/// - Parameters:
/// - messageId: message eventId
/// - Returns: a pill attachment
private func pillTextAttachment(inCurrentRoomForMessageId messageId: String) -> PillAttachmentKind {
var roomMember: MXRoomMember?
// If we have the event locally, try to get the room member
if let event = session.store.event(withEventId: messageId, inRoom: roomState.roomId) {
roomMember = self.roomMember(withUserId: event.sender)
}
let displayText: String
let avatar: PillTextAttachmentItem
if let roomMember {
displayText = VectorL10n.pillMessageFrom(roomMember.displayname ?? roomMember.userId)
avatar = .avatar(url: roomMember.avatarUrl,
string: roomMember.displayname,
matrixId: roomMember.userId)
} else {
displayText = VectorL10n.pillMessage
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId),
items: [
avatar,
.text(displayText)
].compactMap { $0 },
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayText))
}
}
+62 -4
View File
@@ -45,6 +45,11 @@ class PillTextAttachment: NSTextAttachment {
updateBounds()
}
convenience init?(attachmentData: PillTextAttachmentData) {
guard let encodedData = try? Self.serializationService.serialize(attachmentData) else { return nil }
self.init(data: encodedData, ofType: PillsFormatter.pillUTType)
}
/// Create a Mention Pill text attachment for given room member.
///
@@ -55,9 +60,13 @@ class PillTextAttachment: NSTextAttachment {
convenience init?(withRoomMember roomMember: MXRoomMember,
isHighlighted: Bool,
font: UIFont) {
let data = PillTextAttachmentData(matrixItemId: roomMember.userId,
displayName: roomMember.displayname,
avatarUrl: roomMember.avatarUrl,
let data = PillTextAttachmentData(pillType: .user(userId: roomMember.userId),
items: [
.avatar(url: roomMember.avatarUrl,
string: roomMember.displayname,
matrixId: roomMember.userId),
.text(roomMember.displayname)
],
isHighlighted: isHighlighted,
alpha: 1.0,
font: font)
@@ -71,14 +80,63 @@ class PillTextAttachment: NSTextAttachment {
updateBounds()
}
/// Computes size required to display a pill for given display text.
///
/// - Parameters:
/// - font: the text font
/// - Returns: required size for pill
func size(forFont font: UIFont) -> CGSize {
guard let data else {
MXLog.debug("[PillTextAttachment]: data are missing")
return .zero
}
let sizes = PillAttachmentViewProvider.pillAttachmentViewSizes
var width: CGFloat = 0
var textContent = ""
for item in data.items {
switch item {
case .text(let text):
textContent += text
case .avatar, .asset, .spaceAvatar:
width += sizes.avatarSideLength
}
}
// add texts
if !textContent.isEmpty {
let label = UILabel(frame: .zero)
label.font = font
label.text = textContent
width += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
height: sizes.pillBackgroundHeight)).width
}
// add spacing
width += CGFloat(max(0, data.items.count - 1)) * sizes.itemSpacing
// add margins
switch data.items.first {
case .asset, .avatar:
width += sizes.avatarLeading + sizes.horizontalMargin
default:
width += 2 * sizes.horizontalMargin
}
return CGSize(width: width,
height: sizes.pillHeight)
}
}
// MARK: - Private
@available (iOS 15.0, *)
private extension PillTextAttachment {
func updateBounds() {
guard let data = data else { return }
let pillSize = PillAttachmentViewProvider.size(forDisplayText: data.displayText, andFont: data.font)
let pillSize = size(forFont: data.font)
// Offset to align pill centerY with text centerY.
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
+87 -32
View File
@@ -17,16 +17,55 @@
import Foundation
import UIKit
@available (iOS 15.0, *)
struct PillAssetColor: Codable {
var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
var uiColor: UIColor {
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
init(uiColor: UIColor) {
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
}
}
@available (iOS 15.0, *)
struct PillAssetParameter: Codable {
var tintColor: PillAssetColor?
var backgroundColor: PillAssetColor?
var rawRenderingMode: Int = UIImage.RenderingMode.automatic.rawValue
var padding: CGFloat = 2.0
}
@available (iOS 15.0, *)
enum PillTextAttachmentItem: Codable {
case text(String)
case avatar(url: String?, string: String?, matrixId: String)
case spaceAvatar(url: String?, string: String?, matrixId: String)
case asset(named: String, parameters: PillAssetParameter)
}
@available (iOS 15.0, *)
extension PillTextAttachmentItem {
var string: String? {
switch self {
case .text(let text):
return text
default:
return nil
}
}
}
/// Data associated with a Pill text attachment.
@available (iOS 15.0, *)
struct PillTextAttachmentData: Codable {
// MARK: - Properties
/// Matrix item identifier (user id or room id)
var matrixItemId: String
/// Matrix item display name (user or room display name)
var displayName: String?
/// Matrix item avatar URL (user or room avatar url)
var avatarUrl: String?
/// Pill type
var pillType: PillType
/// Items to render
var items: [PillTextAttachmentItem]
/// Wether the pill should be highlighted
var isHighlighted: Bool
/// Alpha for pill display
@@ -36,43 +75,36 @@ struct PillTextAttachmentData: Codable {
/// Helper for preferred text to display.
var displayText: String {
guard let displayName = displayName,
displayName.count > 0 else {
return matrixItemId
}
return displayName
return items.map { $0.string }
.compactMap { $0 }
.joined(separator: " ")
}
// MARK: - Init
/// Init.
///
/// - Parameters:
/// - matrixItemId: Matrix item identifier (user id or room id)
/// - displayName: Matrix item display name (user or room display name)
/// - avatarUrl: Matrix item avatar URL (user or room avatar url)
/// - pillType: Type for the pill
/// - items: Items to display
/// - isHighlighted: Wether the pill should be highlighted
/// - alpha: Alpha for pill display
/// - font: Font for the display name
init(matrixItemId: String,
displayName: String?,
avatarUrl: String?,
init(pillType: PillType,
items: [PillTextAttachmentItem],
isHighlighted: Bool,
alpha: CGFloat,
font: UIFont) {
self.matrixItemId = matrixItemId
self.displayName = displayName
self.avatarUrl = avatarUrl
self.pillType = pillType
self.items = items
self.isHighlighted = isHighlighted
self.alpha = alpha
self.font = font
}
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case matrixItemId
case displayName
case avatarUrl
case pillType
case items
case isHighlighted
case alpha
case font
@@ -84,9 +116,8 @@ struct PillTextAttachmentData: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
matrixItemId = try container.decode(String.self, forKey: .matrixItemId)
displayName = try? container.decode(String.self, forKey: .displayName)
avatarUrl = try? container.decode(String.self, forKey: .avatarUrl)
pillType = try container.decode(PillType.self, forKey: .pillType)
items = try container.decode([PillTextAttachmentItem].self, forKey: .items)
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
alpha = try container.decode(CGFloat.self, forKey: .alpha)
let fontData = try container.decode(Data.self, forKey: .font)
@@ -99,12 +130,36 @@ struct PillTextAttachmentData: Codable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(matrixItemId, forKey: .matrixItemId)
try? container.encode(displayName, forKey: .displayName)
try? container.encode(avatarUrl, forKey: .avatarUrl)
try container.encode(pillType, forKey: .pillType)
try container.encode(items, forKey: .items)
try container.encode(isHighlighted, forKey: .isHighlighted)
try container.encode(alpha, forKey: .alpha)
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
try container.encode(fontData, forKey: .font)
}
// MARK: - Pill representations
var pillIdentifier: String {
switch pillType {
case .user(let userId):
return userId
case .room(let roomId):
return roomId
case .message(let roomId, let messageId):
return "\(roomId)/\(messageId)"
}
}
var markdown: String {
var permalink: String
switch pillType {
case .user(let userId):
permalink = MXTools.permalinkToUser(withUserId: userId)
case .room(let roomId):
permalink = MXTools.permalink(toRoom: roomId)
case .message(let roomId, let messageId):
permalink = MXTools.permalink(toEvent: messageId, inRoom: roomId)
}
return "[\(displayText)](\(permalink))"
}
}
+76
View File
@@ -0,0 +1,76 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@available (iOS 15.0, *)
enum PillType: Codable {
case user(userId: String) /// userId
case room(roomId: String) /// roomId
case message(roomId: String, eventId: String) // roomId, eventId
}
@available (iOS 15.0, *)
extension PillType {
private static var regexPermalinkTarget: NSRegularExpression? = {
let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl ?? kMXMatrixDotToUrl
let pattern = #"\#(clientBaseUrl)/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"#
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
}()
static func from(url: URL) -> PillType? {
guard let regex = regexPermalinkTarget else {
return nil
}
var link = url.absoluteString
// we need to remove percent encoding (it's possible that it has been encoded multiple times)
while let cleaned = link.removingPercentEncoding, cleaned != link {
link = cleaned
}
let pills = regex.matches(in: link, options: [], range: NSRange(link.startIndex..., in: link))
.map { result -> [String]? in
guard result.numberOfRanges > 1 else { return nil }
return (1..<result.numberOfRanges)
.map { Range(result.range(at: $0), in: link) }
.compactMap { $0 }
.map { String(link[$0]).removingPercentEncoding }
.compactMap { $0 }
}
.compactMap { matrixIds -> PillType? in
guard let matrixIds, !matrixIds.isEmpty else {
return nil
}
switch matrixIds[0].first {
case "@":
return .user(userId: matrixIds[0])
case "!", "#":
if matrixIds.count > 1 {
if matrixIds[1].starts(with: "$") {
return .message(roomId: matrixIds[0], eventId: matrixIds[1])
}
}
return .room(roomId: matrixIds[0])
default:
return nil
}
}
return pills.first
}
}
+56 -60
View File
@@ -32,7 +32,7 @@ class PillsFormatter: NSObject {
case identifier
case markdown
}
// MARK: - Internal Methods
/// Insert text attachments for pills inside given message attributed string.
///
@@ -52,17 +52,21 @@ class PillsFormatter: NSObject {
roomState: MXRoomState,
andLatestRoomState latestRoomState: MXRoomState?,
isEditMode: Bool = false) -> NSAttributedString {
let newAttr = NSMutableAttributedString(attributedString: attributedString)
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
if let userId = userIdFromPermalink(url.absoluteString),
let roomMember = roomMember(withUserId: userId,
roomState: roomState,
andLatestRoomState: latestRoomState) {
let isHighlighted = roomMember.userId == session.myUserId && event.sender != session.myUserId
let attachmentString = mentionPill(withRoomMember: roomMember,
andUrl: isEditMode ? nil : url,
isHighlighted: isHighlighted,
font: eventFormatter.defaultTextFont)
let provider = PillProvider(withSession: session,
eventFormatter: eventFormatter,
event: event,
roomState: roomState,
andLatestRoomState: latestRoomState,
isEditMode: isEditMode)
// try to get a mention pill from the url
let label = Range(range, in: newAttr.string).flatMap { String(newAttr.string[$0]) }
if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "", event: event) {
// replace the url with the pill
newAttr.replaceCharacters(in: range, with: attachmentString)
}
}
@@ -80,25 +84,27 @@ class PillsFormatter: NSObject {
mode: PillsReplacementTextMode = .displayname) -> String {
let newAttr = NSMutableAttributedString(attributedString: attributedString)
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
if let displayText = attachment.data?.displayText,
let userId = attachment.data?.matrixItemId,
let permalink = MXTools.permalinkToUser(withUserId: userId) {
let pillString: String
switch mode {
case .displayname:
pillString = displayText
case .identifier:
pillString = userId
case .markdown:
pillString = "[\(displayText)](\(permalink))"
}
newAttr.replaceCharacters(in: range, with: pillString)
guard let data = attachment.data else {
return
}
let pillString: String
switch mode {
case .displayname:
pillString = data.displayText
case .identifier:
pillString = data.pillIdentifier
case .markdown:
pillString = data.markdown
}
newAttr.replaceCharacters(in: range, with: pillString)
}
return newAttr.string
}
/// Creates an attributed string containing a pill for given room member.
///
/// - Parameters:
@@ -111,17 +117,13 @@ class PillsFormatter: NSObject {
andUrl url: URL? = nil,
isHighlighted: Bool,
font: UIFont) -> NSAttributedString {
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
return NSAttributedString(string: roomMember.displayname)
}
let string = NSMutableAttributedString(attachment: attachment)
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
if let url = url {
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
}
return string
return attributedStringWithAttachment(attachment, link: url, font: font)
}
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
///
/// - Parameters:
@@ -140,43 +142,37 @@ class PillsFormatter: NSObject {
/// - roomState: room state for refresh, should be the latest available
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
guard let userId = pill.data?.matrixItemId,
let roomMember = roomState.members.member(withUserId: userId) else {
return
}
switch pill.data?.pillType {
case .user(let userId):
guard let roomMember = roomState.members.member(withUserId: userId) else {
return
}
pill.data?.displayName = roomMember.displayname
pill.data?.avatarUrl = roomMember.avatarUrl
pill.data?.items = [
.avatar(url: roomMember.avatarUrl,
string: roomMember.displayname,
matrixId: roomMember.userId),
.text(roomMember.displayname)
]
default:
break
}
}
}
}
// MARK: - Private Methods
@available (iOS 15.0, *)
private extension PillsFormatter {
/// Extract user id from given permalink
/// - Parameter permalink: the permalink
/// - Returns: userId, if any
static func userIdFromPermalink(_ permalink: String) -> String? {
let baseUrl: String
if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl {
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
} else {
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
extension PillsFormatter {
static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
let string = NSMutableAttributedString(attachment: attachment)
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
if let url = link {
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
}
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
}
/// Retrieve the latest available `MXRoomMember` from given data.
///
/// - Parameters:
/// - userId: the id of the user
/// - roomState: room state for message
/// - latestRoomState: latest room state of the room containing this message
/// - Returns: the room member, if available
static func roomMember(withUserId userId: String,
roomState: MXRoomState,
andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? {
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
return string
}
}
@@ -57,7 +57,8 @@ final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
override func layoutSubviews() {
super.layoutSubviews()
self.avatarImageView.layer.cornerRadius = Constants.cornerRadius
// Ensure we keep a rounded corner if the width is less than 2 * Constants.cornerRadius
self.avatarImageView.layer.cornerRadius = max(2.0, min(self.avatarImageView.bounds.width / 4, Constants.cornerRadius))
}
// MARK: - Public
@@ -23,6 +23,7 @@ final class UserAvatarView: AvatarView {
private func commonInit() {
let avatarImageView = MXKImageView()
avatarImageView.frame = self.frame
self.vc_addSubViewMatchingParent(avatarImageView)
self.avatarImageView = avatarImageView
}
@@ -18,6 +18,7 @@
#import "RoomBubbleCellData.h"
#import "MXKRoomBubbleTableViewCell+Riot.h"
#import "UserEncryptionTrustLevel.h"
#import "RoomEncryptionTrustLevel.h"
#import "RoomReactionsViewSizer.h"
#import "RoomEncryptedDataBubbleCell.h"
#import "LegacyAppDelegate.h"
@@ -6,6 +6,8 @@
#import "AvatarGenerator.h"
#import "BuildInfo.h"
#import "ShareItemSender.h"
#import "UserEncryptionTrustLevel.h"
#import "RoomEncryptionTrustLevel.h"
// MatrixKit imports
#import "MatrixKit-Bridging-Header.h"
+1
View File
@@ -87,3 +87,4 @@ targets:
- "**/*.md" # excludes all files with the .md extension
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
- path: ../Riot/Modules/Encryption/EncryptionTrustLevel.swift
@@ -327,7 +327,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
MXLog.debug("[QRLoginService] Marking the received master key as trusted")
let mskVerificationResult = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
session.crypto.setUserVerification(true, forUser: session.myUserId) {
session.crypto.setUserVerificationForUserId(session.myUserId) {
MXLog.debug("[QRLoginService] Successfully marked the received master key as trusted")
continuation.resume(returning: true)
} failure: { error in
@@ -38,7 +38,7 @@ class MXNotificationSettingsService: NotificationSettingsServiceType {
let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules))
// Set initial value of the content rules
if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] {
if let contentRules = session.notificationCenter.rules?.global.content as? [MXPushRule] {
self.contentRules = contentRules
}
@@ -32,7 +32,7 @@ class CryptoSDKFeatureTests: XCTestCase {
override func setUp() {
RiotSettings.shared.enableCryptoSDK = false
remote = RemoteFeatureClient()
feature = CryptoSDKFeature(remoteFeature: remote)
feature = CryptoSDKFeature(remoteFeature: remote, localTargetPercentage: 0)
}
override func tearDown() {
@@ -423,7 +423,7 @@
}
}];
XCTAssertEqual(hasLink, false, @"There should be no link in this case. We let the UI manage the link");
XCTAssertEqual(hasLink, true, @"There should be a link, so that a Pill can be rendered for this permalink.");
}
#pragma mark - Event sender/target info
@@ -0,0 +1,171 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import XCTest
@testable import Element
@testable import MatrixSDK
class EncryptionTrustLevelTests: XCTestCase {
var encryption: EncryptionTrustLevel!
override func setUp() {
encryption = EncryptionTrustLevel()
}
// MARK: - Helpers
func makeCrossSigning(isVerified: Bool) -> MXCrossSigningInfo {
return .init(
userIdentity: .init(
identity: .other(
userId: "Bob",
masterKey: "MSK",
selfSigningKey: "SSK"
),
isVerified: isVerified
)
)
}
func makeSummary(trusted: Int, total: Int) -> MXTrustSummary {
MXTrustSummary(trustedCount: trusted, totalCount: total)
}
// MARK: - Users
func test_userTrustLevel_whenCrossSigningDisabled() {
let devicesToTrustLevel: [(MXTrustSummary, UserEncryptionTrustLevel)] = [
(makeSummary(trusted: 0, total: 0), .notVerified),
(makeSummary(trusted: 0, total: 2), .notVerified),
(makeSummary(trusted: 1, total: 2), .warning),
(makeSummary(trusted: 3, total: 4), .warning),
(makeSummary(trusted: 5, total: 5), .trusted)
]
for (devices, expected) in devicesToTrustLevel {
let trustLevel = encryption.userTrustLevel(
crossSigning: nil,
devicesTrust: devices
)
XCTAssertEqual(trustLevel, expected, "\(devices.trustedCount) trusted device(s) out of \(devices.totalCount)")
}
}
func test_userTrustLevel_whenCrossSigningNotVerified() {
let devicesToTrustLevel: [(MXTrustSummary, UserEncryptionTrustLevel)] = [
(makeSummary(trusted: 0, total: 0), .notVerified),
(makeSummary(trusted: 0, total: 2), .notVerified),
(makeSummary(trusted: 1, total: 2), .notVerified),
(makeSummary(trusted: 3, total: 4), .notVerified),
(makeSummary(trusted: 5, total: 5), .notVerified)
]
for (devices, expected) in devicesToTrustLevel {
let trustLevel = encryption.userTrustLevel(
crossSigning: makeCrossSigning(isVerified: false),
devicesTrust: devices
)
XCTAssertEqual(trustLevel, expected, "\(devices.trustedCount) trusted device(s) out of \(devices.totalCount)")
}
}
func test_userTrustLevel_whenCrossSigningVerified() {
let devicesToTrustLevel: [(MXTrustSummary, UserEncryptionTrustLevel)] = [
(makeSummary(trusted: 0, total: 0), .trusted),
(makeSummary(trusted: 0, total: 2), .warning),
(makeSummary(trusted: 1, total: 2), .warning),
(makeSummary(trusted: 3, total: 4), .warning),
(makeSummary(trusted: 5, total: 5), .trusted)
]
for (devices, expected) in devicesToTrustLevel {
let trustLevel = encryption.userTrustLevel(
crossSigning: makeCrossSigning(isVerified: true),
devicesTrust: devices
)
XCTAssertEqual(trustLevel, expected, "\(devices.trustedCount) trusted device(s) out of \(devices.totalCount)")
}
}
// MARK: - Rooms
func test_roomTrustLevel() {
let usersDevicesToTrustLevel: [(MXTrustSummary, MXTrustSummary, RoomEncryptionTrustLevel)] = [
// No users verified
(makeSummary(trusted: 0, total: 0), makeSummary(trusted: 0, total: 0), .normal),
// Only some users verified
(makeSummary(trusted: 0, total: 1), makeSummary(trusted: 0, total: 1), .normal),
(makeSummary(trusted: 3, total: 4), makeSummary(trusted: 5, total: 5), .normal),
(makeSummary(trusted: 3, total: 4), makeSummary(trusted: 5, total: 5), .normal),
// All users verified
(makeSummary(trusted: 2, total: 2), makeSummary(trusted: 0, total: 0), .trusted),
(makeSummary(trusted: 3, total: 3), makeSummary(trusted: 0, total: 1), .warning),
(makeSummary(trusted: 3, total: 3), makeSummary(trusted: 3, total: 4), .warning),
(makeSummary(trusted: 4, total: 4), makeSummary(trusted: 5, total: 5), .trusted),
]
for (users, devices, expected) in usersDevicesToTrustLevel {
let trustLevel = encryption.roomTrustLevel(
summary: MXUsersTrustLevelSummary(
usersTrust: users,
devicesTrust: devices
)
)
XCTAssertEqual(trustLevel, expected, "\(users.trustedCount)/\(users.totalCount) trusted users(s), \(devices.trustedCount)/\(devices.totalCount) trusted device(s)")
}
}
}
extension UserEncryptionTrustLevel: CustomStringConvertible {
public var description: String {
switch self {
case .trusted:
return "trusted"
case .warning:
return "warning"
case .notVerified:
return "notVerified"
case .noCrossSigning:
return "noCrossSigning"
case .none:
return "none"
case .unknown:
return "unknown"
@unknown default:
return "unknown"
}
}
}
extension RoomEncryptionTrustLevel: CustomStringConvertible {
public var description: String {
switch self {
case .trusted:
return "trusted"
case .warning:
return "warning"
case .normal:
return "normal"
case .unknown:
return "unknown"
@unknown default:
return "unknown"
}
}
}
+109
View File
@@ -0,0 +1,109 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import Element
@available (iOS 15.0, *)
final class PillTypeTests: XCTestCase {
func testUserPill() throws {
let urls = [
"https://matrix.to/#/@bob:matrix.org",
"https://matrix.to/#/user/@bob:matrix.org"
]
for url in urls {
switch PillType.from(url: URL(string: url)!) {
case .user(let userId):
XCTAssertEqual(userId, "@bob:matrix.org")
default:
XCTFail("Should be a .user pill")
}
}
}
func testRoomPill() throws {
let urls = [
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost",
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost?via=localhost",
"https://matrix.to/#/room/!JppIaYcVkyCiSBVzBn:localhost"
]
for url in urls {
switch PillType.from(url: URL(string: url)!) {
case .room(let roomId):
XCTAssertEqual(roomId, "!JppIaYcVkyCiSBVzBn:localhost")
default:
XCTFail("Should be a .room pill")
}
}
}
func testRoomAlias() throws {
let urls = [
"https://matrix.to/#/%23room-alias:localhost",
"https://matrix.to/#/room/%23room-alias:localhost"
]
for url in urls {
switch PillType.from(url: URL(string: url)!) {
case .room(let roomId):
XCTAssertEqual(roomId, "#room-alias:localhost")
default:
XCTFail("Should be a .room pill")
}
}
}
func testMessagePill() throws {
let urls = [
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc",
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc?via=localhost"
]
for url in urls {
switch PillType.from(url: URL(string: url)!) {
case .message(let roomId, let eventId):
XCTAssertEqual(roomId, "!JppIaYcVkyCiSBVzBn:localhost")
XCTAssertEqual(eventId, "$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc")
default:
XCTFail("Should be a .message pill")
}
}
}
func testMessagePillWithRoomAlias() throws {
let urls = [
"https://matrix.to/#/%23room-alias:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc?via=localhost"
]
for url in urls {
switch PillType.from(url: URL(string: url)!) {
case .message(let roomId, let eventId):
XCTAssertEqual(roomId, "#room-alias:localhost")
XCTAssertEqual(eventId, "$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc")
default:
XCTFail("Should be a .message pill")
}
}
}
func testNotAPermalink() throws {
XCTAssertNil(PillType.from(url: URL(string: "matrix.org")!))
}
}
+636 -14
View File
@@ -27,10 +27,42 @@ private enum Inputs {
static let aliceNewAvatarUrl = "mxc://matrix.org/VyNYAgaFdlLojoOeZETtQ"
static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId)
static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org")
static let bobMember = FakeMXRoomMember(displayname: "Bob", avatarUrl: "", userId: "@bob:matrix.org")
static let alicePermalink = "https://matrix.to/#/@alice:matrix.org"
static let mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: alicePermalink)!])
static let markdownLinkToAlice = "[Alice](\(alicePermalink))"
static let bobUserId = "@bob:matrix.org"
static let bobDisplayname = "Bob"
static let bobAvatarUrl = "mxc://matrix.org/VyNYBgahazAzUuOeZETtQ"
static let bobMember = FakeMXRoomMember(displayname: bobDisplayname, avatarUrl: bobAvatarUrl, userId: bobUserId)
static let anotherUserId = "@another.user:matrix.org"
static let anotherUserPermalink = "https://matrix.to/#/@another.user:matrix.org"
static let markdownLinkToAnotherUser = "[Another user](\(alicePermalink))"
static let mentionToAnotherUser = NSAttributedString(string: anotherUserPermalink, attributes: [.link: URL(string: anotherUserPermalink)!])
static let mentionToAnotherUserWithLabel = NSAttributedString(string: "Link text", attributes: [.link: URL(string: anotherUserPermalink)!])
static let roomId = "!vWieJcXcUdMwavNSvy:matrix.org"
static let roomAlias = "#fake_room_alias:matrix.org"
static let roomDisplayName = "Sample Room"
static let roomPermalink = "https://matrix.to/#/\(roomId)"
static let roomAliasPermalink = "https://matrix.to/%23/\(roomAlias)"
static let roomAvatarUrl = "mxc://matrix.org/VzNZAgahaiAzUoOeZETtQ"
static let mentionToRoom = NSAttributedString(string: roomPermalink, attributes: [.link: URL(string: roomPermalink)!])
static let mentionToRoomWithLabel = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomPermalink)!])
static let mentionToRoomAlias = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomAliasPermalink)!])
static let anotherRoomId = "!zWieBcUcUdMwavNSvy:matrix.org"
static let anotherRoomDisplayName = "Room/Space"
static let anotherRoomAvatarUrl = "mxc://matrix.org/VzNZBgajauAzUoOeZETtQ"
static let messageEventId = "$JrEsoQO77MCdAubG6z-5oXlOBy1I5QL9FTut_Giztoc"
static let messagePermalink = "https://matrix.to/#/\(roomId)/\(messageEventId)?via=matrix.org"
static let messageAnotherRoomPermalink = "https://matrix.to/#/\(anotherRoomId)/\(messageEventId)?via=matrix.org"
static let pillAnotherUserWithLinkText = "Link text"
static let pillMessageAnotherRoomText = "Message in Sample Room"
static let pillMessageFromBobText = "Message from Bob"
}
// MARK: - Tests
@@ -47,11 +79,24 @@ class PillsFormatterTests: XCTestCase {
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains Alice's displayname and avatar url.
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceDisplayname)
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceAvatarUrl)
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname)
switch pillTextAttachmentData.pillType {
case .user(let userId):
XCTAssertEqual(userId, Inputs.aliceUserId)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.aliceAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .user")
}
// Pill has expected size.
let expectedSize = PillAttachmentViewProvider.size(forDisplayText: pillTextAttachment!.data!.displayText,
andFont: pillTextAttachment!.data!.font)
let expectedSize = pillTextAttachment?.size(forFont: pillTextAttachment!.data!.font)
XCTAssertEqual(pillTextAttachment?.bounds.size, expectedSize)
PillsFormatter.refreshPills(in: messageWithPills,
@@ -60,11 +105,23 @@ class PillsFormatterTests: XCTestCase {
// Alice's pill is still highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == true)
// Pill data is refreshed with correct data.
XCTAssertEqual(refreshedPillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
XCTAssertEqual(refreshedPillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
let updatedPillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(updatedPillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname)
switch updatedPillTextAttachmentData.pillType {
case .user(let userId):
XCTAssertEqual(userId, Inputs.aliceUserId)
switch updatedPillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.aliceNewAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .user")
}
// Pill size is updated
let newExpectedSize = PillAttachmentViewProvider.size(forDisplayText: refreshedPillTextAttachment!.data!.displayText,
andFont: refreshedPillTextAttachment!.data!.font)
let newExpectedSize = pillTextAttachment?.size(forFont: refreshedPillTextAttachment!.data!.font)
XCTAssertEqual(refreshedPillTextAttachment?.bounds.size, newExpectedSize)
}
@@ -72,8 +129,21 @@ class PillsFormatterTests: XCTestCase {
let messageWithPills = createMessageWithMentionFromBobToAliceWithLatestRoomState()
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill uses the latest room state data.
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname)
switch pillTextAttachmentData.pillType {
case .user(let userId):
XCTAssertEqual(userId, Inputs.aliceUserId)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.aliceNewAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .message")
}
}
func testPillsToMarkdown() {
@@ -89,6 +159,292 @@ class PillsFormatterTests: XCTestCase {
XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname)
XCTAssertEqual(messageWithUserId, Inputs.messageStart + Inputs.aliceUserId)
}
// Test case: a mention to an unknown user (not a room member)
func testPillMentionningRoomMember() {
let messageWithPills = createMessageWithMentionFromBobToAlice()
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill uses the latest room state data.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname)
switch pillTextAttachmentData.pillType {
case .user(let userId):
XCTAssertEqual(userId, Inputs.aliceUserId)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.aliceAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .user")
}
}
// Test case: a mention to an unknown user (not a room member)
func testPillMentionningUnknownUser() {
let messageWithPills = createMessageWithMentionFromBobToAnotherUser()
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill uses the latest room state data.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.anotherUserId)
switch pillTextAttachmentData.pillType {
case .user(let userId):
XCTAssertEqual(userId, Inputs.anotherUserId)
switch pillTextAttachmentData.items.first {
case .asset(let name, _):
XCTAssertEqual(name, "pill_user")
default:
XCTFail("First pill item should be the asset")
}
default:
XCTFail("Pill should be of type .user")
}
}
// Test case: a mention to an unknown user (not a room member) with a formatted text (HTML or MARKDOWN)
// In this case, we don't want to pillify the link
func testPillMentionningUnknownUserWithFormattedText() {
let messageWithPills = createMessageWithMentionFromBobToAnotherUser(withLinkText: true)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
XCTAssertNil(pillTextAttachment)
}
// Test case: a mention to a room
func testPillMentionningRoom() {
let messageWithPills = createMessageWithMentionToRoom()
XCTAssertEqual(messageWithPills.length, Inputs.messageStart.count + 1) // +1 non-unicode character for the pill/textAttachment
XCTAssert(messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) is PillTextAttachment)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
switch pillTextAttachmentData.pillType {
case .room(let userId):
XCTAssertEqual(userId, Inputs.roomId)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.roomAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .room")
}
}
// Test case: a mention to a space
func testPillMentionningSpace() {
let messageWithPills = createMessageWithMentionToRoom(isSpace: true)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
switch pillTextAttachmentData.pillType {
case .room(let userId):
XCTAssertEqual(userId, Inputs.roomId)
switch pillTextAttachmentData.items.first {
case .spaceAvatar(let url, _, _):
XCTAssertEqual(url, Inputs.roomAvatarUrl)
default:
XCTFail("First pill item should be the spaceAvatar")
}
default:
XCTFail("Pill should be of type .room")
}
}
// Test case: a mention to a room alias
func testPillMentionningRoomByAlias() {
let messageWithPills = createMessageWithMentionToRoom(usingAlias: true)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
switch pillTextAttachmentData.pillType {
case .room(let userId):
XCTAssertEqual(userId, Inputs.roomAlias)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.roomAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .room")
}
}
// Test case: a mention to an unknown room
func testPillMentionningUnknownRoom() {
let messageWithPills = createMessageWithMentionToRoom(knownRoom: false)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillRoomFallbackDisplayName)
switch pillTextAttachmentData.pillType {
case .room(let userId):
XCTAssertEqual(userId, Inputs.roomId)
switch pillTextAttachmentData.items.first {
case .asset(let assetName, let parameters):
XCTAssertEqual(assetName, "link_icon")
default:
XCTFail("First pill item should be the asset")
}
default:
XCTFail("Pill should be of type .room")
}
}
// Test case: a mention to an unknown room using a formatted text (HTML or MARKDOWN)
func testPillMentionningUnknownRoomWithFormattedText() {
let messageWithPills = createMessageWithMentionToRoom(knownRoom: false, withLinkText: "Link label")
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
XCTAssertNil(pillTextAttachment)
}
// Test case: a mention to a message using a formatted text (HTML or MARKDOWN)
func testPillMentionningMessageWithLabel() {
let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: "Link label")
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
XCTAssertNil(pillTextAttachment)
}
// Test case: a mention to a message sent by a room member in the current room
func testPillMentionningMessageInCurrentRoomFromRoomMember() {
// Test: a mention to current room message, sent by a room member (Bob)
let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: Inputs.messagePermalink)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.pillMessageFromBobText)
switch pillTextAttachmentData.pillType {
case .message(let roomId, let messageId):
XCTAssertEqual(roomId, Inputs.roomId)
XCTAssertEqual(messageId, Inputs.messageEventId)
let firstItem = pillTextAttachmentData.items[0]
switch firstItem {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.bobAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .message")
}
}
// Test case: a mention to a message sent in the current room from an unknown user
func testPillMentionningMessageInCurrentRoomFromUnknownUser() {
let messageWithPills = createMessageWithMentionToMessage(sentBy: Inputs.anotherUserId, withLabel: Inputs.messagePermalink)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage)
switch pillTextAttachmentData.pillType {
case .message(let roomId, let messageId):
XCTAssertEqual(roomId, Inputs.roomId)
XCTAssertEqual(messageId, Inputs.messageEventId)
let firstItem = pillTextAttachmentData.items[0]
switch firstItem {
case .asset(let name, _):
XCTAssertEqual(name, "link_icon")
default:
XCTFail("First pill item should be the asset")
}
default:
XCTFail("Pill should be of type .message")
}
}
// Test case: a mention to a message in another room
func testPillMentionningMessageInAnotherRoom() {
let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: true, withLabel: Inputs.messageAnotherRoomPermalink)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessageIn(Inputs.anotherRoomDisplayName))
switch pillTextAttachmentData.pillType {
case .message(let roomId, let messageId):
XCTAssertEqual(roomId, Inputs.anotherRoomId)
XCTAssertEqual(messageId, Inputs.messageEventId)
switch pillTextAttachmentData.items.first {
case .avatar(let url, _, _):
XCTAssertEqual(url, Inputs.anotherRoomAvatarUrl)
default:
XCTFail("First pill item should be the avatar")
}
default:
XCTFail("Pill should be of type .message")
}
}
// Test case: a mention to a message in an unknown room
func testPillMentionningMessageInUnknownRoom() {
let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: false, withLabel: Inputs.messageAnotherRoomPermalink)
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
// Pill is not highlighted.
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
// Attachment has correct type.
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
// Pill data contains the correct displayname and avatar url.
XCTAssertNotNil(pillTextAttachment?.data)
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage)
switch pillTextAttachmentData.pillType {
case .message(let roomId, let messageId):
XCTAssertEqual(roomId, Inputs.anotherRoomId)
XCTAssertEqual(messageId, Inputs.messageEventId)
switch pillTextAttachmentData.items.first {
case .asset(let name, let parameters):
XCTAssertEqual(name, "link_icon")
default:
XCTFail("First pill item should be the asset")
}
default:
XCTFail("Pill should be of type .message")
}
}
}
@available(iOS 15.0, *)
@@ -105,6 +461,24 @@ private extension PillsFormatterTests {
andLatestRoomState: nil)
return messageWithPills
}
func createMessageWithMentionFromBobToAnotherUser(withLinkText: Bool = false) -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
if withLinkText {
formattedMessage.append(Inputs.mentionToAnotherUserWithLabel)
} else {
formattedMessage.append(Inputs.mentionToAnotherUser)
}
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
withSession: session,
eventFormatter: EventFormatter(matrixSession: session),
event: FakeMXEvent(sender: Inputs.anotherUserId),
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()),
andLatestRoomState: nil)
return messageWithPills
}
func createMessageWithMentionFromBobToAliceWithLatestRoomState() -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
@@ -118,35 +492,269 @@ private extension PillsFormatterTests {
andLatestRoomState: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers()))
return messageWithPills
}
func createMessageWithMentionToRoom(isSpace: Bool = false, knownRoom: Bool = true, usingAlias: Bool = false, withLinkText: String? = nil) -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
let mention: NSAttributedString
if usingAlias {
mention = NSAttributedString(string: withLinkText ?? Inputs.roomAliasPermalink , attributes: [.link: URL(string: Inputs.roomAliasPermalink)!])
} else {
mention = NSAttributedString(string: withLinkText ?? Inputs.roomPermalink , attributes: [.link: URL(string: Inputs.roomPermalink)!])
}
formattedMessage.append(mention)
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.bobMember.userId)
session.store = FakeMXStore(withEvents: [event])
if knownRoom {
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
displayName: Inputs.roomDisplayName,
alias: Inputs.roomAlias,
avatar: Inputs.roomAvatarUrl,
matrixSession: session)
if isSpace {
roomSummary.roomType = .space
}
session.addFakeRoom(room)
session.addFakeRoomSummary(roomSummary)
}
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
withSession: session,
eventFormatter: EventFormatter(matrixSession: session),
event: event,
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()),
andLatestRoomState: nil)
return messageWithPills
}
func createMessageWithMentionToMessage(from sender: MXRoomMember, withLabel string: String) -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!]))
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: sender.userId)
session.store = FakeMXStore(withEvents: [event])
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
displayName: Inputs.roomDisplayName,
alias: Inputs.roomAlias,
avatar: Inputs.roomAvatarUrl,
matrixSession: session)
session.addFakeRoom(room)
session.addFakeRoomSummary(roomSummary)
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
withSession: session,
eventFormatter: EventFormatter(matrixSession: session),
event: event,
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
andLatestRoomState: nil)
return messageWithPills
}
func createMessageWithMentionToMessage(sentBy senderId: String, withLabel string: String) -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!]))
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: senderId)
session.store = FakeMXStore(withEvents: [event])
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
displayName: Inputs.roomDisplayName,
alias: Inputs.roomAlias,
avatar: Inputs.roomAvatarUrl,
matrixSession: session)
session.addFakeRoom(room)
session.addFakeRoomSummary(roomSummary)
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
withSession: session,
eventFormatter: EventFormatter(matrixSession: session),
event: event,
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
andLatestRoomState: nil)
return messageWithPills
}
func createMessageWithMentionToAnotherRoomMessage(knownRoom: Bool, withLabel string: String) -> NSAttributedString {
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messageAnotherRoomPermalink)!]))
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.anotherUserId)
session.store = FakeMXStore(withEvents: [event])
if knownRoom {
let room = FakeMXRoom(roomId: Inputs.anotherRoomId, matrixSession: session, andStore: nil)!
let roomSummary = FakeMXRoomSummary(roomId: Inputs.anotherRoomId,
displayName: Inputs.anotherRoomDisplayName,
alias: nil,
avatar: Inputs.anotherRoomAvatarUrl,
matrixSession: session)
session.addFakeRoom(room)
session.addFakeRoomSummary(roomSummary)
}
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
withSession: session,
eventFormatter: EventFormatter(matrixSession: session),
event: event,
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
andLatestRoomState: nil)
return messageWithPills
}
}
// MARK: - Mock objects
private class FakeMXSession: MXSession {
private var mockMyUserId: String
private var mockRooms: [FakeMXRoom] = []
private var mockRoomSummaries: [String: FakeMXRoomSummary] = [:]
private var mockStore: FakeMXStore?
init(myUserId: String) {
mockMyUserId = myUserId
super.init()
let credentials = MXCredentials(homeServer: "mock_home_server",
userId: "mock_user_id",
accessToken: "mock_access_token")
let client = MXRestClient(credentials: credentials)
super.init(matrixRestClient: client)
}
override var myUserId: String! {
return mockMyUserId
}
func addFakeRoom(_ room: FakeMXRoom) {
mockRooms.append(room)
}
override func room(withRoomId roomId: String!) -> MXRoom! {
return mockRooms.first(where: { $0.roomId == roomId })
}
override func room(withAlias roomAlias: String) -> MXRoom? {
for (roomId, summary) in mockRoomSummaries {
if summary.aliases.contains(roomAlias) {
return room(withRoomId: roomId)
}
}
return nil
}
override func roomSummary(withRoomId roomId: String!) -> MXRoomSummary? {
return mockRoomSummaries[roomId]
}
func addFakeRoomSummary(_ roomSummary: FakeMXRoomSummary) {
self.mockRoomSummaries[roomSummary.roomId] = roomSummary
}
override var store: MXStore! {
get { return mockStore }
set { mockStore = newValue as? FakeMXStore }
}
}
private class FakeMXStore: MXMemoryStore {
private var mockEvents: [MXEvent]
init(withEvents events: [MXEvent]) {
self.mockEvents = events
super.init()
}
override func event(withEventId eventId: String, inRoom roomId: String) -> MXEvent? {
return mockEvents.first(where: { $0.eventId == eventId })
}
}
private class FakeMXRoom: MXRoom {
private var mockDisplayName: String? = nil
override init() {
super.init()
}
override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) {
super.init(roomId: roomId, matrixSession: mxSession, andStore: store)
}
override var summary: MXRoomSummary! {
return mxSession?.roomSummary(withRoomId: self.roomId)
}
}
private class FakeMXRoomSummary: MXRoomSummary {
private var mockDisplayName: String?
private var mockAliases: [String]?
private var mockAvatar: String? = nil
override init() {
super.init()
}
init(roomId: String, displayName: String, alias: String?, avatar: String?, matrixSession mxSession: MXSession) {
super.init(roomId: roomId, andMatrixSession: mxSession)
self.mockDisplayName = displayName
self.mockAliases = alias.flatMap { [$0] } ?? []
self.mockAvatar = avatar
}
override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) {
super.init(roomId: roomId, matrixSession: mxSession, andStore: store)
}
override init!(roomId: String!, andMatrixSession mxSession: MXSession!) {
super.init(roomId: roomId, andMatrixSession: mxSession)
}
required init?(coder: NSCoder) {
fatalError()
}
override var displayName: String! {
get { return mockDisplayName }
set { mockDisplayName = newValue }
}
override var avatar: String! {
get { return mockAvatar }
set { mockAvatar = newValue }
}
override var aliases: [String]! {
get { return mockAliases }
set { mockAliases = newValue }
}
}
private class FakeMXRoomState: MXRoomState {
private let mockRoomMembers: MXRoomMembers
private let mockRoomId: String?
init(roomMembers: MXRoomMembers) {
mockRoomMembers = roomMembers
mockRoomId = nil
super.init()
}
init(roomMembers: MXRoomMembers, roomId: String) {
mockRoomMembers = roomMembers
mockRoomId = roomId
super.init()
}
override var members: MXRoomMembers! {
return mockRoomMembers
}
override var roomId: String! {
return mockRoomId
}
}
private class FakeMXUpdatedRoomMembers: MXRoomMembers {
@@ -202,12 +810,21 @@ private class FakeMXRoomMember: MXRoomMember {
private class FakeMXEvent: MXEvent {
private var mockSender: String
private var mockEventId: String?
init(sender: String) {
mockSender = sender
mockEventId = nil
super.init()
}
init(eventId: String, sender: String) {
mockEventId = eventId
mockSender = sender
super.init()
}
required init?(coder: NSCoder) {
fatalError()
@@ -217,4 +834,9 @@ private class FakeMXEvent: MXEvent {
get { return mockSender }
set { mockSender = newValue }
}
override var eventId: String! {
get { return mockEventId }
set { mockEventId = newValue }
}
}