diff --git a/changelog.d/5885.bugfix b/changelog.d/5885.bugfix new file mode 100644 index 0000000000..a555ad0e98 --- /dev/null +++ b/changelog.d/5885.bugfix @@ -0,0 +1 @@ +Don't pause timer when call is held. diff --git a/changelog.d/5907.sdk b/changelog.d/5907.sdk new file mode 100644 index 0000000000..623cc2a174 --- /dev/null +++ b/changelog.d/5907.sdk @@ -0,0 +1 @@ +Replace usage of `System.currentTimeMillis()` by a `Clock` interface diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt new file mode 100644 index 0000000000..46a75b77a7 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Posun přehrávání v hlasových zprávách. Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/es-ES/changelogs/40103030.txt b/fastlane/metadata/android/es-ES/changelogs/40103030.txt new file mode 100644 index 0000000000..36f59a2308 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Cambios principales en esta versión: Hacer la política de servidores de indentidad visible en los ajusted. Temporalmente quitar soporte para Android Auto. +Registro de cambios: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/es-ES/changelogs/40104110.txt b/fastlane/metadata/android/es-ES/changelogs/40104110.txt new file mode 100644 index 0000000000..281df19388 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Cambios principales en esta versión: Varias correciones de bugs y mejoras en la estabilidad +Registro de cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index fdba15e90e..0e484158d9 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -37,3 +37,6 @@ Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido Continúa donde lo dejaste Manténgase en contacto donde quiera que esté con el historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io + +Código abierto +Element Android es un proyecto de código abierto, alojado en GitHub. Porfavor, reporta bugs y problemas en esta dirección: https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/et/changelogs/40104100.txt b/fastlane/metadata/android/et/changelogs/40104100.txt new file mode 100644 index 0000000000..b164935ba4 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: kerimine häälsõnumites ning erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104110.txt b/fastlane/metadata/android/et/changelogs/40104110.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104100.txt b/fastlane/metadata/android/fa/changelogs/40104100.txt new file mode 100644 index 0000000000..e7cdca6641 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104100.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: لغزش در پیام‌های صوتی. رفع اشکال‌های مختلف و بهبودهای امنیتی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104110.txt b/fastlane/metadata/android/fa/changelogs/40104110.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104110.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104100.txt b/fastlane/metadata/android/id/changelogs/40104100.txt new file mode 100644 index 0000000000..3cda40aecc --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Gulir di pesan suara. Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104110.txt b/fastlane/metadata/android/id/changelogs/40104110.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104100.txt b/fastlane/metadata/android/it-IT/changelogs/40104100.txt new file mode 100644 index 0000000000..6376e6c45a --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: scorrimento nei vocali. Varie correzioni e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104110.txt b/fastlane/metadata/android/it-IT/changelogs/40104110.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40100100.txt b/fastlane/metadata/android/lo/changelogs/40100100.txt new file mode 100644 index 0000000000..fe0ee08327 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100100.txt @@ -0,0 +1,2 @@ +ສະບັບໃຫມ່ນີ້ສ່ວນໃຫຍ່ແມ່ນມີການແກ້ໄຂແລະການປັບປຸງ bug. ຕອນນີ້ການສົ່ງຂໍ້ຄວາມແມ່ນໄວຂຶ້ນຫຼາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/lo/changelogs/40100110.txt b/fastlane/metadata/android/lo/changelogs/40100110.txt new file mode 100644 index 0000000000..69139783ad --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100110.txt @@ -0,0 +1,2 @@ +ຮຸ່ນໃຫມ່ນີ້ສ່ວນໃຫຍ່ແມ່ນປະກອບດ້ວຍໜ້າຕາແອັບແລະການປັບປຸງປະສົບການຂອງຜູ້ໃຊ້. ດຽວນີ້ທ່ານສາມາດເຊີນໝູ່, ແລະສ້າງການສົນທະນາກົງໄດ້ໄວຂຶ້ນໂດຍການສະແກນລະຫັດ QR. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/lo/changelogs/40100120.txt b/fastlane/metadata/android/lo/changelogs/40100120.txt new file mode 100644 index 0000000000..256cd1a437 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວຢ່າງ URL, ແປ້ນພິມ Emoji ໃໝ່, ຄວາມສາມາດໃນການຕັ້ງຄ່າຫ້ອງໃໝ່, ແລະຫິມະສຳລັບວັນຄຣິດສະມາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/lo/changelogs/40100130.txt b/fastlane/metadata/android/lo/changelogs/40100130.txt new file mode 100644 index 0000000000..c4878ea5f6 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວຢ່າງ URL, ແປ້ນພິມ Emoji ໃໝ່, ຄວາມສາມາດໃນການຕັ້ງຄ່າຫ້ອງໃໝ່, ແລະຫິມະສຳລັບວັນຄຣິດສະມາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/lo/changelogs/40100140.txt b/fastlane/metadata/android/lo/changelogs/40100140.txt new file mode 100644 index 0000000000..591b9b94b3 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂການອະນຸຍາດຫ້ອງ, ຮູບແບບ ແຈ້ງ / ມືດອັດຕະໂນມັດ, ແລະການແກ້ໄຂບັນຫາຈຳນວນໜຶ່ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/lo/changelogs/40100150.txt b/fastlane/metadata/android/lo/changelogs/40100150.txt new file mode 100644 index 0000000000..98577742c7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮັບຮອງການເຂົ້າລະບົບດ້ວຍສື່ສັງຄົມອອນລາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/lo/changelogs/40100160.txt b/fastlane/metadata/android/lo/changelogs/40100160.txt new file mode 100644 index 0000000000..c369fc88d3 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮັບຮອງການເຂົ້າລະບົບດ້ວຍສື່ສັງຄົມອອນລາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/lo/changelogs/40100170.txt b/fastlane/metadata/android/lo/changelogs/40100170.txt new file mode 100644 index 0000000000..776e1b6082 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100170.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/lo/changelogs/40101000.txt b/fastlane/metadata/android/lo/changelogs/40101000.txt new file mode 100644 index 0000000000..f7d621404a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບຂອງ VoIP (ໂທສຽງ ແລະໂທວີດີໂອໃນການສົນທະນາກົງ) ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/lo/changelogs/40101010.txt b/fastlane/metadata/android/lo/changelogs/40101010.txt new file mode 100644 index 0000000000..e2e1778b7a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/lo/changelogs/40101020.txt b/fastlane/metadata/android/lo/changelogs/40101020.txt new file mode 100644 index 0000000000..581db95e90 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/lo/changelogs/40101030.txt b/fastlane/metadata/android/lo/changelogs/40101030.txt new file mode 100644 index 0000000000..46d861995f --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101030.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/lo/changelogs/40101040.txt b/fastlane/metadata/android/lo/changelogs/40101040.txt new file mode 100644 index 0000000000..8ea7cac4f4 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/lo/changelogs/40101050.txt b/fastlane/metadata/android/lo/changelogs/40101050.txt new file mode 100644 index 0000000000..2823c04b06 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101050.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ບັນຫາດ່ວນສໍາລັບ 1.1.4 +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/lo/changelogs/40101060.txt b/fastlane/metadata/android/lo/changelogs/40101060.txt new file mode 100644 index 0000000000..0cdea9e2c7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ບັນຫາດ່ວນສໍາລັບ 1.1.5 +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/lo/changelogs/40101070.txt b/fastlane/metadata/android/lo/changelogs/40101070.txt new file mode 100644 index 0000000000..08926229f9 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮອງຮັບເບຕ້າສຳລັບ Spaces. ບີບອັດວິດີໂອກ່ອນທີ່ຈະສົ່ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/lo/changelogs/40101080.txt b/fastlane/metadata/android/lo/changelogs/40101080.txt new file mode 100644 index 0000000000..f2947aae63 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປັບປຸງສໍາລັບ Spaces. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/lo/changelogs/40101090.txt b/fastlane/metadata/android/lo/changelogs/40101090.txt new file mode 100644 index 0000000000..129f86c562 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101090.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮັບຮອງສໍາລັບເຄືອຂ່າຍ gitter.im. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/lo/changelogs/40101100.txt b/fastlane/metadata/android/lo/changelogs/40101100.txt new file mode 100644 index 0000000000..337137d3bc --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບສີສັນ ແລະຄຸນສົມບັດໃໝ່ສຳລັບຊ່ອງຫວ່າງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/lo/changelogs/40101110.txt b/fastlane/metadata/android/lo/changelogs/40101110.txt new file mode 100644 index 0000000000..f95972eaa9 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບສີສັນ ແລະຄຸນສົມບັດໃໝ່ສຳລັບຊ່ອງຫວ່າງ (bugfix ສຳລັບ 1.1.10) +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/lo/changelogs/40101120.txt b/fastlane/metadata/android/lo/changelogs/40101120.txt new file mode 100644 index 0000000000..a613e176aa --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບ ແລະແກ້ໄຂການຂັດຂ້ອງຫຼັງຈາກການໂທວິດີໂອ +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/lo/changelogs/40101130.txt b/fastlane/metadata/android/lo/changelogs/40101130.txt new file mode 100644 index 0000000000..3cc85bc6f8 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັນຫາກ່ຽວຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/lo/changelogs/40101140.txt b/fastlane/metadata/android/lo/changelogs/40101140.txt new file mode 100644 index 0000000000..11a5df1059 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັນຫາກ່ຽວກັບຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/lo/changelogs/40101150.txt b/fastlane/metadata/android/lo/changelogs/40101150.txt new file mode 100644 index 0000000000..8751496a49 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປະຕິບັດຂໍ້ຄວາມສຽງພາຍໃຕ້ການຕັ້ງຄ່າຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/lo/changelogs/40101160.txt b/fastlane/metadata/android/lo/changelogs/40101160.txt new file mode 100644 index 0000000000..c102140102 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂຂໍ້ຜິດພາດໃນເວລາສົ່ງຂໍ້ຄວາມເຂົ້າລະຫັດຖ້າມີຄົນຢູ່ໃນຫ້ອງອອກຈາກລະບົບ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/lo/changelogs/40102000.txt b/fastlane/metadata/android/lo/changelogs/40102000.txt new file mode 100644 index 0000000000..69f1667b93 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40102000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການສົນທະນາດ້ວຍສຽງເປີດໄວ້ໂດຍມາດຕະຖານ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/lo/changelogs/40102010.txt b/fastlane/metadata/android/lo/changelogs/40102010.txt new file mode 100644 index 0000000000..237b48ba3b --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40102010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປັບປຸງຫຼາຍຢ່າງກ່ຽວກັບ VoIP ແລະ Spaces (ຍັງຢູ່ໃນເບຕ້າ). +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/lo/changelogs/40103000.txt b/fastlane/metadata/android/lo/changelogs/40103000.txt new file mode 100644 index 0000000000..8e386df4a7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຈັດລະບຽບຫ້ອງຂອງທ່ານໂດຍໃຊ້ Spaces! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/lo/changelogs/40103010.txt b/fastlane/metadata/android/lo/changelogs/40103010.txt new file mode 100644 index 0000000000..0a6a6461fd --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຈັດລະບຽບຫ້ອງຂອງທ່ານໂດຍໃຊ້ Spaces! v1.3.1 ກຳລັງແກ້ໄຂບັນແອັບໃຊ້ບໍ່ໄດ້ທີ່ສາມາດເກີດຂຶ້ນໃນ v1.3.0. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/lo/changelogs/40103020.txt b/fastlane/metadata/android/lo/changelogs/40103020.txt new file mode 100644 index 0000000000..8b9ed12a3a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບ Android Auto. ການແກ້ໄຂຍັກຫຼາຍອັນ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/lo/changelogs/40103030.txt b/fastlane/metadata/android/lo/changelogs/40103030.txt new file mode 100644 index 0000000000..6374f4fdf5 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103030.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເຮັດໃຫ້ນະໂຍບາຍຂອງເຄື່ອງແມ່ຂ່າຍຢັ້ງຢືນຕົວຕົນ (ຫຼາຍໜ່ວຍ) ໃຫ້ເຫັນຢູ່ໃນການຕັ້ງຄ່າ. ລຶບການຮອງຮັບ Android Auto ອອກຊົ່ວຄາວ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/lo/changelogs/40103040.txt b/fastlane/metadata/android/lo/changelogs/40103040.txt new file mode 100644 index 0000000000..896d9190ce --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/lo/changelogs/40103050.txt b/fastlane/metadata/android/lo/changelogs/40103050.txt new file mode 100644 index 0000000000..b76db1a80c --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103050.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/lo/changelogs/40103060.txt b/fastlane/metadata/android/lo/changelogs/40103060.txt new file mode 100644 index 0000000000..2e97183a56 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/lo/changelogs/40103070.txt b/fastlane/metadata/android/lo/changelogs/40103070.txt new file mode 100644 index 0000000000..47eb6c7b4d --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກສ່ວນໃຫຍ່ກ່ຽວກັບການແຈ້ງເຕືອນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/lo/changelogs/40103080.txt b/fastlane/metadata/android/lo/changelogs/40103080.txt new file mode 100644 index 0000000000..a12d374d20 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/lo/changelogs/40103090.txt b/fastlane/metadata/android/lo/changelogs/40103090.txt new file mode 100644 index 0000000000..ee93ca0a96 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103090.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບສະບັບຮ່າງການສົນທະນາດ້ວຍສຽງ. ແກ້ໄຂບັກຫຼາຍໆອັນ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.09 diff --git a/fastlane/metadata/android/lo/changelogs/40103100.txt b/fastlane/metadata/android/lo/changelogs/40103100.txt new file mode 100644 index 0000000000..b8b65387ef --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບແບບສໍາຫຼວດ (ໃນຫ້ອງທົດລອງ). ການອອກແບບຕົວຢ່າງ URL ໃໝ່. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/lo/changelogs/40103110.txt b/fastlane/metadata/android/lo/changelogs/40103110.txt new file mode 100644 index 0000000000..f64ee3b469 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/lo/changelogs/40103120.txt b/fastlane/metadata/android/lo/changelogs/40103120.txt new file mode 100644 index 0000000000..a7b8412cfb --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ!. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/lo/changelogs/40103130.txt b/fastlane/metadata/android/lo/changelogs/40103130.txt new file mode 100644 index 0000000000..cb725ee68c --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.13 diff --git a/fastlane/metadata/android/lo/changelogs/40103140.txt b/fastlane/metadata/android/lo/changelogs/40103140.txt new file mode 100644 index 0000000000..1f72757cf1 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.14 diff --git a/fastlane/metadata/android/lo/changelogs/40103150.txt b/fastlane/metadata/android/lo/changelogs/40103150.txt new file mode 100644 index 0000000000..c11acd15d6 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.15 diff --git a/fastlane/metadata/android/lo/changelogs/40103160.txt b/fastlane/metadata/android/lo/changelogs/40103160.txt new file mode 100644 index 0000000000..a17f218463 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.16 diff --git a/fastlane/metadata/android/lo/changelogs/40103170.txt b/fastlane/metadata/android/lo/changelogs/40103170.txt new file mode 100644 index 0000000000..b8337a19cc --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103170.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.17 diff --git a/fastlane/metadata/android/lo/changelogs/40103180.txt b/fastlane/metadata/android/lo/changelogs/40103180.txt new file mode 100644 index 0000000000..e6b65ff812 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103180.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.18 diff --git a/fastlane/metadata/android/lo/changelogs/40104000.txt b/fastlane/metadata/android/lo/changelogs/40104000.txt new file mode 100644 index 0000000000..9d849da5b7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປະຕິບັດເບື້ອງຕົ້ນຂອງຂໍ້ຄວາມແບບກະທູ້. ຂໍ້ຄວາມຮູບແບບປຸມເປົ້າ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/lo/changelogs/40104020.txt b/fastlane/metadata/android/lo/changelogs/40104020.txt new file mode 100644 index 0000000000..358ca7fd39 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການສະຫນັບສະຫນູນກັບ @room ແລະແບບສໍາຫຼວດທີ່ບໍ່ເປີດເຜີຍໃນບັນດາການປ່ຽນແປງເລັກນ້ອຍອື່ນໆ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/lo/changelogs/40104040.txt b/fastlane/metadata/android/lo/changelogs/40104040.txt new file mode 100644 index 0000000000..7bfaf76bf2 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວສະແດງການພິມ ແລະປັບປັງ IU. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.4 diff --git a/fastlane/metadata/android/lo/changelogs/40104060.txt b/fastlane/metadata/android/lo/changelogs/40104060.txt new file mode 100644 index 0000000000..10ae599698 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ທາມລາຍກະທູ້ ປະຈຸບັນເປັນແບບຖ່າຍທອດສົດ ແລະໄວຂຶ້ນ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.6 diff --git a/fastlane/metadata/android/lo/changelogs/40104070.txt b/fastlane/metadata/android/lo/changelogs/40104070.txt new file mode 100644 index 0000000000..4ede801350 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.7 diff --git a/fastlane/metadata/android/lo/changelogs/40104080.txt b/fastlane/metadata/android/lo/changelogs/40104080.txt new file mode 100644 index 0000000000..5d7de22065 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເລື່ອນໃນຂໍ້ຄວາມສຽງ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40104100.txt b/fastlane/metadata/android/lo/changelogs/40104100.txt new file mode 100644 index 0000000000..5d7de22065 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເລື່ອນໃນຂໍ້ຄວາມສຽງ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40104110.txt b/fastlane/metadata/android/lo/changelogs/40104110.txt new file mode 100644 index 0000000000..edc025cb39 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ໄຂ bug ຕ່າງໆແລະການປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/full_description.txt b/fastlane/metadata/android/lo/full_description.txt new file mode 100644 index 0000000000..094c1b57f5 --- /dev/null +++ b/fastlane/metadata/android/lo/full_description.txt @@ -0,0 +1,42 @@ +ລະບົບແຊັດນີ້ ມີຄວາມປອດໄພ ແລະເປັນແອັບທີ່ເຮັດຊ່ວຍສົ່ງເສີ່ມການເຮັດວຽກຮ່ວມກັນຢ່າງມີປະສິດທິຜົນໝາະສົມສຳລັບການສົນທະນາກຸ່ມໃນຂະນະທີ່ເຮັດວຽກທາງໄກ. ແອັບສົນທະນານີ້ໃຊ້ການເຂົ້າລະຫັດແບບຕົ້ນທາງຈົນຈົບເພື່ອໃຫ້ການປະຊຸມວິດີໂອທີ່ມີປະສິດທິພາບ, ການແບ່ງປັນໄຟລ໌ ແລະການໂທແບບສຽງ. + +ຄຸນສົມບັດຂອງລະບົບລວມມີ: +- ເຄື່ອງມືການສື່ສານອອນໄລນ໌ແບບລຳ້ໜ້າ +- ຂໍ້ຄວາມມີການເຂົ້າລະຫັດຢ່າງເຕັມສ່ວນເພື່ອໃຫ້ການສື່ສານຂອງອົງກອນມີຄວາມປອດໄພ, ເຖິງແມ່ນວ່າຈະເຮັດວຽກຈາກທາງໄກ +- ລະບົບແຊັດແບບແບ່ງກະຈາຍການຄຸ້ມຄອງໂດຍນຳໃຊ້ນະວັດຕະກຳເປີດຂອງລະບົບ Matrix +- ການແບ່ງປັນໄຟລ໌ຢ່າງປອດໄພກັບຂໍ້ມູນທີ່ເຂົ້າລະຫັດໄວ້ໃນຂະນະທີ່ຈັດການໂຄງການ +- ວິດີໂອສົນທະນາກັບ Voice over IP ແລະການແບ່ງປັນຫນ້າຈໍ +- ການເຊື່ອມໂຍງງ່າຍດາຍກັບເຄື່ອງມືການຮ່ວມມືອອນໄລນ໌ຂອງທ່ານ, ເຄື່ອງມືການຄຸ້ມຄອງໂຄງການ, ການບໍລິການ VoIP ແລະຂໍ້ຄວາມອື່ນໆຂອງທີມ + +ລະບົບນີ້ແມ່ນແຕກຕ່າງກັນຢ່າງສິ້ນເຊີງຈາກແອັບຯສົ່ງຂໍ້ຄວາມ ແລະແອັບສົ່ງເສີມການຮ່ວມມືອື່ນໆ. ມັນດໍາເນີນການຢູ່ໃນ Matrix, ເຄືອຂ່າຍເປີດສໍາລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພແລະການສື່ສານແບບກະຈາຍການຄຸ້ມຄອງ. ທ່ານສາມາດຕັ້ງສະຖານີດ້ວຍຕົນເອງເພື່ອໃຫ້ຜູ້ໃຊ້ເປັນເຈົ້າຂອງສູງສຸດແລະຄຸ້ມຄອງຂໍ້ມູນແລະຂໍ້ຄວາມຂອງຕົນເອງ. + +ຄວາມເປັນສ່ວນຕົວ ແລະຂໍ້ຄວາມເຂົ້າລະຫັດ +ລະບົບຈະປົກປ້ອງທ່ານຈາກການໂຄສະນາທີ່ບໍ່ຕ້ອງການ, ການຂຸດຄົ້ນຂໍ້ມູນແລະການສ້າງເງື່ອນໄຂປິດລ້ອມ. ລະບົບຍັງຮັບປະກັນຂໍ້ມູນທັງຫມົດຂອງທ່ານ, ການໂທວິດີໂອແບບຫນຶ່ງຕໍ່ຫນຶ່ງແລະການສື່ສານສຽງໂດຍຜ່ານການເຂົ້າລະຫັດແບບຕົ້ນທາງຫາປາຍທາງ ແລະການກວດສອບອຸປະກອນກ່ອນເຊື່ອມຕໍ່. + +ລະບົບເຮົາໃຫ້ທ່ານຄວບຄຸມຄວາມເປັນສ່ວນຕົວຂອງທ່ານໃນຂະນະທີ່ອະນຸຍາດໃຫ້ທ່ານຕິດຕໍ່ສື່ສານຢ່າງປອດໄພກັບທຸກຄົນໃນເຄືອຂ່າຍ Matrix, ຫຼືເຄື່ອງມືທີ່ທ່ານຄຸ້ນເຄີຍເຊັ່ນ Whatsapp, Slack, Facebook. + +ທ່ານສາມາດສ້າງສະຖານີຂອງຕົນເອງ +ເພື່ອໃຫ້ສາມາດຄວບຄຸມຂໍ້ມູນແລະການສົນທະນາທີ່ລະອຽດອ່ອນຂອງທ່ານຫຼາຍຂຶ້ນ, ທ່ານສາມາດຕິດຕັ້ງສະຖານີສື່ສານຂອງຕົນເອງຫຼືທ່ານສາມາດເລືອກຕິດຕັ້ງໄວ້ກັບເຄືອຂ່າຍ Matrix - ມາດຕະຖານສໍາລັບລະບົບເປີດ, ການສື່ສານແບບກະຈາຍການຄຸ້ມຄອງ. ລະບົບພວກເຮົາໃຫ້ທ່ານມີຄວາມເປັນສ່ວນຕົວ, ໄດ້ຕາມມາດຕະຖານຄວາມປອດໄພແລະມີຄວາມຍືດຫຍຸ່ນໃນການເຊື່ອມໂຍງ. + +ເປັນເຈົ້າຂອງຂໍ້ມູນຂອງທ່ານເອງ +ທ່ານສາມາດເລືອກໄດ້ວ່າຈະເກັບຂໍ້ມູນ ແລະຂໍ້ຄວາມຂອງທ່ານຢູ່ໃສ. ໂດຍບໍ່ມີຄວາມສ່ຽງຕໍ່ການຂຸດຄົ້ນຂໍ້ມູນຫຼືການເຂົ້າເຖິງຈາກພາກສ່ວນທີສາມ. + +ລະບົບພວກເຮົາເຮັດໃຫ້ທ່ານສາມາດຄວບຄຸມດ້ວຍວິທີຕ່າງໆ: +1. ຮັບບັນຊີຟຣີໃນເຊີບເວີສາທາລະນະ matrix.org ທີ່ໂຮສໂດຍຜູ້ພັດທະນາ Matrix, ຫຼືເລືອກຈາກເຄື່ອງແມ່ຂ່າຍສາທາລະນະຫຼາຍພັນເຊີບເວີທີ່ໂຮສໂດຍອາສາສະໝັກ. +2. ຈັດການບັນຊີຂອງທ່ານເອງໂດຍການແລ່ນເຊີບເວີໃນໂຄງລ່າງພື້ນຖານ IT ຂອງທ່ານເອງ +3. ລົງທະບຽນສໍາລັບບັນຊີຢູ່ໃນເຄື່ອງແມ່ຂ່າຍທີ່ກໍາຫນົດເອງໂດຍພຽງແຕ່ສະຫມັກກັບ Element Matrix Services hosting platform + +ສົ່ງຂໍ້ຄວາມ ແລະການຮ່ວມມືກັບລະບົບອື່ນໆທີ່ທ່ານຄຸ້ນເຄີຍ +ທ່ານສາມາດສົນທະນາກັບທຸກຄົນໃນເຄືອຂ່າຍ Matrix, ບໍ່ວ່າເຂົາເຈົ້າກໍາລັງໃຊ້ Element, ແອັບ Matrix ອື່ນ ຫຼືເຖິງແມ່ນວ່າເຂົາເຈົ້າກຳລັງໃຊ້ແອັບສົ່ງຂໍ້ຄວາມອື່ນຢູ່. + +ປອດໄພທີ່ສຸດ +ການເຂົ້າລະຫັດຈາກຕົ້ນທາງຫາປາຍທາງ (ມີພຽງແຕ່ຜູ້ທີ່ຢູ່ໃນການສົນທະນາສາມາດຖອດລະຫັດຂໍ້ຄວາມໄດ້), ແລະການຢັ້ງຢືນອຸປະກອນໂດຍການລົງນາມຂ້າມອຸປະກອນ. + +ການສື່ສານແລະການເຊື່ອມໂຍງທີ່ຄົບຖ້ວນສົມບູນ +ການສົ່ງຂໍ້ຄວາມ, ການໂທສຽງ ແລະວິດີໂອ, ການແບ່ງປັນໄຟລ໌, ການແບ່ງປັນໜ້າຈໍ ແລະການເຊື່ອມໂຍງທັງໝົດ, ບັອດ ແລະວິດເຈັດ. ສ້າງຫ້ອງ, ຊຸມຊົນ, ຕິດຕໍ່ພົວພັນແລະເຮັດສິ່ງຕ່າງໆ. + +ສືບຕໍ່ຈາກການສົນທະນາລ້າສຸດ +ທ່ານສາມາດຕິດຕໍ່ພົວພັນໄດ້ທຸກບ່ອນທີ່ຕ້ອງການ ເຊິ່ງປະຫວັດການສົນທະນາຈະຖືກອັບເດດໄປຫາບັນດາອຸປະກອນຕ່າງໆຂອງທ່ານ ລວມເຖິງໃນເວັບໄຊຕ໌ທີ່ https://app.element.io + +ເຕັກໂນໂລຊີໂອເພິນຊອດ +Element ເປັນໂຄງການເຕັກໂນໂລຊີໂອເພິນຊອດ, ເປັນເຈົ້າພາບໂດຍ GitHub. ກະລຸນາລາຍງານຂໍ້ບົກພ່ອງ ແລະ/ຫຼື ປະກອບສ່ວນເຂົ້າໃນການພັດທະນາຂອງລະບົບຢູ່ https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/lo/short_description.txt b/fastlane/metadata/android/lo/short_description.txt new file mode 100644 index 0000000000..625b078915 --- /dev/null +++ b/fastlane/metadata/android/lo/short_description.txt @@ -0,0 +1 @@ +ສົ່ງຂໍ້ຄວາມກຸ່ມ - ສົ່ງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ, ສົນທະນາກຸ່ມ ແລະໂທວິດີໂອ diff --git a/fastlane/metadata/android/lo/title.txt b/fastlane/metadata/android/lo/title.txt new file mode 100644 index 0000000000..a9054d803b --- /dev/null +++ b/fastlane/metadata/android/lo/title.txt @@ -0,0 +1 @@ +ລະບົບສື່ສານທີ່ມີຄວາມປອດໄພສູງ diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104100.txt b/fastlane/metadata/android/pt-BR/changelogs/40104100.txt new file mode 100644 index 0000000000..eb746aca2b --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Rolar em mensagem de voz. Vários consertos de bugs e melhorias de estabilidade. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104110.txt b/fastlane/metadata/android/pt-BR/changelogs/40104110.txt new file mode 100644 index 0000000000..6e11e92579 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Vários consertos de bugs e melhorias de estabilidade. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104100.txt b/fastlane/metadata/android/sk/changelogs/40104100.txt new file mode 100644 index 0000000000..b96a539364 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Posúvanie v hlasovej správe. Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104110.txt b/fastlane/metadata/android/sk/changelogs/40104110.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104100.txt b/fastlane/metadata/android/sq/changelogs/40104100.txt new file mode 100644 index 0000000000..c45c2b9a33 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Rrëshqitje në mesazh zanor. Ndreqje të metash të ndryshme dhe përmirësime qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104110.txt b/fastlane/metadata/android/sq/changelogs/40104110.txt new file mode 100644 index 0000000000..df9b377adb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash të ndryshme dhe përmirësime qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104100.txt b/fastlane/metadata/android/sv-SE/changelogs/40104100.txt new file mode 100644 index 0000000000..a1585aacf5 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i denna version: Scrolla i röstmeddelanden. Diverse buggfixar och förbättrad stabilitet. +Fullständig lista över ändringar: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104110.txt b/fastlane/metadata/android/sv-SE/changelogs/40104110.txt new file mode 100644 index 0000000000..92cce13669 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i denna version: Diverse buggfixar och förbättrad stabilitet. +Fullständig lista över ändringar: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104100.txt b/fastlane/metadata/android/uk/changelogs/40104100.txt new file mode 100644 index 0000000000..93c9fe4b70 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Прокручування в голосових повідомленнях. Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104110.txt b/fastlane/metadata/android/uk/changelogs/40104110.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104100.txt b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt new file mode 100644 index 0000000000..c78ed7dd2d --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:捲動音訊訊息。多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104110.txt b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 54d02d226e..4b9e605cd0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -57,7 +57,8 @@ import java.util.concurrent.TimeUnit class CommonTestHelper(context: Context) { internal val matrix: TestMatrix - val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private var accountNumber = 0 fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor @@ -167,7 +168,8 @@ class CommonTestHelper(context: Context) { if (rootThreadEventId != null) { room.relationService().replyInThread( rootThreadEventId = rootThreadEventId, - replyInThreadText = formattedMessage) + replyInThreadText = formattedMessage + ) } else { room.sendService().sendTextMessage(formattedMessage) } @@ -237,7 +239,7 @@ class CommonTestHelper(context: Context) { password: String, testParams: SessionTestParams): Session { val session = createAccountAndSync( - userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(), + userNamePrefix + "_" + accountNumber++ + "_" + UUID.randomUUID(), password, testParams ) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index 732f4f7dce..f5f585a1e0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -29,8 +29,10 @@ import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.util.time.DefaultClock import java.io.ByteArrayOutputStream import java.io.InputStream +import java.util.UUID /** * Unit tests AttachmentEncryptionTest. @@ -48,13 +50,18 @@ class AttachmentEncryptionTest { inputStream = if (inputAsByteArray.isEmpty()) { inputAsByteArray.inputStream() } else { - val memoryFile = MemoryFile("file" + System.currentTimeMillis(), inputAsByteArray.size) + val memoryFile = MemoryFile("file_" + UUID.randomUUID(), inputAsByteArray.size) memoryFile.outputStream.write(inputAsByteArray) memoryFile.inputStream } val decryptedStream = ByteArrayOutputStream() - val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream) + val result = MXEncryptedAttachments.decryptAttachment( + attachmentStream = inputStream, + elementToDecrypt = encryptedFileInfo.toElementToDecrypt()!!, + outputStream = decryptedStream, + clock = DefaultClock() + ) assert(result) @@ -117,9 +124,13 @@ class AttachmentEncryptionTest { url = "dummyUrl" ) - assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", - checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", - encryptedFileInfo)) + assertEquals( + "YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption( + "zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", + encryptedFileInfo + ) + ) } @Test @@ -138,8 +149,12 @@ class AttachmentEncryptionTest { url = "dummyUrl" ) - assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", - checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", - encryptedFileInfo)) + assertNotEquals( + "YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption( + "tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", + encryptedFileInfo + ) + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt index c717c8e33f..ba1afd4758 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.time.DefaultClock import kotlin.random.Random internal class CryptoStoreHelper { @@ -34,7 +35,8 @@ internal class CryptoStoreHelper { .build(), crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), userId = "userId_" + Random.nextInt(), - deviceId = "deviceId_sample" + deviceId = "deviceId_sample", + clock = DefaultClock(), ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt index f43c425cc9..3f75aa0979 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -27,6 +27,7 @@ import org.junit.runner.RunWith import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmManager import org.matrix.olm.OlmSession @@ -37,6 +38,7 @@ private const val DUMMY_DEVICE_KEY = "DeviceKey" class CryptoStoreTest : InstrumentedTest { private val cryptoStoreHelper = CryptoStoreHelper() + private val clock = DefaultClock() @Before fun setup() { @@ -106,7 +108,7 @@ class CryptoStoreTest : InstrumentedTest { // Note: we cannot be sure what will be the result of getLastUsedSessionId() here - olmSessionWrapper2.onMessageReceived() + olmSessionWrapper2.onMessageReceived(clock.epochMillis()) cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) // sessionId2 is returned now @@ -114,7 +116,7 @@ class CryptoStoreTest : InstrumentedTest { Thread.sleep(2) - olmSessionWrapper1.onMessageReceived() + olmSessionWrapper1.onMessageReceived(clock.epochMillis()) cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) // sessionId1 is returned now diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt index 5c011c8b2f..27d3fdc856 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.SessionRealmModule import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent @@ -42,6 +43,7 @@ import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMes internal class ChunkEntityTest : InstrumentedTest { private lateinit var monarchy: Monarchy + private val clock = DefaultClock() @Before fun setup() { @@ -59,7 +61,7 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } chunk.addTimelineEvent( @@ -75,7 +77,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } chunk.addTimelineEvent( @@ -153,7 +155,7 @@ internal class ChunkEntityTest : InstrumentedTest { events: List, direction: PaginationDirection) { events.forEach { event -> - val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = event.toEntity(roomId, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } addTimelineEvent( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt index b86c86c0c7..ccf1c7c2c9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt @@ -26,8 +26,8 @@ internal class FakeGetContextOfEventTask constructor(private val tokenChunkEvent override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent( - Random.nextLong(System.currentTimeMillis()).toString(), - Random.nextLong(System.currentTimeMillis()).toString(), + Random.nextLong().toString(), + Random.nextLong().toString(), fakeEvents ) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt index d09bfb18c6..f241be0c5c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt @@ -25,7 +25,7 @@ internal class FakePaginationTask @Inject constructor(private val tokenChunkEven override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) - val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) + val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong().toString(), fakeEvents) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt index 74ca7304f7..ad11ef9a5e 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt @@ -46,8 +46,9 @@ data class IncomingRequestCancellation( * Factory * * @param event the event + * @param currentTimeMillis the current time in milliseconds */ - fun fromEvent(event: Event): IncomingRequestCancellation? { + fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRequestCancellation? { return event.getClearContent() .toModel() ?.let { @@ -55,7 +56,7 @@ data class IncomingRequestCancellation( userId = event.senderId, deviceId = it.requestingDeviceId, requestId = it.requestId, - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt index 45b0926d89..0b2c32284b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt @@ -64,8 +64,9 @@ data class IncomingRoomKeyRequest( * Factory * * @param event the event + * @param currentTimeMillis the current time in milliseconds */ - fun fromEvent(event: Event): IncomingRoomKeyRequest? { + fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? { return event.getClearContent() .toModel() ?.let { @@ -74,7 +75,7 @@ data class IncomingRoomKeyRequest( deviceId = it.requestingDeviceId, requestId = it.requestId, requestBody = it.body ?: RoomKeyRequestBody(), - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt index 5afffef1ae..80f70c83f3 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt @@ -64,8 +64,9 @@ data class IncomingSecretShareRequest( * Factory * * @param event the event + * @param currentTimeMillis the current time in milliseconds */ - fun fromEvent(event: Event): IncomingSecretShareRequest? { + fun fromEvent(event: Event, currentTimeMillis: Long): IncomingSecretShareRequest? { return event.getClearContent() .toModel() ?.let { @@ -74,7 +75,7 @@ data class IncomingSecretShareRequest( deviceId = it.requestingDeviceId, requestId = it.requestId, secretName = it.secretName, - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index b9d0c0ad2c..ec67e4b31d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -129,11 +129,10 @@ interface VerificationService { private const val TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000 private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 - fun isValidRequest(age: Long?): Boolean { + fun isValidRequest(age: Long?, currentTimeMillis: Long): Boolean { if (age == null) return false - val now = System.currentTimeMillis() - val tooInThePast = now - TEN_MINUTES_IN_MILLIS - val tooInTheFuture = now + FIVE_MINUTES_IN_MILLIS + val tooInThePast = currentTimeMillis - TEN_MINUTES_IN_MILLIS + val tooInTheFuture = currentTimeMillis + FIVE_MINUTES_IN_MILLIS return age in tooInThePast..tooInTheFuture } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt index 4380e31bff..aaf23d17b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import javax.inject.Inject @@ -65,6 +66,7 @@ internal class CancelGossipRequestWorker(context: Context, params: WorkerParamet @Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var credentials: Credentials + @Inject lateinit var clock: Clock override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -85,7 +87,7 @@ internal class CancelGossipRequestWorker(context: Context, params: WorkerParamet content = toDeviceContent.toContent(), senderId = credentials.userId ).also { - it.ageLocalTs = System.currentTimeMillis() + it.ageLocalTs = clock.epochMillis() }) params.recipients.forEach { userToDeviceMap -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 6a57d94677..54c9990bf6 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -103,6 +103,7 @@ import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -130,6 +131,7 @@ internal class DefaultCryptoService @Inject constructor( private val userId: String, @DeviceId private val deviceId: String?, + private val clock: Clock, private val myDeviceInfoHolder: Lazy, // the crypto store private val cryptoStore: IMXCryptoStore, @@ -701,11 +703,11 @@ internal class DefaultCryptoService @Inject constructor( } val safeAlgorithm = alg if (safeAlgorithm != null) { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent() starts") runCatching { val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") MXEncryptEventContentResult(content, EventType.ENCRYPTED) }.foldToCallback(callback) } else { @@ -1022,9 +1024,9 @@ internal class DefaultCryptoService @Inject constructor( return withContext(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("importRoomKeys starts") - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) - val t1 = System.currentTimeMillis() + val t1 = clock.epochMillis() Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") @@ -1032,7 +1034,7 @@ internal class DefaultCryptoService @Inject constructor( .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) .fromJson(roomKeys) - val t2 = System.currentTimeMillis() + val t2 = clock.epochMillis() Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms") @@ -1224,7 +1226,7 @@ internal class DefaultCryptoService @Inject constructor( // val deviceKey = deviceInfo.identityKey() // // val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 -// val now = System.currentTimeMillis() +// val now = clock.epochMillis() // if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { // Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") // return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 6cae2d0935..535999373b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -31,19 +31,23 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.logLimit +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject // Legacy name: MXDeviceList @SessionScope -internal class DeviceListManager @Inject constructor(private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val syncTokenStore: SyncTokenStore, - private val credentials: Credentials, - private val downloadKeysForUsersTask: DownloadKeysForUsersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor) { +internal class DeviceListManager @Inject constructor( + private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val syncTokenStore: SyncTokenStore, + private val credentials: Credentials, + private val downloadKeysForUsersTask: DownloadKeysForUsersTask, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor, + private val clock: Clock, +) { interface UserDevicesUpdateListener { fun onUsersDeviceUpdate(userIds: List) @@ -310,9 +314,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM stored } else { Timber.v("## CRYPTO | downloadKeys() : starts") - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") result.also { it.addEntriesFromMap(stored) } @@ -475,8 +479,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (!isVerified) { - Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + - deviceKeys.deviceId + " with error " + errorMessage) + Timber.e( + "## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage + ) return false } @@ -486,9 +492,11 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // best off sticking with the original keys. // // Should we warn the user about it somehow? - Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + - deviceKeys.deviceId + " has changed : " + - previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + Timber.e( + "## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey + ) Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 1c8bce7377..a094189645 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -47,6 +48,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) internal class EventDecryptor @Inject constructor( private val cryptoCoroutineScope: CoroutineScope, private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, private val roomDecryptorProvider: RoomDecryptorProvider, private val messageEncrypter: MessageEncrypter, private val sendToDeviceTask: SendToDeviceTask, @@ -153,7 +155,7 @@ internal class EventDecryptor @Inject constructor( // we should force start a new session for those Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged") // get the one that should be retried according to rate limit - val now = System.currentTimeMillis() + val now = clock.epochMillis() val toUnwedge = wedgedDevices.filter { val lastForcedDate = lastNewSessionForcedDates[it] ?: 0 if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt index 0013c31eea..a2c85e5ceb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.startChain +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -44,8 +45,8 @@ internal class GossipingWorkManager @Inject constructor( } // Prevent sending queue to stay broken after app restart - // The unique queue id will stay the same as long as this object is instanciated - val queueSuffixApp = System.currentTimeMillis() + // The unique queue id will stay the same as long as this object is instantiated + private val queueSuffixApp = UUID.randomUUID() fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { workManagerProvider.workManager diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt index b907b57f82..1612caba9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber import java.util.concurrent.Executors @@ -59,7 +60,9 @@ internal class IncomingGossipingRequestManager @Inject constructor( private val roomEncryptorsStore: RoomEncryptorsStore, private val roomDecryptorProvider: RoomDecryptorProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope) { + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, +) { private val executor = Executors.newSingleThreadExecutor() @@ -89,7 +92,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun onVerificationCompleteForDevice(deviceId: String) { // For now we just keep an in memory cache synchronized(recentlyVerifiedDevices) { - recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() + recentlyVerifiedDevices[deviceId] = clock.epochMillis() } } @@ -100,7 +103,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( } if (verifTimestamp == null) return false - val age = System.currentTimeMillis() - verifTimestamp + val age = clock.epochMillis() - verifTimestamp return age < FIVE_MINUTES_IN_MILLIS } @@ -114,11 +117,11 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun onGossipingRequestEvent(event: Event) { val roomKeyShare = event.getClearContent().toModel() Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare") - // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + // val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } when (roomKeyShare?.action) { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { if (event.getClearType() == EventType.REQUEST_SECRET) { - IncomingSecretShareRequest.fromEvent(event)?.let { + IncomingSecretShareRequest.fromEvent(event, clock.epochMillis())?.let { if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") @@ -129,7 +132,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( } } } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { - IncomingRoomKeyRequest.fromEvent(event)?.let { + IncomingRoomKeyRequest.fromEvent(event, clock.epochMillis())?.let { if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") @@ -141,7 +144,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( } } GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { - IncomingRequestCancellation.fromEvent(event)?.let { + IncomingRequestCancellation.fromEvent(event, clock.epochMillis())?.let { receivedRequestCancellations.add(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt index f8235bf344..89e38cb7cf 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -29,6 +29,7 @@ import javax.crypto.spec.SecretKeySpec import kotlin.experimental.and import kotlin.experimental.xor import kotlin.math.min +import kotlin.system.measureTimeMillis /** * Utility class to import/export the crypto data @@ -310,40 +311,40 @@ internal object MXMegolmExportEncryption { */ @Throws(Exception::class) private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { - val t0 = System.currentTimeMillis() - - // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm - // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. - // noticed as dklen/hlen - val prf = Mac.getInstance("HmacSHA512") - prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) - - // 512 bits key length val key = ByteArray(64) - val uc = ByteArray(64) + measureTimeMillis { + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + val prf = Mac.getInstance("HmacSHA512") + prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) - // U1 = PRF(Password, Salt || INT_32_BE(i)) - prf.update(salt) - val int32BE = ByteArray(4) { 0.toByte() } - int32BE[3] = 1.toByte() - prf.update(int32BE) - prf.doFinal(uc, 0) + // 512 bits key length + val uc = ByteArray(64) - // copy to the key - System.arraycopy(uc, 0, key, 0, uc.size) - - for (index in 2..iterations) { - // Uc = PRF(Password, Uc-1) - prf.update(uc) + // U1 = PRF(Password, Salt || INT_32_BE(i)) + prf.update(salt) + val int32BE = ByteArray(4) { 0.toByte() } + int32BE[3] = 1.toByte() + prf.update(int32BE) prf.doFinal(uc, 0) - // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in uc.indices) { - key[byteIndex] = key[byteIndex] xor uc[byteIndex] - } - } + // copy to the key + System.arraycopy(uc, 0, key, 0, uc.size) - Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms") + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in uc.indices) { + key[byteIndex] = key[byteIndex] xor uc[byteIndex] + } + } + }.also { + Timber.v("## deriveKeys() : $iterations in $it ms") + } return key } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 4947761f05..7eec83abdd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.convertFromUTF8 import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmMessage @@ -55,7 +56,8 @@ internal class MXOlmDevice @Inject constructor( */ private val store: IMXCryptoStore, private val olmSessionStore: OlmSessionStore, - private val inboundGroupSessionStore: InboundGroupSessionStore + private val inboundGroupSessionStore: InboundGroupSessionStore, + private val clock: Clock, ) { val mutex = Mutex() @@ -277,7 +279,7 @@ internal class MXOlmDevice @Inject constructor( // Pretend we've received a message at this point, otherwise // if we try to send a message to the device, it won't use // this session - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) @@ -348,7 +350,7 @@ internal class MXOlmDevice @Inject constructor( val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) // This counts as a received message: set last received message time to now - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { @@ -454,7 +456,7 @@ internal class MXOlmDevice @Inject constructor( payloadString = olmSessionWrapper.mutex.withLock { olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) } } olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) @@ -520,6 +522,7 @@ internal class MXOlmDevice @Inject constructor( return MXOutboundSessionInfo( sessionId = sessionId, sharedWithHelper = SharedWithHelper(roomId, sessionId, store), + clock, restoredOutboundGroupSession.creationTime ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt index 792c9a25dc..8143e36892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import timber.log.Timber import javax.inject.Inject @@ -38,6 +39,7 @@ internal class OneTimeKeysUploader @Inject constructor( private val olmDevice: MXOlmDevice, private val objectSigner: ObjectSigner, private val uploadKeysTask: UploadKeysTask, + private val clock: Clock, context: Context ) { // tell if there is a OTK check in progress @@ -77,7 +79,7 @@ internal class OneTimeKeysUploader @Inject constructor( Timber.v("maybeUploadOneTimeKeys: already in progress") return } - if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { + if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { // we've done a key upload recently. Timber.v("maybeUploadOneTimeKeys: executed too recently") return @@ -94,7 +96,7 @@ internal class OneTimeKeysUploader @Inject constructor( Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}") - lastOneTimeKeyCheck = System.currentTimeMillis() + lastOneTimeKeyCheck = clock.epochMillis() // We then check how many keys we can store in the Account object. val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() @@ -126,7 +128,7 @@ internal class OneTimeKeysUploader @Inject constructor( // Check if we need to forget a fallback key val latestPublishedTime = getLastFallbackKeyPublishTime() - if (latestPublishedTime != 0L && System.currentTimeMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { + if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { // This should be called once you are reasonably certain that you will not receive any more messages // that use the old fallback key Timber.d("## forgetFallbackKey()") @@ -168,7 +170,7 @@ internal class OneTimeKeysUploader @Inject constructor( olmDevice.markKeysAsPublished() if (hadUnpublishedFallbackKey) { // It had an unpublished fallback key that was published just now - saveLastFallbackKeyPublishTime(System.currentTimeMillis()) + saveLastFallbackKeyPublishTime(clock.epochMillis()) } if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt index 69b405aedc..3b43ad672b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber @@ -57,6 +58,7 @@ internal class SendGossipRequestWorker(context: Context, params: WorkerParameter @Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var credentials: Credentials + @Inject lateinit var clock: Clock override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -85,7 +87,7 @@ internal class SendGossipRequestWorker(context: Context, params: WorkerParameter content = toDeviceContent.toContent(), senderId = credentials.userId ).also { - it.ageLocalTs = System.currentTimeMillis() + it.ageLocalTs = clock.epochMillis() }) params.keyShareRequest.recipients.forEach { userToDeviceMap -> @@ -109,7 +111,7 @@ internal class SendGossipRequestWorker(context: Context, params: WorkerParameter content = toDeviceContent.toContent(), senderId = credentials.userId ).also { - it.ageLocalTs = System.currentTimeMillis() + it.ageLocalTs = clock.epochMillis() }) params.secretShareRequest.recipients.forEach { userToDeviceMap -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt index fd472fe73b..113d71d387 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber @@ -63,6 +64,7 @@ internal class SendGossipWorker( @Inject lateinit var credentials: Credentials @Inject lateinit var messageEncrypter: MessageEncrypter @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction + @Inject lateinit var clock: Clock override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -129,7 +131,7 @@ internal class SendGossipWorker( content = toDeviceContent.toContent(), senderId = credentials.userId ).also { - it.ageLocalTs = System.currentTimeMillis() + it.ageLocalTs = clock.epochMillis() }) try { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index f9bcdf2c68..86674b4ac5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -26,13 +26,17 @@ import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val cryptoStore: IMXCryptoStore) { +internal class MegolmSessionDataImporter @Inject constructor( + private val olmDevice: MXOlmDevice, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, +) { /** * Import a list of megolm session keys. @@ -47,7 +51,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi fun handle(megolmSessionsData: List, fromBackup: Boolean, progressListener: ProgressListener?): ImportRoomKeysResult { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val totalNumbersOfKeys = megolmSessionsData.size var lastProgress = 0 @@ -103,7 +107,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) } - val t1 = System.currentTimeMillis() + val t1 = clock.epochMillis() Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index f052194230..b31b5e8a64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO) @@ -64,7 +65,8 @@ internal class MXMegolmEncryption( private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, ) : IMXEncrypting, IMXGroupEncryption { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note @@ -86,11 +88,11 @@ internal class MXMegolmEncryption( override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { - val ts = System.currentTimeMillis() + val ts = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") - Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") + Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -99,7 +101,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis") } } @@ -125,14 +127,14 @@ internal class MXMegolmEncryption( } override suspend fun preshareKey(userIds: List) { - val ts = System.currentTimeMillis() + val ts = clock.epochMillis() Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...") val devices = getDevicesInRoom(userIds) val outboundSession = ensureOutboundSession(devices.allowedDevices) notifyWithheldForSession(devices.withHeldDevices, outboundSession) - Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis") } /** @@ -148,12 +150,14 @@ internal class MXMegolmEncryption( "ed25519" to olmDevice.deviceEd25519Key!! ) - olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, - emptyList(), keysClaimedMap, false) + olmDevice.addInboundGroupSession( + sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, + emptyList(), keysClaimedMap, false + ) defaultKeysBackupService.maybeBackupKeys() - return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore)) + return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock) } /** @@ -243,12 +247,12 @@ internal class MXMegolmEncryption( payload["type"] = EventType.ROOM_KEY payload["content"] = submap - var t0 = System.currentTimeMillis() + var t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) Timber.tag(loggerTag.value).v( - """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms""" .trimMargin() ) val contentMap = MXUsersDevicesMap() @@ -301,7 +305,7 @@ internal class MXMegolmEncryption( cryptoStore.saveGossipingEvents(gossipingEventBuffer) if (haveTargets) { - t0 = System.currentTimeMillis() + t0 = clock.epochMillis() Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) @@ -309,7 +313,7 @@ internal class MXMegolmEncryption( withContext(coroutineDispatchers.io) { sendToDeviceTask.execute(sendToDeviceParams) } - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>") @@ -334,7 +338,8 @@ internal class MXMegolmEncryption( senderKey: String?, code: WithHeldCode) { Timber.tag(loggerTag.value).d("notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + - " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}") + " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}" + ) val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 136fdc05f5..4225d604aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( @@ -42,7 +43,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope) { + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, +) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( @@ -58,7 +61,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor( messageEncrypter = messageEncrypter, warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, coroutineDispatchers = coroutineDispatchers, - cryptoCoroutineScope = cryptoCoroutineScope + cryptoCoroutineScope = cryptoCoroutineScope, + clock = clock, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 091abd4974..28d925d8fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -18,21 +18,24 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber internal class MXOutboundSessionInfo( // The id of the session val sessionId: String, val sharedWithHelper: SharedWithHelper, + private val clock: Clock, // When the session was created - private val creationTime: Long = System.currentTimeMillis()) { + private val creationTime: Long = clock.epochMillis(), +) { // Number of times this session has been used var useCount: Int = 0 fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { var needsRotation = false - val sessionLifetime = System.currentTimeMillis() - creationTime + val sessionLifetime = clock.epochMillis() - creationTime if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 91b6af6fc3..65bbb0c412 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey import org.matrix.android.sdk.internal.util.base64ToBase64Url import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 import org.matrix.android.sdk.internal.util.base64UrlToBase64 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File @@ -42,8 +43,9 @@ internal object MXEncryptedAttachments { fun encrypt(clearStream: InputStream, outputFile: File, + clock: Clock, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val secureRandom = SecureRandom() val initVectorBytes = ByteArray(16) { 0.toByte() } @@ -100,7 +102,7 @@ internal object MXEncryptedAttachments { hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), v = "v2" ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + .also { Timber.v("Encrypt in ${clock.epochMillis() - t0}ms") } } // fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair { @@ -159,8 +161,8 @@ internal object MXEncryptedAttachments { * @param attachmentStream the attachment stream. Will be closed after this method call. * @return the encryption file info */ - fun encryptAttachment(attachmentStream: InputStream): EncryptionResult { - val t0 = System.currentTimeMillis() + fun encryptAttachment(attachmentStream: InputStream, clock: Clock): EncryptionResult { + val t0 = clock.epochMillis() val secureRandom = SecureRandom() // generate a random iv key @@ -221,7 +223,7 @@ internal object MXEncryptedAttachments { ), encryptedByteArray = byteArrayOutputStream.toByteArray() ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + .also { Timber.v("Encrypt in ${clock.epochMillis() - t0}ms") } } /** @@ -234,14 +236,16 @@ internal object MXEncryptedAttachments { */ fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, - outputStream: OutputStream): Boolean { + outputStream: OutputStream, + clock: Clock + ): Boolean { // sanity checks if (null == attachmentStream || elementToDecrypt == null) { Timber.e("## decryptAttachment() : null stream") return false } - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() try { val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) @@ -279,7 +283,8 @@ internal object MXEncryptedAttachments { return false } - return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + Timber.v("Decrypt in ${clock.epochMillis() - t0} ms") + return true } catch (oom: OutOfMemoryError) { Timber.e(oom, "## decryptAttachment() failed: OOM") } catch (e: Exception) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt index c12879dbee..4d5b38acbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -26,6 +26,7 @@ import java.util.UUID import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.experimental.xor +import kotlin.system.measureTimeMillis private const val SALT_LENGTH = 32 private const val DEFAULT_ITERATION = 500_000 @@ -91,52 +92,53 @@ internal fun deriveKey(password: String, iterations: Int, progressListener: ProgressListener?): ByteArray { // Note: copied and adapted from MXMegolmExportEncryption - val t0 = System.currentTimeMillis() - // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. // noticed as dklen/hlen - // dklen = 256 - // hlen = 512 - val prf = Mac.getInstance("HmacSHA512") - - prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) - // 256 bits key length val dk = ByteArray(32) - val uc = ByteArray(64) - // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen - prf.update(salt.toByteArray()) - val int32BE = byteArrayOf(0, 0, 0, 1) - prf.update(int32BE) - prf.doFinal(uc, 0) + measureTimeMillis { + // dklen = 256 + // hlen = 512 + val prf = Mac.getInstance("HmacSHA512") - // copy to the key - System.arraycopy(uc, 0, dk, 0, dk.size) + prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) - var lastProgress = -1 + val uc = ByteArray(64) - for (index in 2..iterations) { - // Uc = PRF(Password, Uc-1) - prf.update(uc) + // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen + prf.update(salt.toByteArray()) + val int32BE = byteArrayOf(0, 0, 0, 1) + prf.update(int32BE) prf.doFinal(uc, 0) - // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in dk.indices) { - dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] - } + // copy to the key + System.arraycopy(uc, 0, dk, 0, dk.size) - val progress = (index + 1) * 100 / iterations - if (progress != lastProgress) { - lastProgress = progress - progressListener?.onProgress(lastProgress, 100) + var lastProgress = -1 + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in dk.indices) { + dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] + } + + val progress = (index + 1) * 100 / iterations + if (progress != lastProgress) { + lastProgress = progress + progressListener?.onProgress(lastProgress, 100) + } } + }.also { + Timber.v("KeysBackupPassword: deriveKeys() : $iterations in $it ms") } - Timber.v("KeysBackupPassword: deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") - return dk } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt index 927d049eca..d7ce553f39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -34,7 +34,7 @@ internal data class OlmSessionWrapper( /** * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` */ - fun onMessageReceived() { - lastReceivedMessageTs = System.currentTimeMillis() + fun onMessageReceived(currentTimeMillis: Long) { + lastReceivedMessageTs = currentTimeMillis } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index fad901f95d..a509315e7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -97,6 +97,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmOutboundGroupSession @@ -110,7 +111,8 @@ internal class RealmCryptoStore @Inject constructor( @CryptoDatabase private val realmConfiguration: RealmConfiguration, private val crossSigningKeysMapper: CrossSigningKeysMapper, @UserId private val userId: String, - @DeviceId private val deviceId: String? + @DeviceId private val deviceId: String?, + private val clock: Clock, ) : IMXCryptoStore { /* ========================================================================================== @@ -307,7 +309,7 @@ internal class RealmCryptoStore @Inject constructor( // Add the device Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = System.currentTimeMillis() + newEntity.firstTimeSeenLocalTs = clock.epochMillis() userEntity.devices.add(newEntity) } else { // Update the device @@ -792,7 +794,7 @@ internal class RealmCryptoStore @Inject constructor( if (outboundGroupSession != null) { val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { - creationTime = System.currentTimeMillis() + creationTime = clock.epochMillis() putOutboundGroupSession(outboundGroupSession) } entity.outboundSessionInfo = info @@ -882,7 +884,8 @@ internal class RealmCryptoStore @Inject constructor( try { val key = OlmInboundGroupSessionEntity.createPrimaryKey( olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), - olmInboundGroupSessionWrapper.senderKey) + olmInboundGroupSessionWrapper.senderKey + ) it.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) @@ -1057,13 +1060,16 @@ internal class RealmCryptoStore @Inject constructor( localCreationTimestamp = 0 ) } - return monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) } @@ -1072,13 +1078,16 @@ internal class RealmCryptoStore @Inject constructor( realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } - val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + val trail = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) return trail } @@ -1153,7 +1162,7 @@ internal class RealmCryptoStore @Inject constructor( override fun saveGossipingEvents(events: List) { monarchy.writeAsync { realm -> - val now = System.currentTimeMillis() + val now = clock.epochMillis() events.forEach { event -> val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now val entity = GossipingEventEntity( @@ -1326,7 +1335,7 @@ internal class RealmCryptoStore @Inject constructor( .findAll() .mapNotNull { entity -> when (entity.type) { - GossipRequestType.KEY -> { + GossipRequestType.KEY -> { IncomingRoomKeyRequest( userId = entity.otherUserId, deviceId = entity.otherDeviceId, @@ -1359,7 +1368,7 @@ internal class RealmCryptoStore @Inject constructor( it.otherUserId = request.userId it.requestId = request.requestId ?: "" it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis() + it.localCreationTimestamp = ageLocalTS ?: clock.epochMillis() if (request is IncomingSecretShareRequest) { it.type = GossipRequestType.SECRET it.requestedInfoStr = request.secretName @@ -1380,7 +1389,7 @@ internal class RealmCryptoStore @Inject constructor( it.otherUserId = request.userId it.requestId = request.requestId ?: "" it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis() + it.localCreationTimestamp = request.localCreationTimestamp ?: clock.epochMillis() if (request is IncomingSecretShareRequest) { it.type = GossipRequestType.SECRET it.requestedInfoStr = request.secretName @@ -1536,13 +1545,16 @@ internal class RealmCryptoStore @Inject constructor( it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) } - val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + val trail = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) return trail } @@ -1707,7 +1719,7 @@ internal class RealmCryptoStore @Inject constructor( * So we need to tidy up a bit */ override fun tidyUpDataBase() { - val prevWeekTs = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1_000 + val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 doRealmTransaction(realmConfiguration) { realm -> // Only keep one week history diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index cac6499486..32f24c5d2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -33,10 +33,13 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration { +internal class RealmCryptoStoreMigration @Inject constructor( + private val clock: Clock, +) : RealmMigration { /** * Forces all RealmCryptoStoreMigration instances to be equal * Avoids Realm throwing when multiple instances of the migration are set @@ -59,7 +62,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration if (oldVersion < 5) MigrateCryptoTo005(realm).perform() if (oldVersion < 6) MigrateCryptoTo006(realm).perform() if (oldVersion < 7) MigrateCryptoTo007(realm).perform() - if (oldVersion < 8) MigrateCryptoTo008(realm).perform() + if (oldVersion < 8) MigrateCryptoTo008(realm, clock).perform() if (oldVersion < 9) MigrateCryptoTo009(realm).perform() if (oldVersion < 10) MigrateCryptoTo010(realm).perform() if (oldVersion < 11) MigrateCryptoTo011(realm).perform() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt index 785e6a04f4..ad195e6e55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt @@ -21,8 +21,12 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator +import org.matrix.android.sdk.internal.util.time.Clock -internal class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) { +internal class MigrateCryptoTo008( + realm: DynamicRealm, + private val clock: Clock, +) : RealmMigrator(realm, 8) { override fun doMigrate(realm: DynamicRealm) { realm.schema.create("MyDeviceLastSeenInfoEntity") @@ -33,7 +37,7 @@ internal class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java) .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true) - val now = System.currentTimeMillis() + val now = clock.epochMillis() realm.schema.get("DeviceInfoEntity") ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index e203f03b06..e0d912a9a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -123,7 +123,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( // val requestMessage = KeyVerificationRequest( // fromDevice = session.sessionParams.deviceId ?: "", // methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), -// timestamp = System.currentTimeMillis().toInt(), +// timestamp = clock.epochMillis().toInt(), // transactionId = transactionId // ) // diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 28bf1d70f7..d62ca5503d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -84,6 +84,7 @@ import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -104,7 +105,8 @@ internal class DefaultVerificationService @Inject constructor( private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, private val crossSigningService: CrossSigningService, private val cryptoCoroutineScope: CoroutineScope, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val clock: Clock, ) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -261,9 +263,11 @@ internal class DefaultVerificationService @Inject constructor( } override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + setDeviceVerificationAction.handle( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), userId, - deviceID) + deviceID + ) listeners.forEach { try { @@ -313,7 +317,7 @@ internal class DefaultVerificationService @Inject constructor( val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), isIncoming = true, otherUserId = senderId, // requestInfo.toUserId, roomId = null, @@ -352,7 +356,7 @@ internal class DefaultVerificationService @Inject constructor( val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), isIncoming = true, otherUserId = senderId, // requestInfo.toUserId, roomId = event.roomId, @@ -552,7 +556,8 @@ internal class DefaultVerificationService @Inject constructor( myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, - autoAccept).also { txConfigure(it) } + autoAccept + ).also { txConfigure(it) } addTransaction(tx) tx.onVerificationStart(startReq) return null @@ -644,9 +649,11 @@ internal class DefaultVerificationService @Inject constructor( if (existingRequest != null) { // Mark this request as cancelled - updatePendingRequest(existingRequest.copy( - cancelConclusion = safeValueOf(cancelReq.code) - )) + updatePendingRequest( + existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + ) + ) } existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) @@ -809,15 +816,19 @@ internal class DefaultVerificationService @Inject constructor( ?.let { vt -> val otherDeviceId = vt.otherDeviceId if (!crossSigningService.canCrossSign()) { - outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) - outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) - outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest( + MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) + ) + outgoingGossipingRequestManager.sendSecretShareRequest( + SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) + ) + outgoingGossipingRequestManager.sendSecretShareRequest( + USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) + ) } - outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest( + KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) + ) } } } @@ -917,16 +928,19 @@ internal class DefaultVerificationService @Inject constructor( qrCodeData = qrCodeData, userId = userId, deviceId = deviceId ?: "", - isIncoming = false) + isIncoming = false + ) tx.transport = transportCreator.invoke(tx) addTransaction(tx) } - updatePendingRequest(existingRequest.copy( - readyInfo = readyReq - )) + updatePendingRequest( + existingRequest.copy( + readyInfo = readyReq + ) + ) } private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { @@ -1115,7 +1129,8 @@ internal class DefaultVerificationService @Inject constructor( myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, - otherDeviceId) + otherDeviceId + ) tx.transport = verificationTransportToDeviceFactory.createTransport(tx) addTransaction(tx) @@ -1150,7 +1165,7 @@ internal class DefaultVerificationService @Inject constructor( val validLocalId = localId ?: LocalEcho.createLocalEchoId() val verificationRequest = PendingVerificationRequest( - ageLocalTs = System.currentTimeMillis(), + ageLocalTs = clock.epochMillis(), isIncoming = false, roomId = roomId, localId = validLocalId, @@ -1175,11 +1190,13 @@ internal class DefaultVerificationService @Inject constructor( transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> // We need to update with the syncedID - updatePendingRequest(verificationRequest.copy( - transactionId = syncedId, - // localId stays different - requestInfo = info - )) + updatePendingRequest( + verificationRequest.copy( + transactionId = syncedId, + // localId stays different + requestInfo = info + ) + ) } requestsForUser.add(verificationRequest) @@ -1228,7 +1245,7 @@ internal class DefaultVerificationService @Inject constructor( val verificationRequest = PendingVerificationRequest( transactionId = localId, - ageLocalTs = System.currentTimeMillis(), + ageLocalTs = clock.epochMillis(), isIncoming = false, roomId = null, localId = localId, @@ -1254,10 +1271,12 @@ internal class DefaultVerificationService @Inject constructor( transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info -> // Nothing special to do in to device mode - updatePendingRequest(verificationRequest.copy( - // localId stays different - requestInfo = info - )) + updatePendingRequest( + verificationRequest.copy( + // localId stays different + requestInfo = info + ) + ) } requestsForUser.add(verificationRequest) @@ -1271,9 +1290,11 @@ internal class DefaultVerificationService @Inject constructor( .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) getExistingVerificationRequest(otherUserId, transactionId)?.let { - updatePendingRequest(it.copy( - cancelConclusion = CancelCode.User - )) + updatePendingRequest( + it.copy( + cancelConclusion = CancelCode.User + ) + ) } } @@ -1307,7 +1328,8 @@ internal class DefaultVerificationService @Inject constructor( myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, - otherDeviceId) + otherDeviceId + ) tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) addTransaction(tx) @@ -1333,7 +1355,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId, existingRequest.requestInfo?.fromDevice ?: "", existingRequest.requestInfo?.methods, - methods) { + methods + ) { verificationTransportRoomMessageFactory.createTransport(roomId, it) } if (methods.isNullOrEmpty()) { @@ -1343,7 +1366,8 @@ internal class DefaultVerificationService @Inject constructor( } // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) - transport.sendToOther(EventType.KEY_VERIFICATION_READY, + transport.sendToOther( + EventType.KEY_VERIFICATION_READY, readyMsg, VerificationTxState.None, CancelCode.User, @@ -1372,7 +1396,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId, existingRequest.requestInfo?.fromDevice ?: "", existingRequest.requestInfo?.methods, - methods) { + methods + ) { verificationTransportToDeviceFactory.createTransport(it) } if (methods.isNullOrEmpty()) { @@ -1446,7 +1471,8 @@ internal class DefaultVerificationService @Inject constructor( qrCodeData = qrCodeData, userId = userId, deviceId = deviceId ?: "", - isIncoming = false) + isIncoming = false + ) tx.transport = transportCreator.invoke(tx) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index 52166761ab..ec4e1aa65c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -34,11 +34,13 @@ import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( private val eventDecryptor: EventDecryptor, + private val clock: Clock, private val verificationService: DefaultVerificationService, @UserId private val userId: String, @DeviceId private val deviceId: String? @@ -71,8 +73,7 @@ internal class VerificationMessageProcessor @Inject constructor( // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. - if (!VerificationService.isValidRequest(event.ageLocalTs - ?: event.originServerTs)) return Unit.also { + if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also { Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index 49235c5744..325a6f0ba2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_REC import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber @@ -61,7 +62,8 @@ internal class VerificationTransportRoomMessage( private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val tx: DefaultVerificationTransaction?, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val clock: Clock, ) : VerificationTransport { override fun sendToOther(type: String, @@ -77,10 +79,12 @@ internal class VerificationTransportRoomMessage( content = verificationInfo.toEventContent()!! ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) + val workerParams = WorkerParamsFactory.toData( + SendVerificationMessageWorker.Params( + sessionId = sessionId, + eventId = event.eventId ?: "" + ) + ) val enqueueInfo = enqueueSendWork(workerParams) // I cannot just listen to the given work request, because when used in a uniqueWork, @@ -155,7 +159,7 @@ internal class VerificationTransportRoomMessage( transactionId = "", fromDevice = userDeviceId ?: "", methods = supportedMethods, - timestamp = System.currentTimeMillis() + timestamp = clock.epochMillis() ) val info = MessageVerificationRequestContent( @@ -175,10 +179,12 @@ internal class VerificationTransportRoomMessage( content ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) + val workerParams = WorkerParamsFactory.toData( + SendVerificationMessageWorker.Params( + sessionId = sessionId, + eventId = event.eventId ?: "" + ) + ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) @@ -230,10 +236,12 @@ internal class VerificationTransportRoomMessage( roomId = roomId, content = MessageVerificationCancelContent.create(transactionId, code).toContent() ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) + val workerParams = WorkerParamsFactory.toData( + SendVerificationMessageWorker.Params( + sessionId = sessionId, + eventId = event.eventId ?: "" + ) + ) enqueueSendWork(workerParams) } @@ -250,10 +258,12 @@ internal class VerificationTransportRoomMessage( ) ).toContent() ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) + val workerParams = WorkerParamsFactory.toData( + SendVerificationMessageWorker.Params( + sessionId = sessionId, + eventId = event.eventId ?: "" + ) + ) val enqueueInfo = enqueueSendWork(workerParams) val workLiveData = workManagerProvider.workManager @@ -361,7 +371,7 @@ internal class VerificationTransportRoomMessage( private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, - originServerTs = System.currentTimeMillis(), + originServerTs = clock.epochMillis(), senderId = userId, eventId = localId, type = type, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt index f89127273b..b1b7ad7a98 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportRoomMessageFactory @Inject constructor( @@ -33,17 +34,21 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor( @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val clock: Clock, ) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { - return VerificationTransportRoomMessage(workManagerProvider, + return VerificationTransportRoomMessage( + workManagerProvider, sessionId, userId, deviceId, roomId, localEchoEventFactory, tx, - taskExecutor.executorScope) + taskExecutor.executorScope, + clock + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt index 40deda2745..bc24ef2966 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -35,13 +35,16 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber +// TODO var could be val internal class VerificationTransportToDevice( private var tx: DefaultVerificationTransaction?, private var sendToDeviceTask: SendToDeviceTask, private val myDeviceId: String?, - private var taskExecutor: TaskExecutor + private var taskExecutor: TaskExecutor, + private val clock: Clock, ) : VerificationTransport { override fun sendVerificationRequest(supportedMethods: List, @@ -56,7 +59,7 @@ internal class VerificationTransportToDevice( transactionId = localId, fromDevice = myDeviceId ?: "", methods = supportedMethods, - timestamp = System.currentTimeMillis() + timestamp = clock.epochMillis() ) val keyReq = KeyVerificationRequest( fromDevice = validKeyReq.fromDevice, @@ -201,7 +204,8 @@ internal class VerificationTransportToDevice( hash, commitment, messageAuthenticationCode, - shortAuthenticationStrings) + shortAuthenticationStrings + ) override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) @@ -221,7 +225,8 @@ internal class VerificationTransportToDevice( hashes, messageAuthenticationCodes, shortAuthenticationStrings, - null) + null + ) } override fun createStartForQrCode(fromDevice: String, @@ -235,7 +240,8 @@ internal class VerificationTransportToDevice( null, null, null, - sharedSecret) + sharedSecret + ) } override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt index e9a2c65ef7..312d911822 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt @@ -19,14 +19,17 @@ package org.matrix.android.sdk.internal.crypto.verification import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportToDeviceFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, @DeviceId val myDeviceId: String?, - private val taskExecutor: TaskExecutor) { + private val taskExecutor: TaskExecutor, + private val clock: Clock, +) { fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { - return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor) + return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor, clock) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index 315d77d932..7d263f1937 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import kotlin.system.measureTimeMillis internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { asyncTransaction(monarchy.realmConfiguration, transaction) @@ -41,13 +42,13 @@ internal suspend fun awaitTransaction(config: RealmConfiguration, transactio bgRealm.beginTransaction() val result: T try { - val start = System.currentTimeMillis() - result = transaction(bgRealm) - if (isActive) { - bgRealm.commitTransaction() - val end = System.currentTimeMillis() - val time = end - start - Timber.v("Execute transaction in $time millis") + measureTimeMillis { + result = transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + } + }.also { + Timber.v("Execute transaction in $it millis") } } finally { if (bgRealm.isInTransaction) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 89474a1bc5..d052a7dea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -137,7 +137,8 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( roomMemberContentsByUser: HashMap, roomEntity: RoomEntity, userId: String, - cryptoService: CryptoService? = null + cryptoService: CryptoService? = null, + currentTimeMillis: Long, ) { when (threadSummaryType) { ThreadSummaryUpdateType.REPLACE -> { @@ -153,14 +154,14 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") } - val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also { + val rootThreadEventEntity = createEventEntity(realm, roomId, rootThreadEvent, currentTimeMillis).also { try { decryptIfNeeded(cryptoService, it, roomId) } catch (e: InterruptedException) { Timber.i("Decryption got interrupted") } } - val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also { + val latestThreadEventEntity = createLatestEventEntity(realm, roomId, rootThreadEvent, roomMemberContentsByUser, currentTimeMillis)?.also { try { decryptIfNeeded(cryptoService, it, roomId) } catch (e: InterruptedException) { @@ -268,8 +269,8 @@ private fun HashMap.addSenderState(realm: Realm, roo /** * Create an EventEntity for the root thread event or get an existing one */ -private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { - val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } +private fun createEventEntity(realm: Realm, roomId: String, event: Event, currentTimeMillis: Long): EventEntity { + val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it } return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } @@ -278,15 +279,17 @@ private fun createEventEntity(roomId: String, event: Event, realm: Realm): Event * state */ private fun createLatestEventEntity( + realm: Realm, roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap, - realm: Realm): EventEntity? { + currentTimeMillis: Long, +): EventEntity? { return getLatestEvent(rootThreadEvent)?.let { it.senderId?.let { senderId -> roomMemberContentsByUser.addSenderState(realm, roomId, senderId) } - createEventEntity(roomId, it, realm) + createEventEntity(realm, roomId, it, currentTimeMillis) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 3083df062e..bc7d40bf6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -30,13 +30,14 @@ import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber +import kotlin.random.Random internal object EventMapper { fun map(event: Event, roomId: String): EventEntity { val eventEntity = EventEntity() // TODO change this as we shouldn't use event everywhere - eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" + eventEntity.eventId = event.eventId ?: "$$roomId-${Random.nextLong()}-${event.hashCode()}" eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent()) @@ -126,7 +127,10 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event { return EventMapper.map(this, castJsonNumbers) } -internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity { +internal fun Event.toEntity(roomId: String, + sendState: SendState, + ageLocalTs: Long?, + contentToInject: String? = null): EventEntity { return EventMapper.map(this, roomId).apply { this.sendState = sendState this.ageLocalTs = ageLocalTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 9b83cca558..78f1c84f3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.util.file.AtomicFileCreator +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.writeToFile import timber.log.Timber import java.io.File @@ -51,7 +52,8 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @UnauthenticatedWithCertificateWithProgress private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, ) : FileService { // Legacy folder, will be deleted @@ -182,7 +184,8 @@ internal class DefaultFileService @Inject constructor( MXEncryptedAttachments.decryptAttachment( inputStream, elementToDecrypt, - outputStream + outputStream, + clock ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 59058bf976..c4f711a9e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerConten import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -41,9 +42,12 @@ private val loggerTag = LoggerTag("CallSignalingHandler", LoggerTag.VOIP) private const val MAX_AGE_TO_RING = 40_000 @SessionScope -internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler, - private val mxCallFactory: MxCallFactory, - @UserId private val userId: String) { +internal class CallSignalingHandler @Inject constructor( + private val activeCallHandler: ActiveCallHandler, + private val mxCallFactory: MxCallFactory, + @UserId private val userId: String, + private val clock: Clock, +) { private val invitedCallIds = mutableSetOf() private val callListeners = mutableSetOf() @@ -184,7 +188,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa if (event.roomId == null || event.senderId == null) { return } - val now = System.currentTimeMillis() + val now = clock.epochMillis() val age = now - (event.ageLocalTs ?: now) if (age > MAX_AGE_TO_RING) { Timber.tag(loggerTag.value).w("Call invite is too old to ring.") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index 547be2253f..9ec892b65d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MxCallFactory @Inject constructor( @@ -36,7 +37,8 @@ internal class MxCallFactory @Inject constructor( private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, private val getProfileInfoTask: GetProfileInfoTask, - @UserId private val userId: String + @UserId private val userId: String, + private val clock: Clock, ) { fun createIncomingCall(roomId: String, opponentUserId: String, content: CallInviteContent): MxCall? { @@ -51,7 +53,8 @@ internal class MxCallFactory @Inject constructor( localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, - getProfileInfoTask = getProfileInfoTask + getProfileInfoTask = getProfileInfoTask, + clock = clock, ).apply { updateOpponentData(opponentUserId, content, content.capabilities) } @@ -68,7 +71,8 @@ internal class MxCallFactory @Inject constructor( localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, - getProfileInfoTask = getProfileInfoTask + getProfileInfoTask = getProfileInfoTask, + clock = clock, ).apply { // Setup with this userId, might be updated when processing the Answer event this.opponentUserId = opponentUserId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index a89713870a..796e83311f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.math.BigDecimal @@ -61,7 +62,8 @@ internal class MxCallImpl( private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, - private val getProfileInfoTask: GetProfileInfoTask + private val getProfileInfoTask: GetProfileInfoTask, + private val clock: Clock, ) : MxCall { override var opponentPartyId: Optional? = null @@ -250,7 +252,7 @@ internal class MxCallImpl( private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, - originServerTs = System.currentTimeMillis(), + originServerTs = clock.epochMillis(), senderId = userId, eventId = localId, type = type, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 75606f2e7a..75a79abcdb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.util.TemporaryFileCreator +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams @@ -87,6 +88,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var thumbnailExtractor: ThumbnailExtractor @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var temporaryFileCreator: TemporaryFileCreator + @Inject lateinit var clock: Clock override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -243,7 +245,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter .also { filesToDelete.add(it) } uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), encryptedFile) { read, total -> + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), encryptedFile, clock) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -329,7 +331,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter if (params.isEncrypted) { Timber.v("Encrypt thumbnail") notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream()) + val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), clock) val contentUploadResponse = fileUploader.uploadByteArray( byteArray = encryptionResult.encryptedByteArray, filename = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt index da7e2d102e..5aaf058757 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPub import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -46,7 +47,8 @@ internal class DefaultContentScannerService @Inject constructor( private val getServerPublicKeyTask: GetServerPublicKeyTask, private val scanEncryptedTask: ScanEncryptedTask, private val scanMediaTask: ScanMediaTask, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val clock: Clock, ) : ContentScannerService { // Cache public key in memory @@ -71,11 +73,13 @@ internal class DefaultContentScannerService @Inject constructor( override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo { val result = if (fileInfo != null) { - scanEncryptedTask.execute(ScanEncryptedTask.Params( - mxcUrl = mxcUrl, - publicServerKey = getServerPublicKey(false), - encryptedInfo = fileInfo - )) + scanEncryptedTask.execute( + ScanEncryptedTask.Params( + mxcUrl = mxcUrl, + publicServerKey = getServerPublicKey(false), + encryptedInfo = fileInfo + ) + ) } else { scanMediaTask.execute(ScanMediaTask.Params(mxcUrl)) } @@ -83,7 +87,7 @@ internal class DefaultContentScannerService @Inject constructor( return ScanStatusInfo( state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED, humanReadableMessage = result.info, - scanDateTimestamp = System.currentTimeMillis() + scanDateTimestamp = clock.epochMillis() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt index b47be235c6..e4b64a1a0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt @@ -31,11 +31,14 @@ internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: .findFirst() } -internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity { +internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, + attachmentUrl: String, + contentScannerUrl: String?, + currentTimeMillis: Long): ContentScanResultEntity { return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl) ?: realm.createObject().also { it.mediaUrl = attachmentUrl - it.scanDateTimestamp = System.currentTimeMillis() + it.scanDateTimestamp = currentTimeMillis it.scannerUrl = contentScannerUrl } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt index 947a66c8b9..27729d38c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt @@ -32,12 +32,14 @@ import org.matrix.android.sdk.internal.di.ContentScannerDatabase import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore import org.matrix.android.sdk.internal.util.isValidUrl +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @SessionScope internal class RealmContentScannerStore @Inject constructor( @ContentScannerDatabase - private val realmConfiguration: RealmConfiguration + private val realmConfiguration: RealmConfiguration, + private val clock: Clock, ) : ContentScannerStore { private val monarchy = Monarchy.Builder() @@ -82,15 +84,15 @@ internal class RealmContentScannerStore @Inject constructor( override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) { monarchy.runTransactionSync { - ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state + ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl, clock.epochMillis()).scanResult = state } } override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) { monarchy.runTransactionSync { - ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply { + ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl, clock.epochMillis()).apply { scanResult = state - scanDateTimestamp = System.currentTimeMillis() + scanDateTimestamp = clock.epochMillis() humanReadableMessage = humanReadable } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index a8158d9bef..7e0b44a314 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -69,6 +69,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -77,7 +78,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val stateEventDataSource: StateEventDataSource, @SessionId private val sessionId: String, private val sessionManager: SessionManager, - private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor + private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, + private val clock: Clock, ) : EventInsertLiveProcessor { private val allowedTypes = listOf( @@ -338,7 +340,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.editions.firstOrNull { it.eventId == txId }?.let { it.eventId = event.eventId - it.timestamp = event.originServerTs ?: System.currentTimeMillis() + it.timestamp = event.originServerTs ?: clock.epochMillis() it.isLocalEcho = false } } else { @@ -349,10 +351,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( eventId = event.eventId, content = ContentMapper.map(newContent), timestamp = if (isLocalEcho) { - System.currentTimeMillis() + clock.epochMillis() } else { // Do not take local echo originServerTs here, could mess up ordering (keep old ts) - event.originServerTs ?: System.currentTimeMillis() + event.originServerTs ?: clock.epochMillis() }, isLocalEcho = isLocalEcho ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 9bd15a0267..6dd2c91048 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelpe import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -56,7 +57,8 @@ internal class DefaultCreateRoomTask @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, private val createRoomBodyBuilder: CreateRoomBodyBuilder, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : CreateRoomTask { override suspend fun execute(params: CreateRoomParams): String { @@ -106,7 +108,7 @@ internal class DefaultCreateRoomTask @Inject constructor( } awaitTransaction(realmConfiguration) { - RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis() } if (otherUserId != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 70ba9287a2..d3d1cb856a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -61,7 +62,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( private val roomMemberEventHandler: RoomMemberEventHandler, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val deviceListManager: DeviceListManager, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -107,7 +109,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) - val now = System.currentTimeMillis() + val now = clock.epochMillis() for (roomMemberEvent in response.roomMemberEvents) { if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) { continue diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index f883cc33ec..6306f3c6fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -53,7 +54,8 @@ internal class DefaultJoinRoomTask @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : JoinRoomTask { override suspend fun execute(params: JoinRoomTask.Params) { @@ -90,7 +92,7 @@ internal class DefaultJoinRoomTask @Inject constructor( throw JoinRoomFailure.JoinedWithTimeout } awaitTransaction(realmConfiguration) { - RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis() } setReadMarkers(roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index 64f1cc34f1..a124a8a4c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHand import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject import kotlin.collections.set @@ -58,7 +59,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor( private val roomFullyReadHandler: RoomFullyReadHandler, private val readReceiptHandler: ReadReceiptHandler, @UserId private val userId: String, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -125,7 +127,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) } if (readReceiptId != null) { - val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis()) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null) } if (shouldUpdateRoomSummary) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index b54cd71e50..7bf7d6b587 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -27,12 +27,16 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor, - private val eventFactory: LocalEchoEventFactory, - private val localEchoRepository: LocalEchoRepository) { +internal class EventEditor @Inject constructor( + private val eventSenderProcessor: EventSenderProcessor, + private val eventFactory: LocalEchoEventFactory, + private val localEchoRepository: LocalEchoRepository, + private val clock: Clock, +) { fun editTextMessage(targetEvent: TimelineEvent, msgType: String, @@ -126,7 +130,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: } private fun updateFailedEchoWithEvent(roomId: String, failedEchoEventId: String, editedEvent: Event) { - val editedEventEntity = editedEvent.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val editedEventEntity = editedEvent.toEntity(roomId, SendState.UNSENT, clock.epochMillis()) localEchoRepository.updateEchoAsync(failedEchoEventId) { _, entity -> entity.content = editedEventEntity.content entity.ageLocalTs = editedEventEntity.ageLocalTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index b596f2288e..c5f9bd13fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -55,12 +56,14 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val cryptoService: DefaultCryptoService, @UserId private val userId: String, + private val clock: Clock, ) : FetchThreadSummariesTask { override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { val filter = FilterFactory.createThreadsFilter( numberOfEvents = params.limit, - userId = if (params.isUserParticipating) userId else null).toJSONString() + userId = if (params.isUserParticipating) userId else null + ).toJSONString() val response = executeRequest( globalErrorReceiver, @@ -94,7 +97,9 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( roomMemberContentsByUser = roomMemberContentsByUser, roomEntity = roomEntity, userId = userId, - cryptoService = cryptoService) + cryptoService = cryptoService, + currentTimeMillis = clock.epochMillis(), + ) } } return Result.SUCCESS diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 116d4aa0a1..8d35a8fea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -42,7 +41,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent @@ -51,6 +49,7 @@ import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -85,10 +84,9 @@ internal interface FetchThreadTimelineTask : Task - val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val eventEntity = event.toEntity(roomId, SendState.UNSENT, clock.epochMillis()) val roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) val localId = UUID.randomUUID().mostSignificantBits @@ -88,7 +92,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) { - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") + Timber.v("## SendEvent: [${clock.epochMillis()}] Update local state of $eventId to ${sendState.name}") timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState) updateEchoAsync(eventId) { realm, sendingEventEntity -> if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index a54ce833d4..ecc8149255 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -77,7 +77,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo val roomId = localEchoIds.roomId val eventId = localEchoIds.eventId localEchoRepository.updateSendState(eventId, roomId, SendState.SENDING) - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId") + Timber.v("## SendEvent: Schedule send event $eventId") val sendWork = createSendEventWork(params.sessionId, eventId, true) timelineSendEventWorkCommon.postWork(roomId, sendWork) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 669891c822..ddbe8a61a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -89,13 +89,13 @@ internal class SendEventWorker(context: Context, params: WorkerParameters, sessi .also { Timber.e("Work cancelled due to input error from parent") } } - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") + Timber.v("## SendEvent: Send event ${params.eventId}") return try { sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId))) Result.success() } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { - Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") + Timber.e("## SendEvent: Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") localEchoRepository.updateSendState( eventId = event.eventId, roomId = event.roomId, @@ -104,7 +104,7 @@ internal class SendEventWorker(context: Context, params: WorkerParameters, sessi ) Result.success() } else { - Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") + Timber.e("## SendEvent: Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Result.retry() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 5b4efa5df6..a1d3e2c0ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -191,7 +191,7 @@ internal class EventSenderProcessorCoroutine @Inject constructor( private suspend fun QueuedTask.waitForNetwork() = waitForNetworkSequencer.post { while (!canReachServer.get()) { - Timber.v("## $this cannot reach server wait ts:${System.currentTimeMillis()}") + Timber.v("## $this cannot reach server wait for $$RETRY_WAIT_TIME_MS ms") delay(RETRY_WAIT_TIME_MS) withContext(Dispatchers.IO) { val hostAvailable = HomeServerAvailabilityChecker(sessionParams).check() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt index 1ee3139194..301f8cb9d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt @@ -136,7 +136,7 @@ internal class EventSenderProcessorThread @Inject constructor( private var retryNoNetworkTask: TimerTask? = null override fun run() { - Timber.v("## SendThread started ts:${System.currentTimeMillis()}") + Timber.v("## SendThread started") try { while (!isInterrupted) { Timber.v("## SendThread wait for task to process") @@ -151,7 +151,7 @@ internal class EventSenderProcessorThread @Inject constructor( } // we check for network connectivity while (!canReachServer) { - Timber.v("## SendThread cannot reach server, wait ts:${System.currentTimeMillis()}") + Timber.v("## SendThread cannot reach server") // schedule to retry waitForNetwork() // if thread as been killed meanwhile @@ -175,7 +175,7 @@ internal class EventSenderProcessorThread @Inject constructor( canReachServer = false if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() while (!canReachServer) { - Timber.v("## SendThread retryLoop cannot reach server, wait ts:${System.currentTimeMillis()}") + Timber.v("## SendThread retryLoop cannot reach server") // schedule to retry waitForNetwork() } @@ -218,7 +218,7 @@ internal class EventSenderProcessorThread @Inject constructor( // state = State.KILLED // is this needed? retryNoNetworkTask?.cancel() - Timber.w("## SendThread finished ${System.currentTimeMillis()}") + Timber.w("## SendThread finished") } private fun waitForNetwork() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 08b2700a43..18fbe73c09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -43,28 +43,32 @@ import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHand import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.createBackgroundHandler +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -internal class DefaultTimeline(private val roomId: String, - private val initialEventId: String?, - private val realmConfiguration: RealmConfiguration, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val readReceiptHandler: ReadReceiptHandler, - private val settings: TimelineSettings, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - paginationTask: PaginationTask, - getEventTask: GetContextOfEventTask, - fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - fetchThreadTimelineTask: FetchThreadTimelineTask, - timelineEventMapper: TimelineEventMapper, - timelineInput: TimelineInput, - threadsAwarenessHandler: ThreadsAwarenessHandler, - lightweightSettingsStorage: LightweightSettingsStorage, - eventDecryptor: TimelineEventDecryptor) : Timeline { +internal class DefaultTimeline( + private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + private val settings: TimelineSettings, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + fetchThreadTimelineTask: FetchThreadTimelineTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + threadsAwarenessHandler: ThreadsAwarenessHandler, + lightweightSettingsStorage: LightweightSettingsStorage, + eventDecryptor: TimelineEventDecryptor, +) : Timeline { companion object { val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread") @@ -370,7 +374,8 @@ internal class DefaultTimeline(private val roomId: String, roomId = roomId, timelineId = timelineID, mode = mode, - dependencies = strategyDependencies + dependencies = strategyDependencies, + clock = clock, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 826c9d7c48..849d7bd7ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTa import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import org.matrix.android.sdk.internal.util.time.Clock internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, @@ -50,8 +51,9 @@ internal class DefaultTimelineService @AssistedInject constructor( private val lightweightSettingsStorage: LightweightSettingsStorage, private val readReceiptHandler: ReadReceiptHandler, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val timelineEventDataSource: TimelineEventDataSource -) : TimelineService { + private val timelineEventDataSource: TimelineEventDataSource, + private val clock: Clock, + ) : TimelineService { @AssistedFactory interface Factory { @@ -75,7 +77,8 @@ internal class DefaultTimelineService @AssistedInject constructor( readReceiptHandler = readReceiptHandler, getEventTask = contextOfEventTask, threadsAwarenessHandler = threadsAwarenessHandler, - lightweightSettingsStorage = lightweightSettingsStorage + lightweightSettingsStorage = lightweightSettingsStorage, + clock = clock ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index 5bca5118b8..aef9e24c8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal interface GetEventTask : Task { @@ -36,7 +37,8 @@ internal interface GetEventTask : Task { internal class DefaultGetEventTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, - private val eventDecryptor: EventDecryptor + private val eventDecryptor: EventDecryptor, + private val clock: Clock, ) : GetEventTask { override suspend fun execute(params: GetEventTask.Params): Event { @@ -59,7 +61,7 @@ internal class DefaultGetEventTask @Inject constructor( } } - event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + event.ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } return event } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 8819ffe69f..bcf202962c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThre import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.concurrent.atomic.AtomicReference @@ -53,11 +54,13 @@ import java.util.concurrent.atomic.AtomicReference * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. */ -internal class LoadTimelineStrategy( +internal class LoadTimelineStrategy constructor( private val roomId: String, private val timelineId: String, private val mode: Mode, - private val dependencies: Dependencies) { + private val dependencies: Dependencies, + clock: Clock, +) { sealed interface Mode { object Live : Mode @@ -153,7 +156,7 @@ internal class LoadTimelineStrategy( } } - private val uiEchoManager = UIEchoManager(uiEchoManagerListener) + private val uiEchoManager = UIEchoManager(uiEchoManagerListener, clock) private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( roomId = roomId, realm = dependencies.realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index d3f24a8568..e5dd8aab30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -54,7 +55,9 @@ internal class TokenChunkEventPersistor @Inject constructor( @SessionDatabase private val monarchy: Monarchy, @UserId private val userId: String, private val lightweightSettingsStorage: LightweightSettingsStorage, - private val liveEventManager: Lazy) { + private val liveEventManager: Lazy, + private val clock: Clock, +) { enum class Result { SHOULD_FETCH_MORE, @@ -166,7 +169,7 @@ internal class TokenChunkEventPersistor @Inject constructor( val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents - val now = System.currentTimeMillis() + val now = clock.epochMillis() stateEvents?.forEach { stateEvent -> val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index f3218173d6..828e01955a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -24,10 +24,14 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.Collections -internal class UIEchoManager(private val listener: Listener) { +internal class UIEchoManager( + private val listener: Listener, + private val clock: Clock, +) { interface Listener { fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean @@ -109,7 +113,7 @@ internal class UIEchoManager(private val listener: Listener) { key = uiEchoReaction.reaction, count = 1, addedByMe = true, - firstTimestamp = System.currentTimeMillis(), + firstTimestamp = clock.epochMillis(), sourceEvents = emptyList(), localEchoEvents = listOf(uiEchoReaction.localEchoId) ).let { updateReactions.add(it) } @@ -143,7 +147,7 @@ internal class UIEchoManager(private val listener: Listener) { fun updateSentStateWithUiEcho(timelineEvent: TimelineEvent): TimelineEvent { if (timelineEvent.root.sendState.isSent()) return timelineEvent val inMemoryState = inMemorySendingStates[timelineEvent.eventId] ?: return timelineEvent - // Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state $inMemoryState from state ${element.root.sendState}") + // Timber.v("## ${clock.epochMillis()} Send event refresh echo with live state $inMemoryState from state ${element.root.sendState}") return timelineEvent.copy( root = timelineEvent.root.copyAll() .also { it.sendState = inMemoryState } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt index bd20ada28b..8e0c3422b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.JsonClass import okio.buffer import okio.source import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.io.File @@ -46,7 +47,10 @@ internal interface InitialSyncStatusRepository { /** * This class handle the current status of an initial sync and persist it on the disk, to be robust against crash */ -internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncStatusRepository { +internal class FileInitialSyncStatusRepository( + directory: File, + private val clock: Clock, +) : InitialSyncStatusRepository { companion object { // After 2 hours, we consider that the downloaded file is outdated: @@ -64,7 +68,7 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta ensureCache() val state = cache?.step ?: InitialSyncStatus.STEP_INIT return if (state >= InitialSyncStatus.STEP_DOWNLOADED && - System.currentTimeMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { + clock.epochMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { Timber.d("INIT_SYNC downloaded file is outdated, download it again") // The downloaded file is outdated setStep(InitialSyncStatus.STEP_INIT) @@ -79,7 +83,7 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta if (step == InitialSyncStatus.STEP_DOWNLOADED) { // Also store the downloaded date newStatus = newStatus.copy( - downloadedDate = System.currentTimeMillis() + downloadedDate = clock.epochMillis() ) } cache = newStatus diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 197037f1cf..f88d973101 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseP import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.logDuration +import org.matrix.android.sdk.internal.util.time.Clock import retrofit2.Response import retrofit2.awaitResponse import timber.log.Timber @@ -78,11 +79,12 @@ internal class DefaultSyncTask @Inject constructor( @SessionFilesDirectory private val fileDirectory: File, private val syncResponseParser: InitialSyncResponseParser, - private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore + private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, + private val clock: Clock, ) : SyncTask { private val workingDir = File(fileDirectory, "is") - private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir) + private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir, clock) override suspend fun execute(params: SyncTask.Params): SyncResponse { return syncTaskSequencer.post { @@ -111,7 +113,8 @@ internal class DefaultSyncTask @Inject constructor( userStore.createOrUpdate( userId = userId, displayName = user?.displayName, - avatarUrl = user?.avatarUrl) + avatarUrl = user?.avatarUrl + ) defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100) } // Maybe refresh the homeserver capabilities data we know @@ -124,7 +127,7 @@ internal class DefaultSyncTask @Inject constructor( if (isInitialSync) { Timber.tag(loggerTag.value).d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy - logDuration("INIT_SYNC strategy: $initSyncStrategy", loggerTag) { + logDuration("INIT_SYNC strategy: $initSyncStrategy", loggerTag, clock) { if (initSyncStrategy is InitialSyncStrategy.Optimized) { roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() @@ -135,7 +138,7 @@ internal class DefaultSyncTask @Inject constructor( // Delete all files workingDir.deleteRecursively() } else { - val syncResponse = logDuration("INIT_SYNC Request", loggerTag) { + val syncResponse = logDuration("INIT_SYNC Request", loggerTag, clock) { executeRequest(globalErrorReceiver) { syncAPI.sync( params = requestParams, @@ -146,7 +149,7 @@ internal class DefaultSyncTask @Inject constructor( // We cannot distinguish request and download in this case. syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() syncStatisticsData.downloadInitSyncTime = syncStatisticsData.requestInitSyncTime - logDuration("INIT_SYNC Database insertion", loggerTag) { + logDuration("INIT_SYNC Database insertion", loggerTag, clock) { syncResponseHandler.handleResponse(syncResponse, token, defaultSyncStatusService) } syncResponseToReturn = syncResponse @@ -174,10 +177,12 @@ internal class DefaultSyncTask @Inject constructor( Timber.tag(loggerTag.value).d( "Incremental sync request parsing, $nbRooms room(s) $nbToDevice toDevice(s). Got nextBatch: $nextBatch" ) - defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncParsing( - rooms = nbRooms, - toDevice = nbToDevice - )) + defaultSyncStatusService.setStatus( + SyncStatusService.Status.IncrementalSyncParsing( + rooms = nbRooms, + toDevice = nbToDevice + ) + ) syncResponseHandler.handleResponse(syncResponse, token, null) syncResponseToReturn = syncResponse Timber.tag(loggerTag.value).d("Incremental sync done") @@ -201,14 +206,14 @@ internal class DefaultSyncTask @Inject constructor( } } else { initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADING) - val syncResponse = logDuration("INIT_SYNC Perform server request", loggerTag) { + val syncResponse = logDuration("INIT_SYNC Perform server request", loggerTag, clock) { reportSubtask(defaultSyncStatusService, InitSyncStep.ServerComputing, 1, 0.2f) { getSyncResponse(requestParams, MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT) } } syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() if (syncResponse.isSuccessful) { - logDuration("INIT_SYNC Download and save to file", loggerTag) { + logDuration("INIT_SYNC Download and save to file", loggerTag, clock) { reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.1f) { syncResponse.body()?.byteStream()?.use { inputStream -> workingFile.outputStream().use { outputStream -> @@ -247,8 +252,8 @@ internal class DefaultSyncTask @Inject constructor( } private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse { - return logDuration("INIT_SYNC handleSyncFile()", loggerTag) { - val syncResponse = logDuration("INIT_SYNC Read file and parse", loggerTag) { + return logDuration("INIT_SYNC handleSyncFile()", loggerTag, clock) { + val syncResponse = logDuration("INIT_SYNC Read file and parse", loggerTag, clock) { syncResponseParser.parse(initSyncStrategy, workingFile) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_PARSED) @@ -257,7 +262,7 @@ internal class DefaultSyncTask @Inject constructor( val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it.ephemeral is LazyRoomSyncEphemeral.Stored } Timber.tag(loggerTag.value).d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files") - logDuration("INIT_SYNC Database insertion", loggerTag) { + logDuration("INIT_SYNC Database insertion", loggerTag, clock) { syncResponseHandler.handleResponse(syncResponse, null, defaultSyncStatusService) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index 2c84f1e692..77bee18df9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -44,12 +44,14 @@ internal class ReadReceiptHandler @Inject constructor( companion object { - fun createContent(userId: String, eventId: String): ReadReceiptContent { + fun createContent(userId: String, + eventId: String, + currentTimeMillis: Long): ReadReceiptContent { return mapOf( eventId to mapOf( READ_KEY to mapOf( userId to mapOf( - TIMESTAMP_KEY to System.currentTimeMillis().toDouble() + TIMESTAMP_KEY to currentTimeMillis.toDouble() ) ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 05dad983da..5437a015fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -79,22 +79,26 @@ import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.util.computeBestChunkSize +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, - private val roomSummaryUpdater: RoomSummaryUpdater, - private val roomAccountDataHandler: RoomSyncAccountDataHandler, - private val cryptoService: DefaultCryptoService, - private val roomMemberEventHandler: RoomMemberEventHandler, - private val roomTypingUsersHandler: RoomTypingUsersHandler, - private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - @UserId private val userId: String, - private val homeServerCapabilitiesService: HomeServerCapabilitiesService, - private val lightweightSettingsStorage: LightweightSettingsStorage, - private val timelineInput: TimelineInput, - private val liveEventService: Lazy) { +internal class RoomSyncHandler @Inject constructor( + private val readReceiptHandler: ReadReceiptHandler, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomAccountDataHandler: RoomSyncAccountDataHandler, + private val cryptoService: DefaultCryptoService, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + @UserId private val userId: String, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val lightweightSettingsStorage: LightweightSettingsStorage, + private val timelineInput: TimelineInput, + private val liveEventService: Lazy, + private val clock: Clock, +) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -130,7 +134,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } else { EventInsertType.INCREMENTAL_SYNC } - val syncLocalTimeStampMillis = System.currentTimeMillis() + val syncLocalTimeStampMillis = clock.epochMillis() val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> { if (isInitialSync && initialSyncStrategy is InitialSyncStrategy.Optimized) { @@ -440,7 +444,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle threadEventEntity = eventEntity, roomMemberContentsByUser = roomMemberContentsByUser, userId = userId, - roomEntity = roomEntity + roomEntity = roomEntity, + currentTimeMillis = clock.epochMillis(), ) } } ?: run { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt index 6fd907d397..ffad0b856c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.util import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber internal fun Collection.logLimit(maxQuantity: Int = 5): String { @@ -34,13 +35,14 @@ internal fun Collection.logLimit(maxQuantity: Int = 5): String { internal suspend fun logDuration(message: String, loggerTag: LoggerTag, + clock: Clock, block: suspend () -> T): T { Timber.tag(loggerTag.value).d("$message -- BEGIN") - val start = System.currentTimeMillis() + val start = clock.epochMillis() val result = logRamUsage(message) { block() } - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.tag(loggerTag.value).d("$message -- END duration: $duration ms") return result diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt index 8a7b50175a..396d12f369 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -18,10 +18,15 @@ package org.matrix.android.sdk.internal.util.system import dagger.Binds import dagger.Module +import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.android.sdk.internal.util.time.DefaultClock @Module internal abstract class SystemModule { @Binds abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider + + @Binds + abstract fun bindClock(clock: DefaultClock): Clock } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt new file mode 100644 index 0000000000..4fe9069b49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util.time + +import javax.inject.Inject + +internal interface Clock { + fun epochMillis(): Long +} + +internal class DefaultClock @Inject constructor() : Clock { + + /** + * Provides a UTC epoch in milliseconds + * + * This value is not guaranteed to be correct with reality + * as a User can override the system time and date to any values. + */ + override fun epochMillis(): Long { + return System.currentTimeMillis() + } +} diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 21ab0bab77..393e942b2a 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -173,4 +173,7 @@ PreferenceManager\.getDefaultSharedPreferences==2 # findViewById ### Do not use `template_` string. Please remove the prefix `template_` to use the generated resource instead. -R\.string\.template_ \ No newline at end of file +R\.string\.template_ + +### Use the Clock interface, or use `measureTimeMillis` +System\.currentTimeMillis\(\)===2 diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 35554ae75a..efb28cdff5 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -43,6 +43,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.time.DefaultClock import im.vector.app.espresso.tools.waitUntilViewVisible import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -52,6 +53,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.util.Optional import java.util.concurrent.TimeoutException +import kotlin.random.Random object EspressoHelper { fun getCurrentActivity(): Activity? { @@ -89,6 +91,8 @@ fun getString(@StringRes id: Int): String { fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { return object : ViewAction { + private val clock = DefaultClock() + override fun getConstraints(): Matcher { return Matchers.any(View::class.java) } @@ -102,14 +106,14 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDispl override fun perform(uiController: UiController, view: View) { println("*** waitForView 1 $view") uiController.loopMainThreadUntilIdle() - val startTime = System.currentTimeMillis() + val startTime = clock.epochMillis() val endTime = startTime + timeout val visibleMatcher = isDisplayed() uiController.loopMainThreadForAtLeast(100) do { - println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}") + println("*** waitForView loop $view end:$endTime current:${clock.epochMillis()}") val viewVisible = TreeIterables.breadthFirstViewTraversal(view) .any { viewMatcher.matches(it) && visibleMatcher.matches(it) } @@ -118,7 +122,7 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDispl println("*** waitForView loop loopMainThreadForAtLeast...") uiController.loopMainThreadForAtLeast(50) println("*** waitForView loop ...loopMainThreadForAtLeast") - } while (System.currentTimeMillis() < endTime) + } while (clock.epochMillis() < endTime) println("*** waitForView timeout $view") // Timeout happens. @@ -168,9 +172,9 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource { val res = object : IdlingResource, ActivityLifecycleCallback { private var callback: IdlingResource.ResourceCallback? = null private var resumedActivity: Activity? = null - private val uniqTS = System.currentTimeMillis() + private val uniqueSuffix = Random.nextLong() - override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS" + override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqueSuffix" override fun isIdleNow(): Boolean { val activity = resumedActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).firstOrNull { diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt index fcec2a9fb5..344a2ecfb1 100644 --- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt +++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt @@ -34,6 +34,7 @@ import org.hamcrest.CoreMatchers.not import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -44,7 +45,7 @@ class RegistrationTest { @Test fun simpleRegister() { - val userId: String = "UiAutoTest_${System.currentTimeMillis()}" + val userId: String = "UiAutoTest_${Random.nextLong()}" val password: String = "password" val homeServerUrl: String = "http://10.0.2.2:8080" diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt index a84aae9994..21fd226317 100644 --- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -48,6 +48,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.matrix.android.sdk.api.session.Session +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -61,7 +62,7 @@ class SecurityBootstrapTest : VerificationTestBase() { @Before fun createSessionWithCrossSigning() { val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) stubAllExternalIntents() } diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index c82b543a08..76f09638be 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -66,7 +67,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() { @Before fun createSessionWithCrossSigning() { val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) doSync { existingSession!!.cryptoService().crossSigningService() diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index 80d8315a0e..76d5717000 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -68,7 +69,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() { fun createSessionWithCrossSigningAnd4S() { val context = InstrumentationRegistry.getInstrumentation().targetContext val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) doSync { existingSession!!.cryptoService().crossSigningService() diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt index 2939dcf4e0..b01c1a895f 100644 --- a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import im.vector.app.core.time.DefaultClock import org.junit.rules.TestWatcher import org.junit.runner.Description import timber.log.Timber @@ -54,7 +55,7 @@ private fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) { val contentValues = ContentValues().apply { put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_TAKEN, DefaultClock().epochMillis()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { useMediaStoreScreenshotStorage( diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index cc69cfd426..07b7c9ebf9 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -29,6 +29,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult @@ -59,6 +60,8 @@ class DebugMenuActivity : VectorBaseActivity() { @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject + lateinit var clock: Clock private lateinit var buffer: ByteArray @@ -165,7 +168,7 @@ class DebugMenuActivity : VectorBaseActivity() { } val builder = NotificationCompat.Builder(this, "CHAN") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title") .setContentText("Content") // No effect because it's a group summary notif @@ -180,16 +183,16 @@ class DebugMenuActivity : VectorBaseActivity() { .setName("User name") .build() ) - .addMessage("Message 1 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) - .addMessage("Message 1 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + .addMessage("Message 1 - 1", clock.epochMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 1 - 2", clock.epochMillis(), Person.Builder().setName("user 1-2").build()) val messagingStyle2 = NotificationCompat.MessagingStyle( Person.Builder() .setName("User name 2") .build() ) - .addMessage("Message 2 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) - .addMessage("Message 2 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + .addMessage("Message 2 - 1", clock.epochMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 2 - 2", clock.epochMillis(), Person.Builder().setName("user 1-2").build()) notificationManager.notify(10, builder.build()) @@ -197,7 +200,7 @@ class DebugMenuActivity : VectorBaseActivity() { 11, NotificationCompat.Builder(this, "CHAN") .setChannelId("CHAN") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title 1") .setContentText("Content 1") // For shortcut on long press on launcher icon @@ -211,7 +214,7 @@ class DebugMenuActivity : VectorBaseActivity() { notificationManager.notify( 12, NotificationCompat.Builder(this, "CHAN2") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title 2") .setContentText("Content 2") .setStyle(messagingStyle2) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt index 0fe89e7fe5..e890180c4f 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -18,13 +18,17 @@ package im.vector.app.fdroid import android.content.Context import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.time.Clock import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import timber.log.Timber object BackgroundSyncStarter { - fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { + fun start(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { if (vectorPreferences.areNotificationEnabledForDevice()) { val activeSession = activeSessionHolder.getSafeActiveSession() ?: return when (vectorPreferences.getFdroidSyncBackgroundMode()) { @@ -38,7 +42,12 @@ object BackgroundSyncStarter { } BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> { // We need to use alarm in this mode - AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay()) + AlarmSyncBroadcastReceiver.scheduleAlarm( + context, + activeSession.sessionId, + vectorPreferences.backgroundSyncDelay(), + clock + ) Timber.i("## Sync: Alarm scheduled to start syncing") } BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index 4be36d7de3..09bd56654d 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -27,6 +27,7 @@ import androidx.core.content.getSystemService import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.services.VectorSyncService +import im.vector.app.core.time.Clock import org.matrix.android.sdk.api.session.sync.job.SyncService import timber.log.Timber @@ -34,10 +35,13 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Timber.d("## Sync: AlarmSyncBroadcastReceiver received intent") - val vectorPreferences = context.singletonEntryPoint() - .takeIf { it.activeSessionHolder().getSafeActiveSession() != null } - ?.vectorPreferences() - ?: return Unit.also { Timber.v("No active session, so don't launch sync service.") } + val singletonEntryPoint = context.singletonEntryPoint() + if (singletonEntryPoint.activeSessionHolder().getSafeActiveSession() == null) { + Timber.v("No active session, so don't launch sync service.") + return + } + val vectorPreferences = singletonEntryPoint.vectorPreferences() + val clock = singletonEntryPoint.clock() val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return VectorSyncService.newPeriodicIntent( @@ -52,7 +56,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { ContextCompat.startForegroundService(context, it) } catch (ex: Throwable) { Timber.i("## Sync: Failed to start service, Alarm scheduled to restart service") - scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay()) + scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay(), clock) Timber.e(ex) } } @@ -61,7 +65,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { companion object { private const val REQUEST_CODE = 0 - fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int) { + fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int, clock: Clock) { // Reschedule Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply { @@ -74,7 +78,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L + val firstMillis = clock.epochMillis() + delayInSeconds * 1000L val alarmMgr = context.getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index c1bc90c4db..aacd7723f5 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -32,7 +32,8 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { BackgroundSyncStarter.start( context, singletonEntryPoint.vectorPreferences(), - singletonEntryPoint.activeSessionHolder() + singletonEntryPoint.activeSessionHolder(), + singletonEntryPoint.clock() ) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7603e738d7..425fd1081a 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,6 +21,7 @@ import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.time.Clock import im.vector.app.fdroid.BackgroundSyncStarter import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.VectorPreferences @@ -66,7 +67,10 @@ object FcmHelper { AlarmSyncBroadcastReceiver.cancelAlarm(context) } - fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { - BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder) + fun onEnterBackground(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { + BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder, clock) } } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 52ad4be087..3d44f10f76 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -26,6 +26,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.time.Clock import im.vector.app.features.settings.VectorPreferences import timber.log.Timber @@ -107,7 +108,10 @@ object FcmHelper { } @Suppress("UNUSED_PARAMETER") - fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { + fun onEnterBackground(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { // No op } } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index e12eecfefc..8917513537 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,6 +43,7 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration @@ -85,6 +86,7 @@ class VectorApplication : @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var clock: Clock @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider @@ -180,7 +182,7 @@ class VectorApplication : override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") - FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) + FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder, clock) } }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) diff --git a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt index eb6f6a94f7..029fce7423 100644 --- a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt @@ -22,15 +22,19 @@ import android.text.format.DateUtils import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.toTimestamp +import im.vector.app.core.time.Clock import org.threeten.bp.LocalDateTime import org.threeten.bp.Period import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject import kotlin.math.absoluteValue -class VectorDateFormatter @Inject constructor(private val context: Context, - private val localeProvider: LocaleProvider, - private val dateFormatterProviders: DateFormatterProviders) { +class VectorDateFormatter @Inject constructor( + private val context: Context, + private val localeProvider: LocaleProvider, + private val dateFormatterProviders: DateFormatterProviders, + private val clock: Clock, +) { private val hourFormatter by lazy { if (DateFormat.is24HourFormat(context)) { @@ -158,8 +162,9 @@ class VectorDateFormatter @Inject constructor(private val context: Context, private fun getRelativeDay(ts: Long): String { return DateUtils.getRelativeTimeSpanString( ts, - System.currentTimeMillis(), + clock.epochMillis(), DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_SHOW_WEEKDAY).toString() + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() } } diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index 283437c679..dc88229a10 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,6 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer @@ -46,6 +47,8 @@ interface SingletonEntryPoint { fun navigator(): Navigator + fun clock(): Clock + fun errorFormatter(): ErrorFormatter fun bugReporter(): BugReporter diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt index 8f70808087..757b415ce5 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -27,6 +27,7 @@ import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper.Listener import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog @@ -45,7 +46,8 @@ import java.io.File class GalleryOrCameraDialogHelper( // must implement GalleryOrCameraDialogHelper.Listener private val fragment: Fragment, - private val colorProvider: ColorProvider + private val colorProvider: ColorProvider, + private val clock: Clock, ) { interface Listener { fun onImageReady(uri: Uri?) @@ -91,7 +93,7 @@ class GalleryOrCameraDialogHelper( } private fun startUCrop(image: MultiPickerImageType) { - val destinationFile = File(activity.cacheDir, image.displayName.insertBeforeLast("_e_${System.currentTimeMillis()}")) + val destinationFile = File(activity.cacheDir, image.displayName.insertBeforeLast("_e_${clock.epochMillis()}")) val uri = image.contentUri createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title)) .withAspectRatio(1f, 1f) diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt index 5dbea8dcc4..131276bea6 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt @@ -33,6 +33,8 @@ import androidx.work.WorkerParameters import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.PendingIntentCompat +import im.vector.app.core.time.Clock +import im.vector.app.core.time.DefaultClock import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode import org.matrix.android.sdk.api.Matrix @@ -77,6 +79,7 @@ class VectorSyncService : SyncService() { @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var matrix: Matrix + @Inject lateinit var clock: Clock override fun provideMatrix() = matrix @@ -102,7 +105,8 @@ class VectorSyncService : SyncService() { syncTimeoutSeconds = syncTimeoutSeconds, syncDelaySeconds = syncDelaySeconds, isPeriodic = true, - isNetworkBack = false + isNetworkBack = false, + currentTimeMillis = clock.epochMillis() ) } @@ -114,9 +118,10 @@ class VectorSyncService : SyncService() { val rescheduleSyncWorkRequest: WorkRequest = OneTimeWorkRequestBuilder() .setInputData(RestartWhenNetworkOn.createInputData(sessionId, syncTimeoutSeconds, syncDelaySeconds, isPeriodic)) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() ) .build() @@ -137,20 +142,27 @@ class VectorSyncService : SyncService() { } // I do not move or rename this class, since I'm not sure about the side effect regarding the WorkManager - class RestartWhenNetworkOn(appContext: Context, workerParams: WorkerParameters) : - Worker(appContext, workerParams) { + class RestartWhenNetworkOn( + appContext: Context, + workerParams: WorkerParameters + ) : Worker(appContext, workerParams) { + override fun doWork(): Result { Timber.d("## Sync: RestartWhenNetworkOn.doWork()") val sessionId = inputData.getString(KEY_SESSION_ID) ?: return Result.failure() val syncTimeoutSeconds = inputData.getInt(KEY_SYNC_TIMEOUT_SECONDS, BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS) val syncDelaySeconds = inputData.getInt(KEY_SYNC_DELAY_SECONDS, BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS) val isPeriodic = inputData.getBoolean(KEY_IS_PERIODIC, false) + + // Not sure how to inject a Clock here + val clock = DefaultClock() applicationContext.rescheduleSyncService( sessionId = sessionId, syncTimeoutSeconds = syncTimeoutSeconds, syncDelaySeconds = syncDelaySeconds, isPeriodic = isPeriodic, - isNetworkBack = true + isNetworkBack = true, + currentTimeMillis = clock.epochMillis() ) // Indicate whether the work finished successfully with the Result return Result.success() @@ -182,7 +194,8 @@ private fun Context.rescheduleSyncService(sessionId: String, syncTimeoutSeconds: Int, syncDelaySeconds: Int, isPeriodic: Boolean, - isNetworkBack: Boolean) { + isNetworkBack: Boolean, + currentTimeMillis: Long) { Timber.d("## Sync: rescheduleSyncService") val intent = if (isPeriodic) { VectorSyncService.newPeriodicIntent( @@ -208,7 +221,7 @@ private fun Context.rescheduleSyncService(sessionId: String, } else { PendingIntent.getService(this, 0, intent, PendingIntentCompat.FLAG_IMMUTABLE) } - val firstMillis = System.currentTimeMillis() + syncDelaySeconds * 1000L + val firstMillis = currentTimeMillis + syncDelaySeconds * 1000L val alarmMgr = getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index a9375b6545..d961dcaa46 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -147,8 +147,11 @@ fun openFileSelection(activity: Activity, * Send an email to address with optional subject and message */ fun sendMailTo(address: String, subject: String? = null, message: String? = null, activity: Activity) { - val intent = Intent(Intent.ACTION_SENDTO, Uri.fromParts( - "mailto", address, null)) + val intent = Intent( + Intent.ACTION_SENDTO, Uri.fromParts( + "mailto", address, null + ) + ) intent.putExtra(Intent.EXTRA_SUBJECT, subject) intent.putExtra(Intent.EXTRA_TEXT, message) @@ -248,7 +251,12 @@ private fun appendTimeToFilename(name: String): String { return """${filename}_$dateExtension.$fileExtension""" } -suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { +suspend fun saveMedia(context: Context, + file: File, + title: String, + mediaMimeType: String?, + notificationUtils: NotificationUtils, + currentTimeMillis: Long) { withContext(Dispatchers.IO) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val filename = appendTimeToFilename(title) @@ -257,8 +265,8 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType put(MediaStore.Images.Media.TITLE, filename) put(MediaStore.Images.Media.DISPLAY_NAME, filename) put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_ADDED, currentTimeMillis) + put(MediaStore.Images.Media.DATE_TAKEN, currentTimeMillis) } val externalContentUri = when { mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI @@ -289,7 +297,7 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType } } } else { - saveMediaLegacy(context, mediaMimeType, title, file) + saveMediaLegacy(context, mediaMimeType, title, file, currentTimeMillis) } } } @@ -298,7 +306,8 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, - file: File) { + file: File, + currentTimeMillis: Long) { val state = Environment.getExternalStorageState() if (Environment.MEDIA_MOUNTED != state) { context.toast(context.getString(R.string.error_saving_media_file)) @@ -319,7 +328,7 @@ private fun saveMediaLegacy(context: Context, } else { title } - val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) + val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename, currentTimeMillis) if (savedFile != null) { val downloadManager = context.getSystemService() downloadManager?.addCompletedDownload( @@ -329,7 +338,8 @@ private fun saveMediaLegacy(context: Context, mediaMimeType ?: MimeTypes.OctetStream, savedFile.absolutePath, savedFile.length(), - true) + true + ) addToGallery(savedFile, mediaMimeType, context) } } catch (error: Throwable) { @@ -408,10 +418,11 @@ fun selectTxtFileToWrite( * @param sourceFile the file source path * @param dstDirPath the dst path * @param outputFilename optional the output filename + * @param currentTimeMillis the current time in milliseconds * @return the created file */ @Suppress("DEPRECATION") -fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? { +fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?, currentTimeMillis: Long): File? { // defines another name for the external media var dstFileName: String @@ -423,7 +434,7 @@ fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: Strin if (dotPos > 0) { fileExt = sourceFile.name.substring(dotPos) } - dstFileName = "vector_" + System.currentTimeMillis() + fileExt + dstFileName = "vector_$currentTimeMillis$fileExt" } else { dstFileName = outputFilename } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 5b9c3f7fb4..729ac10d4b 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.OnSnapPositionChangeListener import im.vector.app.core.utils.SnapOnScrollListener import im.vector.app.core.utils.attachSnapHelperWithListener @@ -64,7 +65,8 @@ data class AttachmentsPreviewArgs( class AttachmentsPreviewFragment @Inject constructor( private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController, - private val colorProvider: ColorProvider + private val colorProvider: ColorProvider, + private val clock: Clock, ) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback { private val fragmentArgs: AttachmentsPreviewArgs by args() @@ -192,7 +194,7 @@ class AttachmentsPreviewFragment @Inject constructor( private fun handleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState - val destinationFile = File(requireContext().cacheDir, currentAttachment.name.insertBeforeLast("_edited_image_${System.currentTimeMillis()}")) + val destinationFile = File(requireContext().cacheDir, currentAttachment.name.insertBeforeLast("_edited_image_${clock.epochMillis()}")) val uri = currentAttachment.queryUri createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), currentAttachment.name) .getIntent(requireContext()) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index 07062fc732..b0da4f0e18 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String import im.vector.app.features.call.conference.jwt.JitsiJWTFactory @@ -46,7 +47,9 @@ class JitsiService @Inject constructor( private val rawService: RawService, private val stringProvider: StringProvider, private val themeProvider: ThemeProvider, - private val jitsiJWTFactory: JitsiJWTFactory) { + private val jitsiJWTFactory: JitsiJWTFactory, + private val clock: Clock, +) { companion object { const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" @@ -60,7 +63,7 @@ class JitsiService @Inject constructor( suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { // Build data for a jitsi widget - val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() + val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + clock.epochMillis() val preferredJitsiDomain = tryOrNull { rawService.getElementWellknown(session.sessionParams) ?.jitsiServer diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt index f1a4975751..489b2d1eae 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt @@ -21,6 +21,7 @@ import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.services.VectorService +import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils import javax.inject.Inject @@ -28,6 +29,7 @@ import javax.inject.Inject class ScreenCaptureService : VectorService() { @Inject lateinit var notificationUtils: NotificationUtils + @Inject lateinit var clock: Clock private val binder = LocalBinder() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -37,7 +39,7 @@ class ScreenCaptureService : VectorService() { } private fun showStickyNotification() { - val notificationId = System.currentTimeMillis().toInt() + val notificationId = clock.epochMillis().toInt() val notification = notificationUtils.buildScreenSharingNotification() startForeground(notificationId, notification) } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index fed61c3b15..f0db3e199f 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -670,15 +670,11 @@ class WebRtcCall( isRemoteOnHold = true isLocalOnHold = true direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY - timer.pause() } else { isRemoteOnHold = false isLocalOnHold = wasLocalOnHold onCallBecomeActive(this@WebRtcCall) direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV - if (!isLocalOnHold) { - timer.resume() - } } for (transceiver in peerConnection?.transceivers ?: emptyList()) { transceiver.direction = direction @@ -941,11 +937,6 @@ class WebRtcCall( wasLocalOnHold = nowOnHold if (prevOnHold != nowOnHold) { isLocalOnHold = nowOnHold - if (nowOnHold) { - timer.pause() - } else { - timer.resume() - } listeners.forEach { tryOrNull { it.onHoldUnhold() } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt index 34aa7ba0ee..dfa7d1aaa3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import com.nulabinc.zxcvbn.Strength import im.vector.app.R import im.vector.app.core.platform.WaitingViewData +import im.vector.app.core.time.Clock import im.vector.app.core.utils.LiveEvent import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener @@ -37,7 +38,9 @@ import javax.inject.Inject /** * The shared view model between all fragments. */ -class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { +class KeysBackupSetupSharedViewModel @Inject constructor( + private val clock: Clock, +) : ViewModel() { companion object { const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2" @@ -85,7 +88,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { fun prepareRecoveryKey(context: Context, withPassphrase: String?) { // Update requestId - currentRequestId.value = System.currentTimeMillis() + currentRequestId.value = clock.epochMillis() isCreatingBackupVersion.value = true recoveryKey.value = null @@ -101,9 +104,11 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { return } - loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_step3_generating_key_status), + loadingStatus.value = WaitingViewData( + context.getString(R.string.keys_backup_setup_step3_generating_key_status), progress, - total) + total + ) } }, object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index a57159075f..95378860e7 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -18,6 +18,7 @@ package im.vector.app.features.crypto.verification import android.content.Context import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer @@ -43,7 +44,9 @@ import javax.inject.Singleton class IncomingVerificationRequestHandler @Inject constructor( private val context: Context, private var avatarRenderer: Provider, - private val popupAlertManager: PopupAlertManager) : VerificationService.Listener { + private val popupAlertManager: PopupAlertManager, + private val clock: Clock, +) : VerificationService.Listener { private var session: Session? = null @@ -105,7 +108,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } ) // 10mn expiration - expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L) + expirationTimestamp = clock.epochMillis() + (10 * 60 * 1000L) } popupAlertManager.postVectorAlert(alert) } @@ -174,7 +177,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } colorAttribute = R.attr.vctr_notice_secondary // 5mn expiration - expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L) + expirationTimestamp = clock.epochMillis() + (5 * 60 * 1000L) } popupAlertManager.postVectorAlert(alert) } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 3ca19b39f9..34bdc5fcd3 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.time.Clock import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -56,10 +57,12 @@ data class DeviceDetectionInfo( val currentSessionTrust: Boolean ) -class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, - session: Session, - private val vectorPreferences: VectorPreferences) : - VectorViewModel(initialState) { +class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( + @Assisted initialState: UnknownDevicesState, + session: Session, + private val vectorPreferences: VectorPreferences, + clock: Clock, +) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() @@ -75,11 +78,10 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted private val ignoredDeviceList = ArrayList() init { - val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) .firstOrNull { it.deviceId == session.sessionParams.deviceId } ?.firstTimeSeenLocalTs - ?: System.currentTimeMillis() + ?: clock.epochMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") ignoredDeviceList.addAll( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index f95baae36b..fa19e39ae7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail +import im.vector.app.core.time.Clock import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -45,7 +46,9 @@ fun ChatEffect.toMessageType(): String { * precisely an event decrypted with a few delay won't trigger an effect; it's acceptable) * Events that are more that 10s old won't trigger effects */ -class ChatEffectManager @Inject constructor() { +class ChatEffectManager @Inject constructor( + private val clock: Clock, +) { interface Delegate { fun stopEffects() @@ -61,7 +64,7 @@ class ChatEffectManager @Inject constructor() { fun checkForEffect(event: TimelineEvent) { val age = event.root.ageLocalTs ?: 0 - val now = System.currentTimeMillis() + val now = clock.epochMillis() // messages older than 10s should not trigger any effect if ((now - age) >= 10_000) return val content = event.root.getClearContent()?.toModel() ?: return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt index 9f10805f95..1f1124b8c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -33,14 +33,18 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyViewHolder import im.vector.app.R +import im.vector.app.core.time.Clock import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import kotlin.math.abs import kotlin.math.min -class RoomMessageTouchHelperCallback(private val context: Context, - @DrawableRes actionIcon: Int, - private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() { +class RoomMessageTouchHelperCallback( + private val context: Context, + @DrawableRes actionIcon: Int, + private val handler: QuickReplayHandler, + private val clock: Clock, +) : EpoxyTouchHelperCallback() { interface QuickReplayHandler { fun performQuickReplyOnHolder(model: EpoxyModel<*>) @@ -141,7 +145,7 @@ class RoomMessageTouchHelperCallback(private val context: Context, private fun drawReplyButton(canvas: Canvas, itemView: View) { // Timber.v("drawReplyButton") val translationX = abs(itemView.translationX) - val newTime = System.currentTimeMillis() + val newTime = clock.epochMillis() val dt = min(17, newTime - lastReplyButtonAnimationTime) lastReplyButtonAnimationTime = newTime val showing = translationX >= minShowDistance diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 4603793bd5..099f3779ee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -296,7 +296,7 @@ class TimelineFragment @Inject constructor( private const val ircPattern = " (IRC)" } - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val timelineArgs: TimelineArgs by args() private val glideRequests by lazy { @@ -1443,7 +1443,7 @@ class TimelineFragment @Inject constructor( } } } - val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler) + val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler, clock) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(views.timelineRecyclerView) } @@ -2186,7 +2186,8 @@ class TimelineFragment @Inject constructor( file = it, title = action.messageContent.body, mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) } .onFailure { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 95553eb1cd..016a39d919 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import kotlin.random.Random /** * Describes the current send mode: @@ -35,7 +36,7 @@ sealed interface SendMode { val text: String, val fromSharing: Boolean, // This is necessary for forcing refresh on selectSubscribe - private val ts: Long = System.currentTimeMillis() + private val random: Int = Random.nextInt() ) : SendMode data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode @@ -83,7 +84,8 @@ data class MessageComposerViewState( constructor(args: TimelineArgs) : this( roomId = args.roomId, startsThread = args.threadTimelineArgs?.startsThread.orFalse(), - rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId + ) fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 4f951dfecb..c77cdceed0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -30,6 +30,7 @@ import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter @@ -47,7 +48,8 @@ class SearchResultController @Inject constructor( private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, private val displayableEventFormatter: DisplayableEventFormatter, - private val userPreferencesProvider: UserPreferencesProvider + private val userPreferencesProvider: UserPreferencesProvider, + private val clock: Clock, ) : TypedEpoxyController() { var listener: Listener? = null @@ -109,7 +111,7 @@ class SearchResultController @Inject constructor( val spannable = setHighLightedText(text, data.highlights) ?: return@forEach val eventDate = Calendar.getInstance().apply { - timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis() + timeInMillis = eventAndSender.event.originServerTs ?: clock.epochMillis() } if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) { GenericHeaderItem_() @@ -125,7 +127,8 @@ class SearchResultController @Inject constructor( .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)) .spannable(spannable.toEpoxyCharSequence()) .sender(eventAndSender.sender - ?: eventAndSender.event.senderId?.let { session.roomService().getRoomMember(it, data.roomId) }?.toMatrixItem()) + ?: eventAndSender.event.senderId?.let { session.roomService().getRoomMember(it, data.roomId) }?.toMatrixItem() + ) .threadDetails(event.threadDetails) .threadSummaryFormatted(displayableEventFormatter.formatThreadSummary(event.threadDetails?.threadSummaryLatestEvent).toString()) .areThreadMessagesEnabled(userPreferencesProvider.areThreadMessagesEnabled()) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 8cea57399a..6c9f7ac4ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,6 +31,7 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull +import im.vector.app.core.time.Clock import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -78,19 +79,21 @@ import javax.inject.Inject import kotlin.math.min import kotlin.system.measureTimeMillis -class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, - private val vectorPreferences: VectorPreferences, - private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, - private val timelineItemFactory: TimelineItemFactory, - private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val mergedHeaderItemFactory: MergedHeaderItemFactory, - private val session: Session, - @TimelineEventControllerHandler - private val backgroundHandler: Handler, - private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, - private val readReceiptsItemFactory: ReadReceiptsItemFactory, - private val reactionListFactory: ReactionsSummaryFactory +class TimelineEventController @Inject constructor( + private val dateFormatter: VectorDateFormatter, + private val vectorPreferences: VectorPreferences, + private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, + private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, + private val timelineItemFactory: TimelineItemFactory, + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val mergedHeaderItemFactory: MergedHeaderItemFactory, + private val session: Session, + @TimelineEventControllerHandler + private val backgroundHandler: Handler, + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, + private val readReceiptsItemFactory: ReadReceiptsItemFactory, + private val reactionListFactory: ReactionsSummaryFactory, + private val clock: Clock, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { /** @@ -209,8 +212,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onChanged(position: Int, count: Int, payload: Any?) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onChanged(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) (position until position + count).forEach { // Invalidate cache modelCache[it] = null @@ -238,8 +243,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onMoved(fromPosition: Int, toPosition: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) val model = modelCache.removeAt(fromPosition) modelCache.add(toPosition, model) requestModelBuild() @@ -249,8 +256,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onInserted(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onInserted(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) repeat(count) { modelCache.add(position, null) } @@ -261,8 +270,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onRemoved(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onRemoved(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) repeat(count) { modelCache.removeAt(position) } @@ -314,7 +325,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (partialState.roomSummary?.membership != Membership.JOIN) { return } - val timestamp = System.currentTimeMillis() + val timestamp = clock.epochMillis() val showingForwardLoader = LoadingItem_() .id("forward_loading_item_$timestamp") @@ -406,14 +417,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEvent = it, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId) + rootThreadEventId = partialState.rootThreadEventId + ) } val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { timelineEventVisibilityHelper.shouldShowEvent( timelineEvent = it, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId) + rootThreadEventId = partialState.rootThreadEventId + ) } val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( @@ -466,7 +479,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec position: Int, receiptsByEvents: Map>): CacheItemData { val wantsDateSeparator = wantsDateSeparator(event, nextEvent) - val mergedHeaderModel = mergedHeaderItemFactory.create(event, + val mergedHeaderModel = mergedHeaderItemFactory.create( + event, nextEvent = nextEvent, partialState = partialState, items = this@TimelineEventController.currentSnapshot, @@ -537,7 +551,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEvent = event, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId)) { + rootThreadEventId = partialState.rootThreadEventId + )) { lastShownEventId = event.eventId } if (lastShownEventId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 63a34fe713..f96ee7eee2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -26,6 +26,7 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericHeaderItem import im.vector.app.core.ui.list.genericItem @@ -49,7 +50,8 @@ class ViewEditHistoryEpoxyController @Inject constructor( private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val eventHtmlRenderer: EventHtmlRenderer, - private val dateFormatter: VectorDateFormatter + private val dateFormatter: VectorDateFormatter, + private val clock: Clock, ) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { @@ -86,7 +88,7 @@ class ViewEditHistoryEpoxyController @Inject constructor( val evDate = Calendar.getInstance().apply { timeInMillis = timelineEvent.originServerTs - ?: System.currentTimeMillis() + ?: clock.epochMillis() } if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) { // need to display header with day diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index e4522b260e..edd2271550 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -31,6 +31,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -142,6 +143,7 @@ class MessageItemFactory @Inject constructor( private val lightweightSettingsStorage: LightweightSettingsStorage, private val spanUtils: SpanUtils, private val session: Session, + private val clock: Clock, private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker, private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, @@ -457,6 +459,7 @@ class MessageItemFactory @Inject constructor( reactionsSummaryEvents = attributes.reactionsSummaryEvents, ) ) + .clock(clock) .callback(callback) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 8a0e1e18fd..e9361e564c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -25,6 +25,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ import org.matrix.android.sdk.api.session.room.timeline.Timeline +import kotlin.random.Random import kotlin.reflect.KMutableProperty0 private const val DEFAULT_PREFETCH_THRESHOLD = 30 @@ -104,7 +105,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut .coerceAtLeast(0) val loadingItem = LoadingItem_() - .id("prefetch_backward_loading${System.currentTimeMillis()}") + .id("prefetch_backward_loading${Random.nextLong()}") .showLoader(false) .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback) @@ -120,7 +121,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut .coerceAtLeast(0) val loadingItem = LoadingItem_() - .id("prefetch_forward_loading${System.currentTimeMillis()}") + .id("prefetch_forward_loading${Random.nextLong()}") .showLoader(false) .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback) add(indexOfPrefetchForward, loadingItem) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt index 0e6530fdca..fc4c55d1f3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -31,6 +31,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick +import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.MessageColorProvider @@ -47,6 +48,9 @@ abstract class VerificationRequestItem : AbsBaseMessageItem(), GalleryOrCameraDialogHelper.Listener { private val viewModel: AccountCreatedViewModel by fragmentViewModel() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding { return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false) @@ -73,7 +75,7 @@ class AccountCreatedFragment @Inject constructor( viewModel.onEach { invalidateState(it) } - views.loginAccountCreatedTime.text = dateFormatter.format(System.currentTimeMillis(), DateFormatKind.MESSAGE_SIMPLE) + views.loginAccountCreatedTime.text = dateFormatter.format(clock.epochMillis(), DateFormatKind.MESSAGE_SIMPLE) } private fun observeViewEvents() { diff --git a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt index b0401ccd30..6a24ac4fd7 100644 --- a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.intent.getMimeTypeFromUri +import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.features.notifications.NotificationUtils import kotlinx.coroutines.withContext @@ -30,7 +31,8 @@ import javax.inject.Inject class DownloadMediaUseCase @Inject constructor( @ApplicationContext private val appContext: Context, private val session: Session, - private val notificationUtils: NotificationUtils + private val notificationUtils: NotificationUtils, + private val clock: Clock, ) { suspend fun execute(input: File): Result = withContext(session.coroutineDispatchers.io) { @@ -40,7 +42,8 @@ class DownloadMediaUseCase @Inject constructor( file = input, title = input.name, mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a1304bb6ae..80f5f47b3b 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -20,6 +20,7 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.takeAs import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter @@ -59,7 +60,8 @@ import javax.inject.Inject class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, private val noticeEventFormatter: NoticeEventFormatter, - private val displayableEventFormatter: DisplayableEventFormatter + private val displayableEventFormatter: DisplayableEventFormatter, + private val clock: Clock, ) { // private val eventDisplay = RiotEventDisplay(context) @@ -87,7 +89,7 @@ class NotifiableEventResolver @Inject constructor( eventId = event.eventId!!, editedEventId = timelineEvent.getEditedEventId(), noisy = false, // will be updated - timestamp = event.originServerTs ?: System.currentTimeMillis(), + timestamp = event.originServerTs ?: clock.epochMillis(), description = bodyPreview, title = stringProvider.getString(R.string.notification_unknown_new_event), soundName = null, @@ -179,15 +181,19 @@ class NotifiableEventResolver @Inject constructor( roomName = roomName, roomIsDirect = room.roomSummary()?.isDirect ?: false, roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail(room.roomSummary()?.avatarUrl, + .resolveThumbnail( + room.roomSummary()?.avatarUrl, 250, 250, - ContentUrlResolver.ThumbnailMethod.SCALE), + ContentUrlResolver.ThumbnailMethod.SCALE + ), senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderInfo.avatarUrl, + .resolveThumbnail( + event.senderInfo.avatarUrl, 250, 250, - ContentUrlResolver.ThumbnailMethod.SCALE), + ContentUrlResolver.ThumbnailMethod.SCALE + ), matrixID = session.myUserId, soundName = null ) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 0fe75ae1a0..4205f2ca5a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -23,6 +23,7 @@ import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom @@ -46,6 +47,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var analyticsTracker: AnalyticsTracker + @Inject lateinit var clock: Clock override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return @@ -138,7 +140,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { eventId = UUID.randomUUID().toString(), editedEventId = null, noisy = false, - timestamp = System.currentTimeMillis(), + timestamp = clock.epochMillis(), senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), senderId = session.myUserId, @@ -189,7 +191,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( event.eventId, false, - System.currentTimeMillis(), + clock.epochMillis(), session.myUser?.displayname ?: context?.getString(R.string.notification_sender_me), session.myUserId, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index e44f42d5cd..08f6ccc2f3 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -51,6 +51,7 @@ import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.resources.StringProvider import im.vector.app.core.services.CallService +import im.vector.app.core.time.Clock import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver @@ -72,9 +73,12 @@ import kotlin.random.Random * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity */ @Singleton -class NotificationUtils @Inject constructor(private val context: Context, - private val stringProvider: StringProvider, - private val vectorPreferences: VectorPreferences) { +class NotificationUtils @Inject constructor( + private val context: Context, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { companion object { /* ========================================================================================== @@ -323,7 +327,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } val contentPendingIntent = PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), contentIntent, PendingIntentCompat.FLAG_IMMUTABLE ) @@ -337,7 +341,7 @@ class NotificationUtils @Inject constructor(private val context: Context, mode = VectorCallActivity.INCOMING_ACCEPT ) ) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) @@ -392,7 +396,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } val contentPendingIntent = PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), contentIntent, PendingIntentCompat.FLAG_IMMUTABLE ) @@ -453,7 +457,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(VectorCallActivity.newIntent(context, call, null)) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) builder.setContentIntent(contentPendingIntent) @@ -467,7 +471,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } return PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -515,7 +519,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId))) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) builder.setContentIntent(contentPendingIntent) return builder.build() @@ -562,7 +566,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ).let { @@ -636,7 +640,7 @@ class NotificationUtils @Inject constructor(private val context: Context, markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) val markRoomReadPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), markRoomReadIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -677,7 +681,7 @@ class NotificationUtils @Inject constructor(private val context: Context, intent.action = DISMISS_ROOM_NOTIF_ACTION val pendingIntent = PendingIntent.getBroadcast( context.applicationContext, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -712,7 +716,7 @@ class NotificationUtils @Inject constructor(private val context: Context, rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val rejectIntentPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -730,7 +734,7 @@ class NotificationUtils @Inject constructor(private val context: Context, joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val joinIntentPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), joinIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -811,7 +815,7 @@ class NotificationUtils @Inject constructor(private val context: Context, .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(roomIntentTap) .getPendingIntent( - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) } @@ -844,7 +848,7 @@ class NotificationUtils @Inject constructor(private val context: Context, intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) return PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, // PendingIntents attached to actions with remote inputs must be mutable PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt index 81300932db..0ccff7dffc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt @@ -29,6 +29,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentFtueProfilePictureBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.onboarding.OnboardingAction @@ -39,10 +40,11 @@ import javax.inject.Inject class FtueAuthChooseProfilePictureFragment @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : AbstractFtueAuthFragment(), GalleryOrCameraDialogHelper.Listener { - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val avatarRenderer: AvatarRenderer by lazy { requireContext().singletonEntryPoint().avatarRenderer() } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueProfilePictureBinding { diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index ae03b5345a..91292e42e5 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -24,6 +24,7 @@ import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import com.tapadoo.alerter.Alerter import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.time.Clock import im.vector.app.core.utils.isAnimationDisabled import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.pin.PinActivity @@ -41,7 +42,9 @@ import javax.inject.Singleton * will be back in the queue in first position. */ @Singleton -class PopupAlertManager @Inject constructor() { +class PopupAlertManager @Inject constructor( + private val clock: Clock, +) { companion object { const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE @@ -116,7 +119,7 @@ class PopupAlertManager @Inject constructor() { return } if (currentAlerter != null) { - if (currentAlerter!!.expirationTimestamp != null && System.currentTimeMillis() > currentAlerter!!.expirationTimestamp!!) { + if (currentAlerter!!.expirationTimestamp != null && clock.epochMillis() > currentAlerter!!.expirationTimestamp!!) { // this alert has expired, remove it // perform dismiss try { @@ -162,7 +165,7 @@ class PopupAlertManager @Inject constructor() { currentAlerter = next next?.let { if (!shouldBeDisplayedIn(next, currentActivity)) return - val currentTime = System.currentTimeMillis() + val currentTime = clock.epochMillis() if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) { // skip try { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 891b1f6e89..82cc270458 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentCreateRoomBinding import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.navigation.Navigator @@ -63,7 +64,8 @@ data class CreateRoomArgs( class CreateRoomFragment @Inject constructor( private val createRoomController: CreateRoomController, private val createSpaceController: CreateSubSpaceController, - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : VectorBaseFragment(), CreateRoomController.Listener, GalleryOrCameraDialogHelper.Listener, @@ -75,7 +77,7 @@ class CreateRoomFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreateRoomBinding { return FragmentCreateRoomBinding.inflate(inflater, container, false) @@ -167,7 +169,8 @@ class CreateRoomFragment @Inject constructor( } else { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) } - RoomJoinRuleBottomSheet.newInstance(state.roomJoinRules, + RoomJoinRuleBottomSheet.newInstance( + state.roomJoinRules, allowed.map { it.toOption(false) }, state.isSubSpace, state.parentSpaceSummary?.displayName diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index eb69e36ba0..85151e1258 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.analytics.plan.MobileScreen @@ -57,7 +58,8 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( private val controller: RoomSettingsController, colorProvider: ColorProvider, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + clock: Clock, ) : VectorBaseFragment(), RoomSettingsController.Callback, @@ -70,7 +72,7 @@ class RoomSettingsFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel private val roomProfileArgs: RoomProfileArgs by args() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomSettingGenericBinding { return FragmentRoomSettingGenericBinding.inflate(inflater, container, false) @@ -198,7 +200,8 @@ class RoomSettingsFragment @Inject constructor( RoomSettingsAction.SetAvatarAction( RoomSettingsViewState.AvatarAction.UpdateAvatar( newAvatarUri = uri, - newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) ) ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 6a115ad272..721f99178f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -30,6 +30,7 @@ import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.R import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.shareMedia import im.vector.app.databinding.FragmentRoomUploadsBinding @@ -43,7 +44,8 @@ import javax.inject.Inject class RoomUploadsFragment @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val notificationUtils: NotificationUtils + private val notificationUtils: NotificationUtils, + private val clock: Clock, ) : VectorBaseFragment() { private val roomProfileArgs: RoomProfileArgs by args() @@ -88,7 +90,8 @@ class RoomUploadsFragment @Inject constructor( file = it.file, title = it.title, mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) }.onFailure { failure -> if (!isAdded) return@onFailure diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 195072465b..ca42a07d50 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -26,6 +26,7 @@ import com.squareup.seismic.ShakeDetector import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.time.Clock import im.vector.app.features.disclaimer.SHARED_PREF_KEY import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.themes.ThemeUtils @@ -33,7 +34,10 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import javax.inject.Inject -class VectorPreferences @Inject constructor(private val context: Context) { +class VectorPreferences @Inject constructor( + private val context: Context, + private val clock: Clock, +) { companion object { const val SETTINGS_HELP_PREFERENCE_KEY = "SETTINGS_HELP_PREFERENCE_KEY" @@ -664,9 +668,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { */ fun getMinMediasLastAccessTime(): Long { return when (getSelectedMediasSavingPeriod()) { - MEDIA_SAVING_3_DAYS -> System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60 - MEDIA_SAVING_1_WEEK -> System.currentTimeMillis() / 1000 - 7 * 24 * 60 * 60 - MEDIA_SAVING_1_MONTH -> System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 + MEDIA_SAVING_3_DAYS -> clock.epochMillis() / 1000 - 3 * 24 * 60 * 60 + MEDIA_SAVING_1_WEEK -> clock.epochMillis() / 1000 - 7 * 24 * 60 * 60 + MEDIA_SAVING_1_MONTH -> clock.epochMillis() / 1000 - 30 * 24 * 60 * 60 MEDIA_SAVING_FOREVER -> 0 else -> 0 } @@ -872,8 +876,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { * @return true if user should always appear offline */ fun userAlwaysAppearsOffline(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE, - getDefault(R.bool.settings_presence_user_always_appears_offline_default)) + return defaultPrefs.getBoolean( + SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE, + getDefault(R.bool.settings_presence_user_always_appears_offline_default) + ) } /** @@ -1005,9 +1011,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun prefSpacesShowAllRoomInHome(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, + return defaultPrefs.getBoolean( + SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, // migration of old property - !labsSpacesOnlyOrphansInHome()) + !labsSpacesOnlyOrphansInHome() + ) } /* diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index 678356b05b..99acea79df 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.getSizeOfFiles import im.vector.app.core.utils.toast @@ -74,7 +75,8 @@ import java.util.UUID import javax.inject.Inject class VectorSettingsGeneralFragment @Inject constructor( - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : VectorSettingsBaseFragment(), GalleryOrCameraDialogHelper.Listener { @@ -82,7 +84,7 @@ class VectorSettingsGeneralFragment @Inject constructor( override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val mUserSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index cef68c01c1..4225cbdbec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -36,12 +36,15 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.safeOpenOutputStream import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.time.Clock import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { +class KeyRequestsFragment @Inject constructor( + private val clock: Clock, +) : VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDevtoolKeyrequestsBinding { return FragmentDevtoolKeyrequestsBinding.inflate(inflater, container, false) @@ -126,7 +129,7 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment(), SpaceDetailEpoxyController.Listener, GalleryOrCameraDialogHelper.Listener, OnBackPressed { @@ -42,7 +44,7 @@ class CreateSpaceDetailsFragment @Inject constructor( override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 57b1c97efb..0264fdabfc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -38,7 +38,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.home.AvatarRenderer @@ -60,9 +60,9 @@ import javax.inject.Inject class SpaceSettingsFragment @Inject constructor( private val epoxyController: SpaceSettingsController, - private val colorProvider: ColorProvider, + colorProvider: ColorProvider, + clock: Clock, private val avatarRenderer: AvatarRenderer, - private val drawableProvider: DrawableProvider ) : VectorBaseFragment(), SpaceSettingsController.Callback, GalleryOrCameraDialogHelper.Listener, @@ -73,7 +73,7 @@ class SpaceSettingsFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentRoomSettingGenericBinding.inflate(inflater) @@ -236,7 +236,8 @@ class SpaceSettingsFragment @Inject constructor( RoomSettingsAction.SetAvatarAction( RoomSettingsViewState.AvatarAction.UpdateAvatar( newAvatarUri = uri, - newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) ) ) } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 291153ef2b..bb1f8db6ff 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem +import kotlin.random.Random data class ThreePidUser( val email: String, @@ -142,7 +143,7 @@ class UserListViewModel @AssistedInject constructor( } private fun retryUserSearch(state: UserListViewState) { - identityServerUsersSearch.tryEmit(UserSearch(state.searchTerm, cacheBuster = System.currentTimeMillis())) + identityServerUsersSearch.tryEmit(UserSearch(state.searchTerm, cacheBuster = Random.nextLong())) } private fun handleSearchUsers(searchTerm: String) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt index d2f7927d75..6c9b8d34c2 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt @@ -20,11 +20,13 @@ import android.content.Context import android.os.Build import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode +import im.vector.app.core.time.Clock import timber.log.Timber import java.io.File import javax.inject.Inject class VoicePlayerHelper @Inject constructor( + private val clock: Clock, context: Context ) { private val outputDirectory: File by lazy { @@ -46,9 +48,9 @@ class VoicePlayerHelper @Inject constructor( if (targetFile.exists()) { targetFile.delete() } - val start = System.currentTimeMillis() + val start = clock.epochMillis() val session = FFmpegKit.execute("-i \"${file.path}\" -c:a aac \"${targetFile.path}\"") - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.d("Convert to mp4 in $duration ms. Size in bytes from ${file.length()} to ${targetFile.length()}") return when { ReturnCode.isSuccess(session.returnCode) -> { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index 2d40f5f7a3..e5fd60dbc3 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -23,10 +23,14 @@ import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.Level import com.arthenica.ffmpegkit.ReturnCode import im.vector.app.BuildConfig +import im.vector.app.core.time.Clock import timber.log.Timber import java.io.File -class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") { +class VoiceRecorderL( + context: Context, + private val clock: Clock, +) : AbstractVoiceRecorder(context, "mp4") { override fun setOutputFormat(mediaRecorder: MediaRecorder) { // Use AAC/MP4 format here mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) @@ -43,9 +47,9 @@ class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") { if (targetFile.exists()) { targetFile.delete() } - val start = System.currentTimeMillis() + val start = clock.epochMillis() val session = FFmpegKit.execute("-i \"${recordedFile.path}\" -c:a libvorbis \"${targetFile.path}\"") - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.d("Convert to ogg in $duration ms. Size in bytes from ${recordedFile.length()} to ${targetFile.length()}") return when { ReturnCode.isSuccess(session.returnCode) -> { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt index 004d520a6f..0cee8f4f6e 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -18,16 +18,18 @@ package im.vector.app.features.voice import android.content.Context import android.os.Build +import im.vector.app.core.time.Clock import javax.inject.Inject class VoiceRecorderProvider @Inject constructor( - private val context: Context + private val context: Context, + private val clock: Clock, ) { fun provideVoiceRecorder(): VoiceRecorder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { VoiceRecorderQ(context) } else { - VoiceRecorderL(context) + VoiceRecorderL(context, clock) } } } diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml index fe3abdcf80..1e619580f2 100644 --- a/vector/src/main/res/values-ar/strings.xml +++ b/vector/src/main/res/values-ar/strings.xml @@ -245,7 +245,7 @@ أزِل انضم ارفض - انتقل إلى أول رسالة غير مقروءة. + انتقل إلى غير المقروءة. ترك الغرفة أمتأكّد من ترك الغرفة؟ امنع @@ -253,7 +253,7 @@ أخفِ كل رسائل هذا المستخدم اعرض كل رسائل هذا المستخدم ‏⁨%1$s⁩ يكتب… - لا تصريح لديك للنشر في هذه الغرفة + لا أذن لديك للنشر في هذه الغرفة. اخرج البصمة (⁨%s⁩): تعذّر التحقق من معرّف الخادوم البعيد. @@ -408,7 +408,7 @@ أشِر إليه لن تستطيع العودة عن هذا التغيير إذ أنك تمنح المستخدم نفس مستوى السلطة الذي لك. أمتأكد؟ - أمتأكد من منع هذا المستخدم من هذه الدردشة؟ + حظر مستخدم عن هذه الغرفة سيمنعه من الانضمام إليها ثانية. ثِق لا تثق تجاهل @@ -481,7 +481,7 @@ <b>غير<b/> مؤكّدة مؤكّدة أكّد - اسرد الأعضاء + أعضاء ادعُ قد يعني هذا بأن أحدهم يعترض الاتصال بعدوانية، أو أن هاتفك لا يثق بالشهادة التي قدّمها الخادوم البعيد. إن قال مدير الخادوم بأن هذا متوقع، فتأكد من أن البصمة أدناه تطابق البصمة التي وفّرها. @@ -735,7 +735,7 @@ %d مكالمةً فائة %d مكالمة فائة - يتصل… + يَرِن… اختر نغمة للمكالمات: نغمة المكالمات الواردة أكّد المكالمة قبل إجرائها @@ -1001,4 +1001,142 @@ غادر غادر الغرفة المرفوعات + أنت تستعرض هذا النقاش سلفًا! + أنت تستعرض هذه الغرفة سلفًا! + منفصل عن الشبكة. تحقق من اتصالك. + حدث عالجه مدير الغرفة. + ستعرض غرفك هنا. لانضمام لغرفة أو لإنشاء واحدة اضغط زر +. + ستعرض رسائلك المباشرة هنا. لبدأ محادثات جديدة اضغط زر +. + المحادثات + الغُرف + جميع الريائل مقروءة + دعاه %s + أرسلَ لك طلبًا + أعد المحاولة + اعرضه في الغرفة + ردّ + حرّر + خطأ مجهول + يريد %s التحقق من جلستك + طلب تحقق + فهمتُ + مستوثق! + التوقيع + الخوارزمية + النسخة + يتحقق من حالة النسخ الاحتياطي + استعد من نسخ احتياطي + الأذونات + أذونات الفضاء + أذونات الغرفة + %1$s و%2$s وآخرون + %1$s و%2$s + فك الحظر عن مستخدم سيمح له الانضمام لهذا الفضاء. + فك الحظر عن مستخدم سيمح له الانضمام لهذه الغرفة. + حظر مستخدم عن هذه الفضاء سيمنعه من الانضمام إليه ثانية. + فك حظر مستخدم + سبب الحظر + احظر مستخدمًا + سيزال المستخدم من هذه الفضاء. +\n +\nلمنعه من الانضمام ثانية لهذه الفضاء احظره. + سيزال المستخدم من هذه الغرفة. +\n +\nلمنعه من الانضمام ثانية لهذه الغرفة احظره. + سبب الإزالة + أزل مستخدمًا + أمتأكد من سحب دعوة هذا المستخدم؟ + اسحب الدعوة + ألغ تجاهل المستخدم + بتجاهل هذا المستخدم ستزال الرسائل من الغرف المشتركة بينكما. +\n +\nيمكنم التراجع عن هذا الإجراء في أي وقت عبر الإعدادات العامة. + تجاهل مستخدم + اخفض الرتبة + لن تتمكن من التراجع عن هذا التغيير لأنك ستخفض رتبتك ، إذا كنت آخر مستخدم ذي امتياز في الغرفة ، فسيكون من المستحيل استعادة الامتيازات. + أتريد خفض رتبتك؟ + اسحب الدعوة + هذه الغرفة ليست علنية، لا يمكنك إعادة الانضمام إليها بدون دعوة. + امنح اذن النفاذ للمتراسلين. + لمسح رمز الاستحابة السريعة اسمح بالنفاذ للكاميرا. + تجري مكالمة مرئية… + + لا مكالمات مرئية فائتة + مكالمة مرئية فائتة + مكالمتان مرئيتان فائتتان + %d مكالمات مرئية فائتة + %d مكالمة مرئية فائتة + %d مكالمة مرئية فائتة + + استخدم نغمة ${app_name} الافتراضية للمكالمات الواردة + امنع المكالمات العَرضية + استزد + عدّل + جربه + ليس الآن + عطّل + فعّل + لا يمكنك إجراء مكالمة مع نفسك، انتظر قبول منتسبين لطلب + شغّلتّ التعمية بين الطرفيات (خورزمية مجهولة %1$s). + وضع المزامنة في الخلفية + على الهاتف لن تتلق إخطارات عند ذكرك أو ذكر كلمة مفتاحية في الغرف المعماة. + ترقيات الغرف + رسائل آلي (Bot) + الكلمات المفتاحيّة + \@room + الرسائل المباشرة المعماة + الرسائل المباشرة + اسم المستخدم + اسمي العلني + عند ترقية الغرف + الرسائل المعماة في المحادثات الجماعية + الرسائل المعماة في المحادثات الفردية + اختر لون ضوء التنبيهوالاهتزاز والصوت… + اضبط الإخطارات الصامتة + اضبط إخطارات المكالمات + تجاهل التحسين + يأثر تحسين البطارية على عمل ${app_name}. + تحسين البطارية + عطّل القيود + ابدأ مع التشغيل + فعّل البدأ مع التشغيل + ستبدأ الخدمة عند إعادة تشغيل الجهاز. + نُقر الإخطار! + رجاء أنقر على الإخطارات، إن لم تستطع رؤيتها تحقق من إعدادات النظام. + عرض الإخطارات + أنت تستعرض الإخطارات! أنقرني! + أضف حسابًا + يمكن للمدعووين فقط العثور على الغرفة والانضمام إليها + إعداد نفاذ مجهول (%s) + يمكن لأي شخص طلب الانضمام لهذه الغرفة ليحدد الأعضاء إن كانوا سيقبلونهم أو يرفضونهم + أتريد نشر هذه الغرفة للعلن في دليل %1$s؟ + ألغ نشر هذا العنوان + انشر هذا العنوان + أضف عنوانًا محليًا + لا تملك هذه الغرفة عناوين محلية + العناوين المحلية + أتريد حذف العنوان \"%1$s\" ؟ + أتريد إلغاء تشر العنوان \"%1$s\" ؟ + انشر + الحظور + اختر + المصدر الافتراضي للوسائط + اختر + الوسائط + أدر البريد الإلكتروني وأرقام الهاتف المرتبطة بحسابك + البريد الإلكتروني وأرقام الهاتف + كلمة السر غير صالحة + كلمة السر + لا ودجات نشطة + استخدم المايكروفون + استخدم الكاميرا + احجب الكل + اسمح + معرف الغرفة + معرف الودجة + سمتك + معرفك + رابط صورتك الرمزية + اسمك العلني + افتح في المتصفح \ No newline at end of file diff --git a/vector/src/main/res/values-bn/strings.xml b/vector/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..8555330f05 --- /dev/null +++ b/vector/src/main/res/values-bn/strings.xml @@ -0,0 +1,4 @@ + + + %s এর আমন্ত্রণ + \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 3121f859f3..61be21b7bf 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -1566,7 +1566,9 @@ \n \nMůžete tuto akci kdykoli zvrátit v obecných nastaveních. Odignorovat uživatele - Odignorování tohoto uživatele opět ukáže všechny jejich zprávy. + Zrušení ignorování tohoto uživatele opět ukáže všechny jejich zprávy. +\n +\nTato akce povede k restartování aplikace a může trvat nějakou dobu. Zrušit pozvánku Jste si jisti, že chcete zrušit pozvánku tomuto uživateli\? Vykopnout uživatele @@ -2504,4 +2506,32 @@ Vezmi mě domů Přizpůsobit profil Zakázat + Načítání polohy živě… + 8 hodin + 1 hodinu + 15 minut + Sdílet svoji polohu živě na + (%1$s) + %1$s (%2$s) + Nelze přehrát %1$s + Pozastavit %1$s + Přehrát %1$s + %1$d minut %2$d sekund + %1$s, %2$s, %3$s + Sdíleli svou polohu živě + ${app_name} se skvěle hodí i na pracoviště. Aplikaci důvěřují nejlépe zabezpečené organizace na světě. + BETA + Vlákna jsou ve fázi vývoje s novými zajímavými funkcemi, jako jsou například vylepšená oznámení. Rádi si vyslechneme vaši zpětnou vazbu! + Zpětná vazba k vláknům Beta + Poskytnout zpětnou vazbu + BETA + Pokud je tato funkce povolena, budete se ostatním uživatelům vždy jevit jako offline, a to i při používání aplikace. + Režim offline + Přítomnost + Váš domovský server v současné době vlákna nepodporuje, takže tato funkce může být nespolehlivá. Některé zprávy ve vláknech nemusí být spolehlivě dostupné. %sChcete přesto vlákna povolit\? + Vlákna Beta + Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat. %sPovolením vláken se aplikace restartuje. U některých účtů to může trvat déle. + Vlákna Beta + Zjistit více + Vyzkoušet to \ No newline at end of file diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 0f80bc8e0f..a4ab637dae 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1,5 +1,5 @@ - + la invitación de %s %1$s invitó a %2$s %1$s te ha invitado @@ -317,7 +317,7 @@ Eliminar Unirse Rechazar - Mensajes no leídos. + Ir a mensajes no leídos Salir de la sala ¿Seguro que quieres salir de la sala? CONVERSACIONES DIRECTAS @@ -332,7 +332,7 @@ %s está escribiendo… %1$s y %2$s están escribiendo… %1$s y %2$s y otros están escribiendo… - No tienes permiso para publicar en esta sala + No tienes permiso para publicar en esta sala. Confiar No confiar Cerrar Sesión @@ -574,7 +574,7 @@ Quita el veto al usuario con la ID dada Define el nivel de autoridad de un usuario Invita al usuario con la ID dada a la sala actual - Se une a la sala con el alias dado + Se une a la sala con la dirección dada Salir de la sala Establecer el tema de la sala Expulsa al usuario con la ID dada @@ -582,7 +582,7 @@ Activar/Desactivar markdown Para reparar la gestión de las Aplicaciones de Matrix Vista previa de medios antes de enviar - Esta sala ha sido reemplazada y ya no está activa + Esta sala ha sido reemplazada y ya no está activa. La conversación continúa aquí Esta sala es una continuación de otra conversación Haz clic aquí para ver mensajes más antiguos @@ -972,7 +972,7 @@ Enviar un nuevo mensaje directo Ver el directorio de la sala Nombre o ID (#ejemplo:servidor.org) - Habilitar \"desplazar para contestar\" en la línea de tiempo + Habilitar \"deslizar para contestar\" en la línea de tiempo Enlace copiado al portapapeles Creando sala… Ver historial de modificaciones @@ -1306,7 +1306,9 @@ \n \n Puede revertir esta acción en cualquier momento en la configuración general. Dejar de ignorar al usuario - Si ignora a este usuario, se mostrarán todos sus mensajes nuevamente. + Si deja de ignorar a este usuario, se mostrarán todos sus mensajes nuevamente. +\n +\nTome nota de que esta acción reiniciará la aplicación y podría tardar algún tiempo. ¿Estás seguro de que deseas cancelar la invitación de este usuario\? Patear usuario patear al usuario los eliminará de esta sala. @@ -1482,7 +1484,7 @@ Una vez habilitado, el cifrado de una sala no se puede deshabilitar. Los mensajes enviados en una sala cifrada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar el cifrado puede impedir que muchos bots y puentes funcionen correctamente. Para estar seguro, verifique %s comprobando un código de un solo uso. Para estar seguro, hágalo en persona o use otra forma de comunicarse. - Compare los emoji únicos, asegurándose de que aparezcan en el mismo orden. + Verifica si los mismos emojis aparecen en el mismo orden en ambos usuarios. Compare el código con el que se muestra en la pantalla del otro usuario. Los mensajes con este usuario están cifrados Extremo-a-Extremo y no pueden ser leídos por terceros. Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza. @@ -1505,7 +1507,7 @@ Esta sesión es de confianza para mensajería segura porque %1$s (%2$s) la ha verificado: %1$s (%2$s) iniciado sesión con una nueva sesión: Hasta que este usuario confíe en esta sesión, los mensajes enviados hacia y desde ella se etiquetan con advertencias. Alternativamente, puede verificarlo manualmente. - ¡Casi ahí! ¿Es %s muestra el mismo escudo\? + ¡Casi estamos! ¿Muestra %s el mismo tick\? Use una contraseña o clave de recuperación Si no puede acceder a una sesión existente No puedo encontrar secretos almacenados @@ -1560,7 +1562,7 @@ Cifrado no habilitado El cifrado usado por esta sala no es compatible %s creado y configurado la sala. - ¡Casi ahí! ¿El otro dispositivo muestra el mismo escudo\? + ¡Casi estamos! ¿El otro dispositivo muestra el mismo tick\? ¡Casi ahí! Esperando confirmación… No se pudieron importar las claves Mensajes que contienen @room @@ -1822,8 +1824,8 @@ Continuar Los espacios son una nueva manera para agrupar salas y personas. Únete a un Espacio - Crea un Espacio - Añade un Espacio + Crear espacio + Añadir espacio Crea un Espacio Crear un espacio Sincronización inicial: @@ -1991,7 +1993,7 @@ Solo la gente invitada puede encontrar y unirse a l sala Privada (solo mediante invitación) Cualquiera puede solicitar entrar en la sala, serán los miembros quienes decidan aceptar o rechazar la solicitud - Configurar las direcciones para esta sala de forma que otros usuarios puedan encontrarla mediante tu homeserver (%1$s). + Configurar las direcciones para esta sala de forma que otros usuarios puedan encontrarla mediante tu servidor doméstico (%1$s) Ninguna dirección publicada aún, añade una debajo. Las direcciones publicadas pueden ser usadas por cualquiera en cualquier servidor para unirse a tu sala. Para publicar una dirección es necesario configurarla previamente como dirección local. Ver y gestionar direcciones de este espacio. @@ -2018,7 +2020,7 @@ Llamada de audio perdida %d llamadas de audio perdidas - Homeserver API URL + URL de API del servidor doméstico Faltan permisos Para enviar mensajes de voz has de otorgar el permiso de Micrófono. Para llevar a cabo esta acción has de otorgar el permiso de Cámara en las preferencias del sistema. @@ -2036,7 +2038,7 @@ Otros espacios o habitaciones que puede que no conozcas Unirse a la sala de repuesto Tiene borradores sin enviar - Puedes contactarme si tienes dudas de seguimiento + Puedes contactarme si tienes alguna duda Necesitas permiso para actualizar una sala Actualizar el espacio padre automáticamente Invitar usuarios automáticamente @@ -2077,7 +2079,7 @@ Añade algunos detalles para ayudarlo a destacar. Puedes cambiar esto en cualquier momento. Un espacio privado para ti y tus compañeros de equipo Un espacio privado para organizar tus salas - Asegúrate de que son las personas correctas las que tienen acceso a %s. Puedes cambiar esto más tarde. + Asegúrate de que las personas correctas tienen acceso a %s. ¿Con quién estás trabajando\? Para unirte a un espacio existente necesitas una invitación. Puedes cambiar esto más tarde @@ -2178,7 +2180,7 @@ Cambiar el avatar de la sala actual Cambiar su apodo para mostrar solo en la sala actual Establecer el nombre de la sala - Deja de ignorar a un usuario, muestra sus mensajes en el futuro. + Deja de ignorar a un usuario, muestra sus mensajes en el futuro Ignorar a un usuario, ocultándole sus mensajes Espacio que sabes que contiene esta sala Cualquiera puede encontrar el espacio y unirse @@ -2304,4 +2306,181 @@ Filtrar %1$s, %2$s y otros Copiar enlace al hilo + Enviar sticker + Ocurrió un error al cargar el mapa + ¡Estás listo! + Desde un hilo + Los hilos ayudan a mantener sus conversaciones en el tema fijado y son fáciles de organizar. + Puedes apagar esto en cualquier momento en configuración + Tu servidor doméstico actualmente no soporta hilos, por lo tanto esta función podría ser poco fiable. Algunos mensajes en hilos puede que no estén disponibles de forma fiable. %s ¿Deseas activar la función de hilos de todas formas\? + Beta de hilos + Nos estamos acercando a el lanzamiento de una beta pública para hilos. +\n +\nMientras nos preparamos, tenemos que hacer algunos cambios: los hilos creados a partir de ahora se mostrarán como respuestas normales. +\n +\nEsto será una transición debido a que los hilos son ahora parte de la especificación Matrix. + Hilos acercándose a versión beta 🎉 + Ya tengo una cuenta + Crear cuenta + Saltar este paso + Guardar y continuar + Tus preferencias han sido guardadas. + Vámonos + Añadir una foto de perfil + Puedes cambiar esto más tarde + Nombre público + Escoger un nombre público + Tu cuenta %s ha sido creada. + ¡Felicidades! + Dar opinión + BETA + Modo fuera de línea + Presencia + Nosotros no compartimos información con terceros + Nosotros no grabamos o perfilamos ningún dato de cuenta + Ayudar a mejorar ${app_name} + 8 horas + 1 hora + 15 minutos + Permitir acceso + Mostrar ubicaciónes de usuarios en la línea de tiempo + Ubicación en vivo activada + Cargando ubicación en vivo… + Parar + Mostrar burbujas de mensajes + Abrir cámara + Enviar imágenes y vídeos + Subir archivo + Abrir contactos + + %1$d más + %1$d más + + Mostrar menos + Compartir ubicación + Crear encuesta + aquí + Aprender más + Pruébalo + Deshabilitar + + %d cambio de ACL del servidor + %d cambios de ALC de los servidores + + Compartir esta ubicación + Compartir ubicación en tiempo real + Compartir mi ubicación actual + Compartir ubicación + ${app_name} también es estupenda para el trabajo. Es confiada por las organizaciones más seguras del mundo. + Para descubrir contactos ya existentes, tendrás que enviar información de contacto (emails y números de teléfono) a tu servidor de identidad. Aplicamos hashes en tus datos antes de enviarlos por privacidad. + Los hilos son un trabajo en progreso con nuevas y excitantes características nuevas, como notificaciones mejoradas. ¡Nos encantaría escuchar tus comentarios! + Si se activa, siempre aparecerás como fuera de línea para otros usuarios, incluso cuando uses la aplicación. + Los hilos ayudan a mantener tus conversaciones en el asunto y las hacen más fáciles de rastrear. %sHabilitar la función de hilos refrescará la aplicación. Esto podría tardar más en algunas cuentas. + Notificación de sala + Usuarios + Notificar a toda la sala + La compartición de ubicación está en progreso + ${app_name} Ubicación en directo + Cuando se active serás capaz de enviar tu ubicación a cualquier sala + Activar compartir ubicación + Abrir con + ${app_name} no ha podido acceder a tu ubicación. Por favor, inténtalo de nuevo más tarde. + ${app_name} no ha podido acceder a tu ubicación + If te gustaría compartir tu ubicación en tiempo real, ${app_name} necesita permiso de ubicación siempre cuando la aplicación esté en segundo plano +\nSolo acederemos a tu ubicación para la duración que escogas. + Compartir tu ubicación en directo para + Compartir esta ubicación + Compartir ubicación en directo + Compartir mi ubicación actual + Hacer zoom a ubicación actual + Pin de ubicación selecionada en el mapa + Mapa + Compartir ubicación + Ubicación + Compartir ubicación + Los resultados solo se revelan cuando finalices la encuesta + Encuesta cerrada + Los votantes verán los resultados tan pronto como hayan votado + Abrir encuesta + Tipo de encuesta + Editar encuesta + ¿Estás seguro de que quieres eliminar esta encuesta\? No podrás recuperarla cuando se elimine. + Eliminar encuesta + Encuesta finalizada + Voto enviado + Finalizar encuesta + Esto evitará que las personas puedan votar y mostrará los resultados finales de la encuesta. + ¿Finalizar encuesta\? + opción ganadora + Finalizar encuesta + + Resultado final basado en %1$d voto + Resultado final basado en %1$d votos + + + %1$d voto enviado. Vota para ver los resultados + %1$d votos enviados. Vota para ver los resultados + + No hay ningún voto + + Basado en %1$d voto + Basado en %1$d votos + + + %1$d voto + %1$d votos + + + Al menos %1$s opción es requerida + Al menos %1$s opciones son requeridas + + La pregunta no puede estar vacía + CREAR ENCUESTA + AÑADIR OPCIÓN + Opción %1$d + Crear opciones + Pregunta o asunto + Pregunta o asunto de encuesta + Crear Encuesta + Reinicia la aplicación para que el cambio surta efecto. + Ayúdanos a identificar problemas y a mejorar ${app_name} compartiendo datos de uso anónimos. Para comprender como las personas usan múltiples dispositivos, generaremos un identificador aleatorio, compartido por tus dispositivos. +\n +\nPuedes leer todos nuestros términos en %s. + Habilitar matemáticas con LaTeX + (%1$s) + %1$s (%2$s) + Ocurrió un error al reproducir %1$s + Pausar %1$s + Reproducir %1$s + %1$d minutos %2$d segundos + %1$s,%2$s,%3$s + Nota: la applicación se reiniciará + Activar mensajes en hilo + Tu sistema automáticamente enviará registros cuando ocurra un fallo al desencriptar + Reportar errores de desencriptado automáticamente. + No disponible + Fuera de línea + En línea + El servidor doméstico no acepta nombres de usuario con únicamente dígitos. + La encriptación está configurada de manera incorrecta + Sobreescribir color del nick + Restaurar Encriptación + Por favor, contacta con un administrador para restablecer la encriptación a un estado válido. + La encriptación ha sido configurada de forma incorrecta. + Compartieron su localización en vivo + Compartieron su localización + Puedes cambiar esto en todo momento. + Esto se mostrará cuando mandes mensajes. + Llévame a casa + Personalizar perfil + Conectar a servidor + ¿Querías unirte a un servidor ya existente\? + Encriptado extremo-a-extremo, sin requerir un número de teléfono. Sin anuncios o minado de datos. + Elige dónde se almacenan tus conversaciones, dándote control e independencia. Conectado mediante Matrix. + La comunicación segura e independiente que te da el mismo nivel de privacidad que una conversación cara a cara en tu propio hogar. + Enviar emials y números de teléfono a %s + Tus contactos son privados. Para descubrir usuarios desde tus contactos, necesitamos tu permiso para enviar información de contactos a tu servidor de identidad. + BETA + Comentarios de la beta de hilos + Beta de hilos \ No newline at end of file diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 19b4d37102..79eee81fa9 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -743,7 +743,9 @@ \nÜldistest seadistustest saad seda alati muuta. Eira selle kasutaja sõnumeid Lõpeta selle kasutaja eiramine - Selle kasutaja eiramise lõpetamine teeb tema sõnumid uuesti nähtavaks. + Selle kasutaja eiramise lõpetamine teeb tema sõnumid uuesti nähtavaks. +\n +\nPalun arvesta, et samaga käivitud rakendub uuesti ning andmete sünkroniseerimiseks kulub natuke aega. Lõpeta eiramine Tühista kutse Kas oled kindel et sa soovid tühistada kutse sellele kasutajale\? @@ -2457,4 +2459,32 @@ Mine avalehele Isikupärasta oma profiili Lülita välja + Reaalajas asukoha laadimine… + 8 tunni kestel + 1 tunni kestel + 15 minuti jooksul + Jaga oma asukohta reaalajas + (%1$s) + %1$s (%2$s) + %1$s helisõnumi esitamine ei õnnestu + Peata helisõnumi esitus: %1$s + Esita helisõnumit: %1$s + %1$d minutit %2$d sekundit + %1$s, %2$s, %3$s + Jagas oma asukohta + ${app_name} sobib ideaalselt kasutamiseks töökeskkonnas. Ta on kasutusel ka mitmetes üliturvalistes organisatsioonides. + BEETA + Jutulõngade beetaversiooni tagasiside + Jaga tagasisidet + BEETA + Kui see valik on kasutusel, siis sa alati oled teiste jaoks võrgust väljas. Seda ka siis, kui kasutad rakendust. + Ei ole võrgus + Olek võrgus + Sinu koduserver hetkel ei toeta jutulõngasid ning seega antud funktsionaalsus ei pruugi toimida korralikult. Kõik sõnumid jutulõngas ilmselt ei ole loetavad. %sKas sa ikkagi soovid jutulõngad kasutusele võtta\? + Jutulõngade beetaversioon + Jutulõngade beetaversioon + Lisateave + Proovi nüüd + Jutulõngad on hetkel arendusjärgus funktsionaalsus ning samaga lisandub ka senisest parem versioon teavitustest. Me hea meelega tahaksime kuulda, mida sa nendest muutustest arvad! + Jutulõngad aitavad hoida sinu vestlusi teemakohastena ning kergesti jälgitavatena.%sJutulõngade kasutusele võtmisel laadime rakenduse uuesti. Kui sul on väga mahukad kontod, siis võib natuke aega kuluda. \ No newline at end of file diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index 9a23913f76..4d01fe269b 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -1256,7 +1256,9 @@ اگر مدیر کارساز خبر از مورد انتظار بودنش داده، مطمئن شوید که اثر انگشت زیر با اثر انگشت ارائه شده به دستشان مطابق است. می‌تواند به این معنی باشد که کسی شدامدتان را بدخواهانه دستکاری کرده یا تلفنتان، به گواهی فراهم‌شده به دست کارساز دوردست، اطمینان ندارد. با لغو مسدودیت، کاربر می‌تواند مجددا به اتاق اضافه شود. - لغو نادیده‌گیری کاربر سبب می‌شود دوباره همه پیام‌های او نمایش داده شود. + ناچشم‌پوشی این کاربر موجب نمایانی تمامی پیام‌ها از سویش خواهد شد. +\n +\nبه خاطر داشته باشید که این کنش کاره را دوباره آغاز خواهد کرد و ممکن است مدّتی زمان ببرد. نادیده‌گرفتن این کاربر پیام‌هایش را از اتاق‌های مشترکتان حذف خواهد کرد. \n \nهرگاه که بخواهید می‌توانید این کنش را در تنظیمات کلی لغو کنید. @@ -2457,4 +2459,32 @@ مرا به خانه ببر شخصی سازی نمایه از کار انداختن + رشته‌ها کاری در حال پیشرفت با ویژگی‌های جذّاب جدید مانند آگاهی‌های بهبودیافته هستند. دوست داریم بازخوردتان را بشنویم! + کارساز خانگیتان در حال حاضر از رشته‌ها پشتیبانی نمی‌کند؛ بنابراین ممکن است این ویژگی قابل اتّکا نباشد. ممکن است برخی پیام‌های رشته‌ای همواره موجود نباشند. %sدر هر صورت می‌خواهید رشته‌ها را به کار بیندازید؟ + رشته‌ها به روی موضوع نگه داشتن گفت‌وگوها کمک کرده و ردیابیشان را آسان می‌کنند. %sبه کارانداختن رشته‌ها کاره را دوباره آغاز خواهد کرد. ممکن است این کار برای برخی حساب‌ها بیش‌تر طول بکشد. + بار کردن مکان زنده… + ۸ ساعت + ۱ ساعت + ۱۵ دقیقه + هم‌رسانی مکان زنده‌تان برای + (%1$s) + %1$s (%2$s) + ناتوان در پخش %1$s + مکث %1$s + پخش %1$s + %1$d دقیقه و %2$d ثانیه + %1$s، %2$s، %3$s + مکان زنده‌اش را هم‌رساند + ${app_name} برای محل کار نیز عالیست. مورد اعتماد امن‌ترین سازمان‌های جهان است. + آزمایشی + بازخورد رشته‌های آزمایشی + بازخورد دادن + آزمایشی + اگر به کار افتاده باشد، برای دیگر کاربران، همواره برون‌خط ظاهر خواهید شد، حتا هنگام استفاده از برنامه. + حالت برون‌خط + حضور + رشته‌های آزمایشی + رشته‌های آزمایشی + بیش تر بدانید + بیازماییدش \ No newline at end of file diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 33bde2944e..3a8184cef7 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1486,7 +1486,9 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze \n \nEzt a műveletet bármikor visszavonhatja az általános beállításokban. Felhasználó figyelembe vétele - A felhasználó figyelembe vétele után újra meg fog jelenni az összes üzenete. + A felhasználó figyelembe vétele után újra meg fog jelenni az összes üzenete. +\n +\nEz a művelet újraindítja az alkalmazást ami sokáig tarthat. Meghívás visszavonása Biztos, hogy visszavonja a felhasználó meghívását\? a felhasználó kirúgása eltávolítja a szobából. @@ -2457,4 +2459,32 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Vigyél haza Profil személyre szabása Tiltás + Élő földrajzi helyzet meghatározás betöltése… + 8 óra + 1 óra + 15 perc + Folyamatos helyzet megosztása ezért: + (%1$s) + %1$s (%2$s) + Nem lehet lejátszani ezt: %1$s + Megállít: %1$s + Lejátszás: %1$s + %1$d perc %2$d másodperc + %1$s, %2$s, %3$s + A földrajzi helyzetüket folyamatosan megosztják + ${app_name} megállja a helyét a munkahelyen is. A világ legbiztonságosabb szervezetei bíznak meg benne. + Béta + Üzenetszálak fejlesztése folyamatban van, új és izgalmas funkciókkal, mint a fejlesztett értesítések. Szeretnénk hallani a véleményed! + Béta Üzenetszálak visszajelzés + Visszajelzés adása + Béta + Ha engedélyezed úgy látszol majd mások számára, mintha nem kapcsolódnál a hálózathoz még akkor is amikor az alkalmazást használod. + Hálózati kapcsolat nélkül + Állapot + A Matrix szervered jelenleg nem támogatja az üzenetszálakat így ez a funkció nem lesz megbízható. Bizonyos üzenetszálas üzenetek nem jelennek meg megbízhatóan. %sBiztos, hogy így is engedélyezed az üzenetszálakat\? + Béta Üzenetszálak + Az üzenetszálak segítenek a különböző témájú beszélgetések figyelemmel kísérésében. %sÜzenetszálak engedélyezése újraindítja az alkalmazást. Ez néhány fióknál sokáig tarthat. + Béta Üzenetszálak + Tudj meg többet + Próbáld ki \ No newline at end of file diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index 5ca1a361d4..f0533eba10 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -72,7 +72,7 @@ Gabung Tolak Nanti - Kirim catat gangguan + Kirim catatan gangguan Raksasa Kecil Normal @@ -87,7 +87,7 @@ Gandakan ke clipboard Kirim tampilan layar Mohon uraikan kutu tersebut. Apa yang Anda lakukan\? Apa yang Anda harapkan terjadi\? Apa yang sebenarnya terjadi\? - Catat dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk catat dan tangkapan layar, tidak akan terlihat secara umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silakan hapus centang: + Catatan dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk catatan dan tangkapan layar, tidak akan terlihat secara umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silakan hapus centang: Sepertinya Anda mengguncang ponsel akibat frustrasi. Apakah Anda ingin membuka halaman laporan kutu\? Pengiriman laporan kutu gagal (%s) Kemajuan (%s%%) @@ -590,7 +590,9 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Keluarkan pengguna Apakah Anda yakin ingin membatalkan undangan untuk pengguna ini\? Batalkan undangan - Membatalkan abaian pengguna ini akan menampilkan semua pesan dari mereka. + Membatalkan abaian pengguna ini akan menampilkan semua pesan dari mereka. +\n +\nDicatat bahwa tindakan ini akan memulai ulang aplikasi dan mungkin membutuhkan beberapa waktu. Batal pengabaian pengguna Mengabaikan pengguna ini akan menghilangkan pesan mereka dari ruangan yang Anda bagikan. \n @@ -1179,8 +1181,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kirim lampiran Sepertinya server terlalu lama merespons, hal ini dapat disebabkan oleh konektivitas yang buruk atau kesalahan pada server. Silakan coba lagi dalam beberapa saat. Silakan coba lagi setelah Anda menerima syarat dan ketentuan homeserver Anda. - Log verbose akan membantu pengembang dengan menyediakan lebih banyak catat saat Anda mengirim RageShake. Bahkan ketika diaktifkan, aplikasi tidak mencatat isi pesan atau data pribadi lainnya. - Aktifkan catat verbose. + Catatan verbose akan membantu pengembang dengan menyediakan lebih banyak catat saat Anda mengirim RageShake. Bahkan ketika diaktifkan, aplikasi tidak mencatat isi pesan atau data pribadi lainnya. + Aktifkan catatan verbose. Kode verifikasi salah. Kode Sebuah pesan teks telah dikirim ke %s. Silakan masukkan kode verifikasi yang ada di dalamnya. @@ -2285,7 +2287,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Mulai ulang aplikasi ini untuk menerapkan perubahan. Aktifkan matematika LaTeX Anda tidak diperbolehkan untuk bergabung ke ruangan ini - Sistem Anda akan mengirimkan catat secara otomatis ketika sebuah kesalahan dekripsi terjadi + Sistem Anda akan mengirimkan catatan secara otomatis ketika sebuah kesalahan dekripsi terjadi Buat poll Buka kontak Kirim stiker @@ -2345,7 +2347,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Siapa saja yang sering Anda chat\? Anda sudah menampilkan utasan ini! Tampilkan Di Ruangan - Balas Di Utasan + Balas di utasan Perintah \"%s\" dikenal tetapi tidak didukung dalam utasan. Dari sebuah Utasan Tip: Tekan lama pada sebuah pesan dan gunakan “%s”. @@ -2412,4 +2414,32 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kembalikan saya ke beranda Ubah profil Nonaktifkan + Memuat lokasi langsung… + 8 jam + 1 jam + 15 menit + Bagikan lokasi langsung Anda untuk + (%1$s) + %1$s (%2$s) + Tidak dapat memainkan %1$s + Jeda %1$s + Mainkan %1$s + %1$d menit %2$d detik + %1$s, %2$s, %3$s + Membagikan lokasi langsungnya + ${app_name} juga baik untuk tempat kerja. Dipercayai oleh organisasi yang paling aman di dunia. + BETA + Utasan adalah fitur yang sedang dikerjakan dengan fitur baru dan asik. Kami ingin dengar masukan Anda! + Masukan Utasan Beta + Berikan masukan + BETA + Jika diaktifkan, Anda akan selalu terlihat luring kepada pengguna lain, bahkan ketika menggunakan aplikasi. + Mode luring + Presensi + Homeserver Anda saat ini tidak mendukung utasan, jadi fitur ini mungkin tidak andal. Beberapa pesan yang diutas mungkin tidak tersedia. %sApakah Anda ingin mengaktifkan utasan saja\? + Utasan Beta + Utasan membantu percakapan sesuai topik dan mudah untuk dilacak. %sMengaktifkan utasan akan memuat aplikasi ulang. Ini mungkin membutuhkan beberapa waktu untuk beberapa akun. + Utasan Beta + Pelajari lebih lanjut + Coba \ No newline at end of file diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml index 90446c8819..f052e88ee5 100644 --- a/vector/src/main/res/values-is/strings.xml +++ b/vector/src/main/res/values-is/strings.xml @@ -1350,7 +1350,7 @@ Skilaboð frá vélmennum Boð á spjallrás Stikkorð - \@spjallrás + \@room Hópskilaboð Bein skilaboð Notandanafnið mitt @@ -1446,7 +1446,7 @@ Láta mig vita fyrir Uppgötvun Uppfærslur spjallrásar - Skilaboð sem innihalda @spjallrás + Skilaboð sem innihalda @room Þegar spjallrásir eru uppfærðar Heimildir svæðis Ástæða fyrir banni diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 15f08ba6da..66fff2077e 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1519,7 +1519,9 @@ \n \nPuoi annullare questa azione in qualsiasi momento nelle impostazioni generali. Non ignorare più - Se non ignori più l\'utente vedrai di nuovo tutti i suoi messaggi. + Se non ignori più l\'utente vedrai di nuovo tutti i suoi messaggi. +\n +\nNota che quest\'azione riavvierà l\'app e potrebbe richiedere del tempo. Annulla invito Sei sicuro di voler annullare l\'invito per questo utente\? Butta fuori l\'utente @@ -2448,4 +2450,32 @@ Congratulazioni! Personalizza profilo Disattiva + Caricamento posizione in tempo reale… + 8 ore + 1 ora + 15 minuti + Condividi la tua posizione in tempo reale per + (%1$s) + %1$s (%2$s) + Impossibile riprodurre %1$s + Metti in pausa %1$s + Riproduci %1$s + %1$d minuti %2$d secondi + %1$s, %2$s, %3$s + Ha condiviso la sua posizione + ${app_name} è ottimo anche per i luoghi di lavoro. È considerato affidabile dalle più sicure organizzazioni del mondo. + BETA + Le conversazioni sono ancora in sviluppo e ci saranno nuove funzionalità in arrivo, come le notifiche migliorate. Vorremmo sapere la tua opinione! + Feedback della beta conversazioni + Invia un feedback + BETA + Se attiva, apparirai sempre offline agli altri utenti, anche quando usi l\'applicazione. + Modalità offline + Presenza + Il tuo homeserver attualmente non supporta i messaggi in conversazioni, perciò questa funzione sarà inaffidabile. Alcuni messaggi in conversazioni potrebbero non essere disponibili. %sVuoi attivare comunque le conversazioni\? + Beta conversazioni + Le conversazioni ti aiutano a tenere le tue discussioni in tema e rintracciabili. %sL\'attivazione ricaricherà l\'app. Potrebbe richiedere più tempo per alcuni account. + Beta conversazioni + Maggiori informazioni + Provalo \ No newline at end of file diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 672e2472bb..49335373a0 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -512,7 +512,7 @@ セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ - (編集済) + (編集した) 会話を検索… 全てのメッセージ (音量大) 全てのメッセージ @@ -962,7 +962,7 @@ インテグレーション(統合) アプリがバックグラウンドにある場合、着信メッセージは通知されません。 ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 -\nこれは無線とバッテリーの使用量に影響し、$ {app_name}がイベントを待機していることを示す永続的な通知が表示されます。 +\nこれは無線とバッテリーの使用量に影響し、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期をします。 \n端末の状態によっては、OSによって同期が延期される場合があります。 LEDの色、振動、音を選択してください… @@ -2372,4 +2372,4 @@ \nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 スレッドはベータ版になります 🎉 無効にする - \ No newline at end of file + diff --git a/vector/src/main/res/values-lo/strings.xml b/vector/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..72b2ae1d21 --- /dev/null +++ b/vector/src/main/res/values-lo/strings.xml @@ -0,0 +1,2323 @@ + + + ການລົງທະບຽນ Token + ເພີ່ມບັນຊີ + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ຂອງ ${app_name}. ບໍ່ມີບັນຊີ Google ຢູ່ໃນໂທລະສັບ. ກະລຸນາເປີດຕົວຈັດການບັນຊີ ແລະເພີ່ມບັນຊີ Google. + ດຶງຂໍ້ມູນໂທເຄັນ FCMບໍ່ສຳເລັດ: +\n%1$s + ດຶງຂໍ້ມູນໂທເຄັນ FCM ສຳເລັດແລ້ວ: +\n%1$s + Firebase Token + ແກ້ໄຂບໍລິການຫຼິ້ນ + ${app_name} ໃຊ້ Google Play Services ເພື່ອສົ່ງຂໍ້ຄວາມ ແຕ່ເບິ່ງຄືວ່າບໍ່ໄດ້ຖືກຕັ້ງຄ່າຢ່າງຖືກຕ້ອງ: +\n%1$s + Google Play Services APK ສາມາດໃຊ້ໄດ້ ແລະ ອັບເດດແລ້ວ. + ກວດສອບບໍລິການການຫຼິ້ນ + ການແຈ້ງເຕືອນບາງລາຍການຖືກປິດໃຊ້ງານໃນການຕັ້ງຄ່າແບບກຳນົດເອງຂອງທ່ານ. + ສັງເກດເຫັນວ່າຂໍ້ຄວາມບາງປະເພດໄດ້ຖືກຕັ້ງໃຫ້ເປັນແບບບໍ່ມີສຽງ (ຈະສ້າງການແຈ້ງການທີ່ບໍ່ມີສຽງ). + ການຕັ້ງຄ່າແບບກຳນົດເອງ. + ເປີດໃຊ້ + ບໍ່ໄດ້ເປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບລະບົບນີ້. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າ ${app_name}. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ງານສໍາລັບລະບົບນີ້. + ການຕັ້ງຄ່າລະບົບ. + ເປີດໃຊ້ + ການແຈ້ງເຕືອນຖືກປິດການນຳໃຊ້ສຳລັບບັນຊີຂອງທ່ານ. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າບັນຊີ. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ງານສຳລັບບັນຊີຂອງທ່ານ. + ຕັ້ງຄ່າບັນຊີ. + ເປີດການຕັ້ງຄ່າ + ການແຈ້ງເຕືອນຖືກປິດໃຊ້ງານຢູ່ໃນການຕັ້ງຄ່າລະບົບ. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າລະບົບ. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ໃນການຕັ້ງຄ່າລະບົບ. + ການຕັ້ງຄ່າລະບົບ. + ການທົດສອບໜຶ່ງ ຫຼື ຫຼາຍກວ່ານັ້ນບໍ່ສຳເລັດ, ກະລຸນາສົ່ງລາຍງານຂໍ້ຜິດພາດເພື່ອຊ່ວຍພວກເຮົາກວດສອບ. + ການທົດສອບຫນຶ່ງ ຫຼື ຫຼາຍກວ່ານັ້ນບໍ່ສຳເລັດ, ລອງວິທີແກ້ໄຂທີ່ໄດ້ແນະນໍາ. + ການວິນິດໄສພື້ນຖານແມ່ນ OK. ຖ້າທ່ານຍັງບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນ, ກະລຸນາສົ່ງລາຍງານຂໍ້ຜິດພາດເພື່ອຊ່ວຍພວກເຮົາສືບສວນ. + ກຳລັງດຳເນິນການ… (%1$d ຂອງ %2$d) + ດໍາເນີນການທົດສອບ + ການແກ້ໄຂບັນຫາດ້ວຍການວິນິດໄສ + ແກ້ໄຂບັນຫາການແຈ້ງເຕືອນ + ຄໍາສໍາຄັນບໍ່ສາມາດມີ \'%s\' + ຄໍາສໍາຄັນບໍ່ສາມາດເລີ່ມຕົ້ນດ້ວຍ \'.\' + ເພີ່ມຄໍາສໍາຄັນໃຫມ່ + ຄໍາສໍາຄັນຂອງທ່ານ + ແຈ້ງໃຫ້ຂ້າພະເຈົ້າຊາບສໍາລັບ + ອື່ນໆ + ການກ່າວເຖິງແລະຄໍາສໍາຄັນ + ການແຈ້ງເຕືອນເລີ່ມຕົ້ນ + ເປີດໃຊ້ການແຈ້ງເຕືອນອີເມວສຳລັບ %s + ເພື່ອຮັບອີເມວພ້ອມການແຈ້ງເຕືອນ, ກະລຸນາເຊື່ອມຕໍ່ອີເມວເຂົ້າບັນຊີ Matrix ຂອງທ່ານ + ການແຈ້ງເຕືອນທາງອີເມວ + ຄວາມສຳຄັນຂອງການແຈ້ງເຕືອນຕາມເຫດການ + ການຕັ້ງຄ່າການແຈ້ງເຕືອນຂັ້ນສູງ + ໃຫ້ແນ່ໃຈວ່າທ່ານໄດ້ກົດໃສ່ການເຊື່ອມຕໍ່ໃນອີເມວທີ່ພວກເຮົາໄດ້ສົ່ງໃຫ້ທ່ານ. + ລຶບ ອອກບໍ%s\? + ເບີໂທລະສັບ + ບໍ່ມີອີເມວໃດຖືກເພີ່ມໃສ່ບັນຊີຂອງທ່ານ + ທີ່ຢູ່ອີເມວ + ສະແດງຂໍ້ມູນລະບົບການຕັ້ງຄ່າແອັບພລິເຄຊັນ. + ຂໍ້ມູນການສະຫມັກ + ເພີ່ມເບີໂທລະສັບ + ບໍ່ມີເບີໂທລະສັບທີ່ຖືກເພີ່ມໃສ່ບັນຊີຂອງທ່ານ + ເພີ່ມທີ່ຢູ່ອີເມວ + ສະແດງຊື່ + ຮູບໂປຣໄຟລ໌ + ໄດ້ອອກຈາກລະບົບແລ້ວ! + ອອກຈາກຫ້ອງແລ້ວ! + ເພີ່ມໃສ່ໜ້າຈໍຫຼັກ + ບໍ່ມີ + ການກ່າວເຖິງ & ຄຳສັບສຳຄັນເທົ່ານັ້ນ + ຂໍ້ຄວາມທັງໝົດ + ບໍ່ມີຜົນ + ການກັ່ນຕອງຫ້າມຜູ້ໃຊ້ + ການກັ່ນຕອງສະມາຊິກຫ້ອງ + ຊອກຫາ + homeserver ຂອງທ່ານບໍ່ຮອງຮັບກະທູ້ໃນປັດຈຸບັນ, ດັ່ງນັ້ນຄຸນສົມບັດນີ້ອາດຈະບໍ່ໜ້າເຊື່ອຖື. ບາງຂໍ້ຄວາມທີ່ເປັນກະທູ້ອາດຈະບໍ່ມີຄວາມໜ້າເຊື່ອຖືໄດ້. %sທ່ານຕ້ອງການເປີດໃຊ້ຫົວຂໍ້ແນວໃດ\? + ກະທູ້ເບຕ້າ + ກະທູ້ຊ່ວຍໃຫ້ການສົນທະນາຂອງທ່ານຢູ່ໃນຫົວຂໍ້ແລະງ່າຍຕໍ່ການຕິດຕາມ. %s ການເປີດໃຊ້ງານກະທູ້ຈະໂຫຼດແອັບຄືນໃໝ່. ອັນນີ້ອາດຈະໃຊ້ເວລາດົນກວ່າສໍາລັບບາງບັນຊີ. + ກະທູ້ເບຕ້າ + ພວກເຮົາໃກ້ຈະອອກ ຈາກກະທູ້ Beta ສາທາລະນະ. +\n +\nໃນຂະນະທີ່ພວກເຮົາກະກຽມສໍາລັບສິ່ງນີ້, ພວກເຮົາຈໍາເປັນຕ້ອງເຮັດການປ່ຽນແປງບາງຢ່າງ: ກະທູ້ທີ່ສ້າງຂຶ້ນກ່ອນນີ້ຈະຖືກສະແດງເປັນການຕອບປົກກະຕິ. +\n +\nສິ່ງນີ້ຈະເປັນການປ່ຽນແປງຄັ້ງດຽວເນື່ອງຈາກກະທູ້ເປັນສ່ວນໜຶ່ງຂອງຂໍ້ມູນສະເພາະຂອງ Matrix. + ກະທູ້ໃກ້ຮອດເບຕ້າ 🎉 + ຈາກກະທູ້ + ຄຳແນະນຳ: ແຕະໃສ່ຂໍ້ຄວາມດົນໆ ແລະໃຊ້ “%s”. + ກະທູ້ຊ່ວຍໃຫ້ການສົນທະນາຂອງທ່ານຢູ່ໃນຫົວຂໍ້ ແລະ ງ່າຍຕໍ່ການຕິດຕາມ. + ຈັດລະບຽບການສົນທະນາດ້ວຍກະທູ້ + ສະແດງຫົວຂໍ້ທັງໝົດທີ່ທ່ານໄດ້ເຂົ້າຮ່ວມ + ກະທູ້ຂອງຂ້ອຍ + ສະແດງຫົວຂໍ້ທັງໝົດຈາກຫ້ອງປັດຈຸບັນ + ກະທູ້ທັງໝົດ + ການກັ່ນຕອງ + ກະທູ້ + ກະທູ້ + ການກັ່ນຕອງກະທູ້ຢູ່ໃນຫ້ອງ + + ເລືອກ %d ແລ້ວ + + ປ່ຽນຫົວຂໍ້ + ຍົກລະດັບພື້ນທີ່ + ຍົກລະດັບຫ້ອງ + ສົ່ງເຫດການ m.room.server_acl + ປ່ຽນການອະນຸຍາດ + ປ່ຽນຊື່ພຶ້ນທີ່ + ປ່ຽນຊື່ຫ້ອງ + ປ່ຽນການເບິ່ງເຫັນປະຫວັດ + ເປີດການເຂົ້າລະຫັດພື້ນທີ່ + ເປີດການເຂົ້າລະຫັດຫ້ອງ + ປ່ຽນທີ່ຢູ່ຫຼັກສຳລັບພື້ນທີ່ + ປ່ຽນທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງ + ປ່ຽນຮູບແທນຕົວຂອງພຶ້ນທີ່ + ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + ແກ້ໄຂ widget + ແຈ້ງເຕືອນທຸກຄົນ + ລຶບຂໍ້ຄວາມທີ່ຄົນອື່ນສົ່ງມາ + ຫ້າມຜູ້ໃຊ້ + ລຶບຜູ້ໃຊ້ອອກ + ປ່ຽນການຕັ້ງຄ່າ + ເຊີນຜູ້ໃຊ້ + ສົ່ງຂໍ້ຄວາມ + ບົດບາດເລີ່ມຕົ້ນ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ປັບປຸງບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່ນີ້ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ປັບປຸງບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ + ເລືອກບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່ນີ້ + ເລືອກບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ + ການອະນຸຍາດ + ເບິ່ງ ແລະອັບເດດບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່. + ເບິ່ງ ແລະ ອັບເດດບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ. + ການອະນຸຍາດພື້ນທີ່ + ການອະນຸຍາດຫ້ອງ + ຍອມຮັບໃບຢັ້ງຢືນຖ້າຜູ້ເບິ່ງແຍງເຊີບເວີໄດ້ເຜີຍແຜ່ລາຍນິ້ວມືທີ່ກົງກັບຂ້າງເທິງ. + ຖ້າຫາກຜູ້ເບິ່ງແຍງເຊີບເວີໄດ້ບອກວ່າເປັນເຊັ່ນນີ້, ໃຫ້ແນ່ໃຈວ່າລາຍນິ້ວມືຂ້າງລຸ່ມນີ້ກົງກັບລາຍນິ້ວມືທີ່ເຂົາເຈົ້າສະໜອງໃຫ້. + ອັນນີ້ອາດໝາຍຄວາມວ່າມີຄົນຂັດຂວາງການຮັບສົ່ງຂອງທ່ານຢ່າງເຈດຕະນາ, ຫຼືວ່າໂທລະສັບຂອງທ່ານບໍ່ມີຄວາມໜ້າເຊື່ອຖືການຢັ້ງຢືນທີ່ສະໜອງໃຫ້ໂດຍເຊີບເວີທາງໄກ. + ບໍ່ສາມາດຢືນຢັນຕົວຕົນຂອງເຊີບເວີທາງໄກໄດ້. + ລາຍນິ້ວມື (%s): + ບໍ່ສົນໃຈ + ອອກຈາກລະບົບ + ເຊື່ອຖືບໍ່ໄດ້ + ເຊື່ອຖືໄດ້ + + %d ຂໍ້ຄວາມໃໝ່ + + ການຕັ້ງຄ່າເຂົ້າລະຫັດບໍ່ຖືກຕ້ອງ ດັ່ງນັ້ນທ່ານບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້. ກົດເພື່ອເປີດການຕັ້ງຄ່າ. + ຕັ້ງຄ່າການເຂົ້າລະຫັດບໍ່ຖືກຕ້ອງ ດັ່ງນັ້ນທ່ານບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້. ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບເພື່ອກູ້ຄືນການເຂົ້າລະຫັດໃຫ້ເປັນສະຖານະທີ່ຖືກຕ້ອງ. + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ໂພສໃສ່ຫ້ອງນີ້. + %1$s, %2$s ແລະ ອື່ນໆ + %1$s ແລະ %2$s + %1$s & %2$s ແລະ ຄົນອື່ນໆກຳລັງພິມ… + %1$s & %2$s ກຳລັງພິມ… + %s ກຳລັງພິມ… + ການຍົກເລີກການຫ້າມຜູ້ໃຊ້ຈະອະນຸຍາດໃຫ້ພວກເຂົາເຂົ້າຮ່ວມພື້ນທີ່ອີກເທື່ອຫນຶ່ງ. + ການຍົກເລີກຜູ້ໃຊ້ຈະອະນຸຍາດໃຫ້ພວກເຂົາເຂົ້າຮ່ວມຫ້ອງອີກເທື່ອຫນຶ່ງ. + ການຫ້າມຜູ້ໃຊ້ຈະລຶບພວກເຂົາອອກຈາກພື້ນທີ່ນີ້ ແລະປ້ອງກັນບໍ່ໃຫ້ເຂົາເຈົ້າເຂົ້າຮ່ວມອີກຄັ້ງ. + ການຫ້າມຜູ້ໃຊ້ຈະລຶບເຂົາເຈົ້າອອກຈາກຫ້ອງນີ້ ແລະປ້ອງກັນບໍ່ໃຫ້ເຂົາເຈົ້າເຂົ້າຮ່ວມອີກຄັ້ງ. + ຍົກເລີກການຫ້າມຜູ້ໃຊ້ + ເຫດຜົນທີ່ຫ້າມ + ຫ້າມຜູ້ໃຊ້ + ຜູ້ໃຊ້ຈະຖືກລຶບອອກຈາກພື້ນທີ່ນີ້. +\n +\nເພື່ອປ້ອງກັນບໍ່ໃຫ້ພວກເຂົາເຂົ້າຮ່ວມອີກເທື່ອຫນຶ່ງ, ທ່ານຄວນຫ້າມພວກເຂົາແທນ. + ຜູ້ໃຊ້ຈະຖືກລຶບອອກຈາກຫ້ອງນີ້. +\n +\nເພື່ອປ້ອງກັນບໍ່ໃຫ້ພວກເຂົາເຂົ້າຮ່ວມອີກເທື່ອຫນຶ່ງ, ທ່ານຄວນຫ້າມພວກເຂົາແທນ. + ເຫດຜົນທີ່ຈະລຶບອອກ + ເອົາຜູ້ໃຊ້ອອກ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການຍົກເລີກການເຊີນຜູ້ໃຊ້ນີ້\? + ຍົກເລີກການເຊີນ + ຍົກເລີກການບໍ່ສົນໃຈ + ການບໍ່ສົນໃຈຜູ້ໃຊ້ນີ້ຈະສະແດງຂໍ້ຄວາມທັງໝົດຈາກເຂົາເຈົ້າອີກຄັ້ງ. +\n +\nກະລຸນາຮັບຊາບວ່າຄຳສັ່ງນີ້ຈະປິດເປີດແອັບຄືນໃໝ່ ແລະ ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ບໍ່ສົນໃຈ + ການບໍ່ສົນໃຈຜູ້ໃຊ້ນີ້ຈະລຶບຂໍ້ຄວາມຂອງເຂົາເຈົ້າອອກຈາກຫ້ອງທີ່ທ່ານແບ່ງປັນ. +\n +\nທ່ານສາມາດຍົກເລີກການກະທຳນີ້ໄດ້ທຸກເວລາໃນການຕັ້ງຄ່າທົ່ວໄປ. + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ຫຼຸດລະດັບ + ທ່ານຈະບໍ່ສາມາດຍົກເລີກການປ່ຽນແປງນີ້ໄດ້ໃນຂະນະທີ່ທ່ານກໍາລັງ ລຸດລະດັັບຕົວທ່ານເອງ, ຖ້າທ່ານເປັນຜູ້ໃຊ້ສິດທິພິເສດສຸດທ້າຍຢູ່ໃນຫ້ອງ, ມັນຈະເປັນໄປບໍ່ໄດ້ທີ່ຈະຄືນສິດທິພິເສດ. + ຫຼຸດລະດັບຕົວເອງບໍ\? + ທ່ານຈະບໍ່ສາມາດຍົກເລີກການປ່ຽນແປງນີ້ໄດ້ເນື່ອງຈາກທ່ານກໍາລັງສົ່ງເສີມຜູ້ໃຊ້ໃຫ້ມີລະດັບພະລັງງານດຽວກັນກັບຕົວທ່ານເອງ. +\nທ່ານແນ່ໃຈບໍ່\? + ກ່າວເຖິງ + ລຶບອອກຈາກການສົນທະນາ + ຍົກເລີກການຫ້າມ + ຫ້າມ + ຍົກເລີກການເຊີນ + ເຊີນ + ຂໍ້ຄວາມໂດຍກົງ + ຫ້ອງນີ້ບໍ່ແມ່ນສາທາລະນະ. ທ່ານຈະບໍ່ສາມາດເຂົ້າຮ່ວມຄືນໃໝ່ໄດ້ໂດຍບໍ່ມີການເຊີນ. + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກຫ້ອງ\? + ອອກຈາກຫ້ອງ + + ສະມາຊິກ %d + + ໄປຫາລາຍການທີ່ຍັງບໍ່ໄດ້ອ່ານ + ສະມາຊິກ + ສືບຕໍ່ + ບໍ່ + ແມ່ນແລ້ວ + ອະນຸຍາດໃຫ້ເຂົ້າເຖິງລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານ. + ເພື່ອສະແກນລະຫັດ QR, ທ່ານຈໍາເປັນຕ້ອງອະນຸຍາດໃຫ້ເຂົ້າເຖິງກ້ອງຖ່າຍຮູບ. + ${app_name} ຕ້ອງການການອະນຸຍາດເພື່ອເຂົ້າເຖິງກ້ອງຖ່າຍຮູບ ແລະໄມໂຄຣໂຟນຂອງທ່ານເພື່ອໂທວິດີໂອ. +\n +\nກະລຸນາອະນຸຍາດໃຫ້ເຂົ້າເຖິງໃນpop-ups ຖັດໄປເພື່ອໃຫ້ສາມາດໂທອອກໄດ້. + ${app_name} ຕ້ອງການການອະນຸຍາດເພື່ອເຂົ້າເຖິງໄມໂຄຣໂຟນຂອງທ່ານເພື່ອປະຕິບັດການໂທສຽງ. + ຂໍ້ມູນ + ກຳລັງວາງສາຍ… + ທາງໄກບໍ່ສາມາດຮັບໄດ້. + ບໍ່ມີຄໍາຕອບ + ຜູ້ໃຊ້ທີ່ທ່ານໂທຫາບໍ່ຫວ່າງ. + ຜູ້ໃຊ້ບໍ່ຫວ່າງ + ທ່ານຖືສາຍການໂທ + %s ຖືສາຍ + ຖື + ປະຫວັດຫຍໍ້ + ໂທສຽງດ້ວຍ %s + ໂທວິດີໂອດ້ວຍ %s + ກຳລັງດຳເນີນການໂທດ້ວຍວິດີໂອ… + ກຳລັງດຳເນີນການໂທ… + ສາຍໂທເຂົ້າ + ສາຍເຂົ້າວິດີໂອ + + %d ບໍ່ໄດ້ຮັບສາຍວິດີໂອ + + + %d ສາຍບໍ່ໄດ້ຮັບ + + ສິ້ນສຸດການໂທ + ກຳລັງໂທເຂົ້ົ້າ… + ກຳລັງເຊື່ອມຕໍ່ການໂທ… + ໂທ + ເລືອກສຽງເອີ້ນເຂົ້າສໍາລັບການໂທ: + ສຽງເອີ້ນເຂົ້າ + ໃຊ້ສຽງເອີ້ນເຂົ້າ ${app_name} ເລີ່ມຕົ້ນສຳລັບສາຍໂທເຂົ້າ + ຂໍການຢືນຢັນກ່ອນເລີ່ມການໂທ + ປ້ອງກັນການໂທຫາໂດຍບັງເອີນ + ໂທ + ຂະຫນາດນ້ອຍ + ຂະຫນາດກາງ + ຂະຫນາດໃຫຍ່ + ຕົ້ນສະບັບ + + %d ການປ່ຽນແປງສະມາຊິກ + + ກະລຸນາເປີດ ${app_name} ໃນອຸປະກອນອື່ນທີ່ສາມາດຖອດລະຫັດຂໍ້ຄວາມໄດ້ເພື່ອໃຫ້ສາມາດສົ່ງລະຫັດໄປຫາລະບົບນີ້. + ຮ້ອງຂໍລະຫັດການເຂົ້າລະຫັດຄືນໃໝ່ຈາກລະບົບອື່ນຂອງທ່ານ. + ມີການຮ້ອງຂໍຫຼາຍເກີນໄປ + ບໍ່ມີ JSON ທີ່ຖືກຕ້ອງ + JSON ບໍ່ຖືກຕ້ອງ + ບໍ່ໄດ້ຮັບອະນຸຍາດ, ຂາດຂໍ້ມູນການພິສູດຢືນຢັນທີ່ຖືກຕ້ອງ + SSL ຜິດພາດ. + SSL ຜິດພາດ: ຂ້ມູນການເບິ່ງເຫັນຍັງບໍ່ໄດ້ຮັບການກວດສອບ. + ເລືອກ homeserver + ບໍ່ສາມາດເຂົ້າຫາ homeserver ທີ່ URL %s ໄດ້. ກະລຸນາກວດເບິ່ງລິ້ງຂອງທ່ານ ຫຼືເລືອກ homeserver ດ້ວຍຕົນເອງ. + ບໍ່ສາມາດເຂົ້າຫາ homeserver ຢູ່ URL ນີ້, ກະລຸນາກວດເບິ່ງ + ນີ້ບໍ່ແມ່ນທີ່ຢູ່ເຊີບເວີ Matrix ທີ່ຖືກຕ້ອງ + ກະລຸນາໃສ່ URL ທີ່ຖືກຕ້ອງ + ກະລຸນາກວດເບິ່ງ ແລະຍອມຮັບນະໂຍບາຍຂອງ homeserver ນີ້: + ການກວດສອບທີ່ຢູ່ອີເມວບໍ່ສຳເລັດ: ໃຫ້ແນ່ໃຈວ່າທ່ານໄດ້ກົດໃສ່ການເຊື່ອມຕໍ່ໃນອີເມວ + ຕ້ອງໃສ່ອິເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ. + homeserver ນີ້ຕ້ອງການໃຫ້ແນ່ໃຈວ່າທ່ານບໍ່ແມ່ນຫຸ່ນຍົນ + ລືມລະຫັດຜ່ານ\? + ເບີໂທລະສັບນີ້ຖືກກໍານົດໄວ້ແລ້ວ. + ທີ່ຢູ່ອີເມວນີ້ຖືກກຳນົດແລ້ວ. + ອັນນີ້ເບິ່ງຄືວ່າບໍ່ແມ່ນທີ່ຢູ່ອີເມວທີ່ຖືກຕ້ອງ + ຊື່ຜູ້ໃຊ້ ແລະ/ຫຼືລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ + ສົ່ງ + ເຂົ້າສູ່ລະບົບດ້ວຍການເຂົ້າສູ່ລະບົບຄັ້ງດຽວ + ເຂົ້າສູ່ລະບົບ + ຂໍອະໄພ, ບໍ່ພົບແອັບພລິເຄຊັນພາຍນອກເພື່ອເຮັດສຳເລັດການດຳເນິນການນີ້. + ໃນປັດຈຸບັນທ່ານບໍ່ໄດ້ເປີດໃຊ້ສະຕິກເກີແພັກເກັດໃດໆ. +\n +\nເພີ່ມບາງອັນດຽວນີ້ບໍ\? + ໃຊ້ເປັນຄ່າເລີ່ມຕົ້ນ ແລະ ຢ່າຖາມອີກ + ຖາມທຸກຄັ້ງ + ເອົາວິດີໂອ + ຖ່າຍຮູບ + ຖ່າຍຮູບ ຫຼືວິດີໂອ + ສົ່ງສະຕິກເກີ + ສົ່ງໄຟລ໌ + ເປີດ HD + ປິດ HD + ດ້ານຫລັງ + ດ້ານໜ້າ + ສະຫຼັບກ້ອງຖ່າຍຮູບ + ຊຸດຫູຟັງໄຮ້ສາຍ + ຊຸດຫູຟັງ + ລໍາໂພງ + ໂທລະສັບ + ເລືອກອຸປະກອນສຽງ + ການສ້າງການເຊື່ອມຕໍ່ແບບສົດໆບໍ່ສຳເລັດ. +\nກະລຸນາຮ້ອງຂໍໃຫ້ຜູ້ເບິ່ງແຍງ homeserver ຂອງທ່ານເພື່ອກໍານົດຄ່າເຊີບເວີ TURN ເພື່ອໃຫ້ການໂທເຮັດວຽກໄດ້ຢ່າງຫມັ້ນຄົງ. + ${app_name} ໂທລົ້ມເຫລວ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເລີ່ມການໂທດ້ວຍວິດີໂອ\? + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເລີ່ມການໂທສຽງ\? + ສົ່ງສຽງ + ລີ່ມການໂທວິດີໂອ + ເລີ່ມການໂທດ້ວຍສຽງ + ຊອກຫາ + API URL ຂອງສະຖານີ + Homeserver URL + ອອກຈາກລະບົບ + ຊື່ຜູ້ໃຊ້ + ເຂົ້າຮ່ວມຫ້ອງ + ຄວາມຄືບໜ້າ (%s%%) + ບໍ່ສາມາດສົ່ງລາຍງານຂໍ້ຜິດພາດໄດ້ (%s) + ໄດ້ສົ່ງລາຍງານຂໍ້ຜິດພາດໄປແລ້ວ + ສັ່ນເພື່ອລາຍງານຂໍ້ຜິດພາດ + ແອັບພລິເຄຊັນໄດ້ຂັດຂ້ອງໃນຄັ້ງທີ່ຜ່ານມາ. ທ່ານຕ້ອງການເປີດໜ້າຈໍລາຍງານການຂັດຂ້ອງບໍ\? + ເບິ່ງຄືວ່າທ່ານກຳລັງສັ່ນໂທລະສັບດ້ວຍຄວາມຫງຸດຫງິດ. ທ່ານຕ້ອງການເປີດໜ້າຈໍລາຍງານຂໍ້ຜິດພາດບໍ\? + ເພື່ອວິນິດໄສບັນຫາ, ບັນທຶກຈາກລູກຄ້ານີ້ຈະຖືກສົ່ງພ້ອມກັບບົດລາຍງານຂໍ້ຜິດພາດນີ້. ລາຍງານຂໍ້ຜິດພາດນີ້, ລວມທັງບັນທຶກ ແລະພາບໜ້າຈໍ, ຈະບໍ່ປາກົດໃຫ້ສາທາລະນະເຫັນໄດ້. ຖ້າທ່ານຕ້ອງການພຽງແຕ່ສົ່ງຂໍ້ຄວາມຂ້າງເທິງ, ກະລຸນາ ຢາກົດເລືອກ: + ອະທິບາຍບັນຫາຂອງທ່ານຢູ່ບ່ອນນີ້ + ຖ້າເປັນໄປໄດ້, ກະລຸນາຂຽນຄໍາອະທິບາຍເປັນພາສາອັງກິດ. + ກະລຸນາອະທິບາຍຂໍ້ບົກພ່ອງ. ທ່ານເຮັດຫຍັງລົງໄປ\?ທ່ານຄາດວ່າຈະມີຫຍັງເກີດຂຶ້ນ\? ທີ່ຈິງແລ້ວເກີດຫຍັງຂຶ້ນ\? + ລາຍງານຂໍ້ຜິດພາດ + ສົ່ງຮູບໜ້າຈໍ + ສົ່ງປະຫວັດການຮ້ອງຂໍການແບ່ງປັນທີ່ສໍາຄັນ + ສົ່ງບັນທຶກການຂັດຂ້ອງ + ສົ່ງບັນທຶກ + ພື້ນທີ່ + ຊຸມຊົນ + ສະແດງໃຫ້ເຫັນຫ້ອງທັງຫມົດໃນລະບົບຫ້ອງ, ລວມທັງຫ້ອງທີ່ມີເນື້ອຫາຊັດເຈນ. + ສະແດງຫ້ອງທີ່ມີເນື້ອຫາບໍ່ຈະແຈ້ງ + ຫ້ອງ + ບໍ່ມີຜົນໄດ້ຮັບອີກຕໍ່ໄປ + ບໍ່ມີຜົນໄດ້ຮັບ + ຕິດຕໍ່ Matrix ເທົ່ານັ້ນ + ການສົນທະນາ + ຫ້ອງແນະນຳ + ການແຈ້ງເຕືອນລະບົບ + ບູລິມະສິດຕໍ່າ + ເຊີນ + ກັ່ນຕອງຊື່ຫ້ອງ + ຫ້ອງ + ຄົນ + ລາຍການທີ່ມັກ + ການແຈ້ງເຕືອນ + ຄ່າໃໝ່ + ຄວາມສໍາເລັດ + ຜິດພາດ + ການຢືນຢັນ + ສຳເນົາລິ້ງໃສ່ກະທູ້ + ເບິ່ງຢູ່ໃນຫ້ອງ + ສຳເນົາໃສ່ຄລິບບອດແລ້ວ + ສຶກສາເພີ່ມເຕີມ + ຕັ້ງຄ່າຄືນໃໝ່ + ປິດ + ຫຼິ້ນ + ຕັດການເຊື່ອມຕໍ່ + ຖອນຄືນ + ປ່ຽນຊື່ + ລຶບ + ແບ່ງປັນ + ດາວໂຫຼດ + ວົງຢືມ + ສົ່ງ + ອອກໄປ + ບັນທຶກ + ຍົກເລີກ + ເຊີນ + ຫຼຸດລົງ + ບໍ່ສົນໃຈ + ຂ້າມ + ຍອມຮັບ + ປະຕິເສດ + ເຂົ້າຮ່ວມ + ລຶບອອກ + ປ່ຽນແປງ + ເຫັນດີ + ລອງໃຊ້ເບິ່ງ + ບໍ່ແມ່ນຕອນນີ້ + ປິດໃຊ້ງານ + ເປີດໃຊ້ + ຍົກເລີກການເຜີຍແຜ່ + ສະຫຼັບ + ເພີ່ມ + ສຳເນົາ + ປິດ + ເປີດ + ໝາຍວ່າອ່ານແລ້ວ + ຕອບກັບດ່ວນ + ໝາຍທັງໝົດວ່າອ່ານແລ້ວ + ເບິ່ງກະທູ້ + ໂທວິດີໂອ + ໂທສຽງ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກລະບົບ\? + ອກຈາກລະບົບ + ວາງສາຍ + ຫຼຸດລົງ + ຍອມຮັບ + ສຳເລັດແລ້ວ + ຫຼື + ລຶບວິດເຈັດບໍ່ສຳເລັດ + ເພີ່ມວິດເຈັດບໍ່ສຳເລັດ + ທ່ານບໍ່ສາມາດໂທຫາຕົວທ່ານເອງໄດ້, ລໍຖ້າໃຫ້ຜູ້ເຂົ້າຮ່ວມທີ່ຈະຮັບເອົາການເຊີນ + ທ່ານບໍ່ສາມາດໂທຫາຕົວທ່ານເອງໄດ້ + ກອງປະຊຸມໃຊ້ນະໂຍບາຍຄວາມປອດໄພ ແລະອະນຸຍາດ Jitsi. ຄົນທັງໝົດຢູ່ໃນຫ້ອງໃນປັດຈຸບັນຈະເຫັນຄຳເຊີນໃຫ້ເຂົ້າຮ່ວມ ໃນຂະນະທີ່ການປະຊຸມຂອງທ່ານກຳລັງເກີດຂຶ້ນ. + ເລີ່ມການປະຊຸມສຽງ + ເລີ່ມການປະຊຸມວິດີໂອ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເລີ່ມການໂທ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ໂທຢູ່ໃນຫ້ອງນີ້ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເລີ່ມການປະຊຸມທາງໂທລະສັບ + ທ່ານບໍ່ມີສິດໃນການເລີ່ມການໂທປະຊຸມຢູ່ໃນຫ້ອງນີ້ + ຂາດການອະນຸຍາດ + ເພື່ອສົ່ງຂໍ້ຄວາມສຽງ, ກະລຸນາອະນຸຍາດໄມໂຄຣໂຟນ. + ເພື່ອດໍາເນີນການນີ້, ກະລຸນາອະນຸຍາດກ້ອງຖ່າຍຮູບຈາກການຕັ້ງຄ່າລະບົບ. + ຂາດການອະນຸຍາດບາງຢ່າງເພື່ອດໍາເນີນການນີ້, ກະລຸນາໃຫ້ການອະນຸຍາດຈາກການຕັ້ງຄ່າລະບົບ. + ພື້ນທີ່ + ເລີ່ມການສົນທະນາ + ລາຍງານເນື້ອຫາ + ບໍ່ມີ + ເບິ່ງແຫຼ່ງທີ່ຖອດລະຫັດ + ເບິ່ງແຫຼ່ງ + ລິ້ງຖາວອນ + ຕໍ່ມາ + ຕົກລົງ + ກຳລັງໂຫລດ… + ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ຂອງທ່ານເວັ້ນເສຍແຕ່ທ່ານຈະສໍາຮອງລະຫັດຂອງທ່ານກ່ອນທີ່ຈະອອກຈາກລະບົບ. + ສຳຮອງ + ທ່ານແນ່ໃຈບໍ່\? + ກຳລັງສຳຮອງຂໍ້ມູນ… + ຂ້ອຍບໍ່ຕ້ອງການຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງຂ້ອຍ + ສຳຮອງຂໍ້ມູນກະເເຈຄວາມປອດໄພ ຄວນມີການເຄື່ອນໄຫວຢູ່ໃນທຸກລະບົບຂອງທ່ານເພື່ອຫຼີກເວັ້ນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານ. + ການສຳຮອງຂໍ້ມູນກະແຈກຳລັງດຳເນີນຢູ່. ຖ້າທ່ານອອກຈາກລະບົບດຽວນີ້, ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານ. + ທ່ານຈະສູນເສຍຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ຫາກທ່ານອອກຈາກລະບົບດຽວນີ້ + ໃຊ້ ກະເເຈສຳນອງ + ສໍາຮອງກະແຈ + ສົ່ງສະຕິກເກີ + ລາຍງານຂໍ້ຜິດພາດ + ການຕັ້ງຄ່າ + ການແຈ້ງເຕືອນສຽງງຽບ + ການແຈ້ງເຕືອນສຽງລົບກວນ + ກຳລັງຟັງການແຈ້ງເຕືອນ + ຟັງເຫດການ + ຮູບແບບຫົວຂໍ້ສີດໍາ + ຮູບແບບສີສັນມືດ + ຮູບແບບສີສັນແຈ້ງ + ຄ່າເລີ່ມຕົ້ນຂອງລະບົບ + ທ່ານໄດ້ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ (ລະບົບທີ່ບໍ່ສາມາດຮັບຮູ້ໄດ້ %1$s). + %1$s ເປີດໃຊ້ການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ (ລະບົບທີ່ບໍ່ສາມາດຮັບຮູ້ໄດ້ %2$s). + ທ່ານໄດ້ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. + %1$s ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. + ທ່ານໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມທີ່ນີ້. + %1$s ໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມທີ່ນີ້. + ທ່ານໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ຂອງຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ຂອງຫ້ອງນີ້. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ຫຼັກ ແລະທາງເລືອກສຳລັບຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ຕົ້ນຕໍ ແລະທາງເລືອກສຳລັບຫ້ອງນີ້. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ທາງເລືອກສຳລັບຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ທາງເລືອກສຳລັບຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບທີ່ຢູ່ສຳຮອງ %1$s ສໍາລັບຫ້ອງນີ້ອອກ. + + + %1$s ໄດ້ລຶບທີ່ຢູ່ສຳຮອງ %2$s ອອກຈາກຫ້ອງນີ້. + + + ທ່ານໄດ້ເພີ່ມທີ່ຢູ່ສຳຮອງ %1$s ສໍາລັບຫ້ອງນີ້. + + + %1$s ໄດ້ເພີ່ມທີ່ຢູ່ສຳຮອງ %2$s ສໍາລັບຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບທີ່ຢູ່ຫຼັກຂອງຫ້ອງນີ້ອອກ. + %1$s ລຶບທີ່ຢູ່ຫຼັກຂອງຫ້ອງນີ້ອອກ. + ທ່ານຕັ້ງຄ່າທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງນີ້ເປັນ %1$s. + %1$s ຕັ້ງຄ່າທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງນີ້ເປັນ %2$s. + ທ່ານໄດ້ເພີ່ມ %1$s ແລະເອົາອອກ %2$sທີ່ຢູ່ຂອງຫ້ອງນີ້. + %1$s ເພີ່ມ %2$s ແລະ ເອົາອອກ %3$s ເປັນທີ່ຢູ່ຂອງຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບອອກ %1$s ທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + + %1$s ລຶບອອກ %2$s ເປັນທີ່ຢູ່ຂອງຫ້ອງນີ້. + + + ທ່ານໄດ້ເພີ່ມ %1$s ເປັນທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + + %1$s ເພີ່ມ %2$s ເປັນທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + ທ່ານໄດ້ຖອນຄຳເຊີນຂອງ %1$s ແລ້ວ. ເຫດຜົນ: %2$s + %1$s ຖອນຄຳເຊີນຂອງ %2$s. ເຫດຜົນ: %3$s + ທ່ານຍອມຮັບຄຳເຊີນສຳລັບ %1$s. ເຫດຜົນ: %2$s + %1$s ຍອມຮັບຄຳເຊີນສຳລັບ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ຖືກຫ້າມ %1$s. ເຫດຜົນ: %2$s + %1$s ຖືກຫ້າມ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ຍົກເລີກການຫ້າມ %1$s. ເຫດຜົນ: %2$s + %1$s ຍົກເລີກການຫ້າມ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ລຶບ %1$s ອອກແລ້ວ. ເຫດຜົນ: %2$s + %1$s ລຶບອອກ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ປະຕິເສດການເຊີນ. ເຫດຜົນ: %1$s + %1$s ປະຕິເສດຄຳເຊີນ. ເຫດຜົນ: %2$s + ທ່ານອອກໄປ. ເຫດຜົນ: %1$s + ອອກ %1$s. ເຫດຜົນ: %2$s + ທ່ານໄດ້ອອກຈາກຫ້ອງ. ເຫດຜົນ: %1$s + %1$s ອອກຈາກຫ້ອງ. ເຫດຜົນ: %2$s + ທ່ານເຂົ້າຮ່ວມ. ເຫດຜົນ: %1$s + %1$s ເຂົ້າຮ່ວມ. ເຫດຜົນ: %2$s + ທ່ານໄດ້ເຂົ້າຮ່ວມຫ້ອງ. ເຫດຜົນ: %1$s + %1$s ເຂົ້າຮ່ວມຫ້ອງ. ເຫດຜົນ: %2$s + %1$s ເຊີນທ່ານ. ເຫດຜົນ: %2$s + ທ່ານໄດ້ຮັບເຊີນ %1$s. ເຫດຜົນ: %2$s + %1$s ເຊີນ %2$s. ເຫດຜົນ: %3$s + ການເຊື້ອເຊີນຂອງທ່ານ. ເຫດຜົນ: %1$s + ຄຳເຊີນຂອງ %1$s. ເຫດຜົນ: %2$s + ກຳລັງສົ່ງຂໍ້ຄວາມ… + ຂໍ້ຄວາມຖືກສົ່ງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຂໍ້ມູນບັນຊີ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nຊຸມຊົນນໍາເຂົ້າ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຫ້ອງຊ້າຍ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງນຳເຂົ້າຫ້ອງຮັບເຊີນ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງໂຫຼດບົດສົນທະນາຂອງທ່ານ +\nຖ້າທ່ານໄດ້ເຂົ້າຮ່ວມຫຼາຍຫ້ອງ, ນີ້ອາດຈະໃຊ້ເວລາໄລຍະຫນຶ່ງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຫ້ອງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າ crypto + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງນຳເຂົ້າບັນຊີ… + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງດາວໂຫຼດຂໍ້ມູນ… + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງລໍຖ້າການຕອບສະໜອງຂອງເຊີບເວີ… + ຫ້ອງຫວ່າງ (ແມ່ນ %s) + ຫ້ອງຫວ່າງ + + %1$s, %2$s, %3$s ແລະ %4$d ອື່ນໆ + + %1$s, %2$s, %3$s ແລະ %4$s + %1$s, %2$s ແລະ %3$s + %1$s ແລະ %2$s + ເຊີນຫ້ອງ + ເບີໂທລະສັບ + ທີ່ຢູ່ອີເມວ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເຂົ້າຮ່ວມຫ້ອງນີ້ + Matrix ຜິດພາດ + ບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້ + ອຸປະກອນຂອງຜູ້ສົ່ງບໍ່ໄດ້ສົ່ງກະແຈໃຫ້ພວກເຮົາສໍາລັບຂໍ້ຄວາມນີ້. + ** ບໍ່ສາມາດຖອດລະຫັດໄດ້: %s ** + %1$s ຈາກ %2$s ຫາ %3$s + %1$s ໄດ້ປ່ຽນລະດັບພະລັງງານຂອງ %2$s. + ທ່ານໄດ້ປ່ຽນລະດັບພະລັງງານຂອງ %1$s. + ກຳນົດເອງ + ກຳນົດເອງ (%1$d) + ຜູ້ເບີ່ງເເຍງລະບົບ + ຄ່າເລີ່ມຕົ້ນ + ຜູ້ຄວບຄຸມ + ທ່ານໄດ້ແກ້ໄຂວິດເຈັດ %1$s + %1$s ແກ້ໄຂວິດເຈັດ %2$s + ທ່ານລຶບວິດເຈັດ %1$s ອອກແລ້ວ + %1$s ລຶບວິດເຈັດ %2$s ອອກແລ້ວ + ທ່ານໄດ້ເພີ່ມວິດເຈັດ %1$s ແລ້ວ + %1$s ເພີ່ມວິດເຈັດ %2$s ແລ້ວ + ທ່ານຍອມຮັບຄຳເຊີນສຳລັບ %1$s + %1$s ຍອມຮັບຄຳເຊີນສຳລັບ %2$s + ທ່ານໄດ້ຖອນຄຳເຊີນສຳລັບ %1$s ແລ້ວ + %1$s ຖອນຄຳເຊີນສຳລັບ %2$s + ທ່ານໄດ້ຖອນຄຳເຊີນສຳລັບ %1$s ເຂົ້າຮ່ວມຫ້ອງແລ້ວ + %1$s ຖອນຄຳເຊີນສຳລັບ %2$s ເຂົ້າຮ່ວມຫ້ອງ + ທ່ານໄດ້ຮັບເຊີນ %1$s + %1$s ເຊີນ %2$s + ທ່ານໄດ້ສົ່ງຄຳເຊີນໄປຫາ %1$s ເພື່ອເຂົ້າຮ່ວມຫ້ອງ + %1$s ສົ່ງຄຳເຊີນໃຫ້ %2$s ເຂົ້າຮ່ວມຫ້ອງ + ທ່ານໄດ້ລຶບຮູບແທນຕົວຂອງຫ້ອງອອກແລ້ວ + %1$s ລຶບຮູບແທນຕົວຂອງຫ້ອງອອກແລ້ວ + ທ່ານໄດ້ລຶບຫົວຂໍ້ຫ້ອງອອກ + %1$s ລຶບຫົວຂໍ້ຫ້ອງອອກ + ທ່ານໄດ້ລຶບຊື່ຫ້ອງອອກ + %1$s ລຶບຊື່ຫ້ອງອອກ + (ຮູບຕົວແທນຖືກປ່ຽນແປງເຊັ່ນດຽວກັນ) + 🎉 ເຊີບເວີທັງໝົດຖືກຫ້າມບໍ່ໃຫ້ເຂົ້າຮ່ວມ! ຫ້ອງນີ້ບໍ່ສາມາດໃຊ້ໄດ້ອີກຕໍ່ໄປ. + ບໍ່ມີການປ່ຽນແປງ. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ຖືກຫ້າມແລ້ວ. + ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ໄດ້ຖືກອະນຸຍາດແລ້ວ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກລຶບອອກຈາກລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ. + • ຕອນນີ້ເຊີບເວີທີ່ກົງກັບ %s ໄດ້ຖືກອະນຸຍາດແລ້ວ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກເອົາອອກຈາກລາຍການຫ້າມແລ້ວ. + • ຕອນນີ້ເຊີບເວີທີ່ກົງກັບ %s ຖືກຫ້າມແລ້ວ. + + %d ປ່ຽນແປງເຊີບເວີ ACL + + ທ່ານໄດ້ປ່ຽນເຊີບເວີ ACLs ສໍາລັບຫ້ອງນີ້. + %s ໄດ້ປ່ຽນເຊີບເວີ ACLs ສໍາລັບຫ້ອງນີ້. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ຖືກຫ້າມ. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ແມ່ນອະນຸຍາດ. + • ເຊີບເວີທີ່ກົງກັບ %s ໄດ້ຮັບອະນຸຍາດ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກຫ້າມ. + ທ່ານຕັ້ງ ACLs ເຊີບເວີສໍາລັບຫ້ອງນີ້. + %s ຕັ້ງ ACL ຂອງເຊີບເວີສຳລັບຫ້ອງນີ້. + ທ່ານໄດ້ຍົກລະດັບຢູ່ທີ່ນີ້. + %s ຍົກລະດັບຢູ່ບ່ອນນີ້ແລ້ວ. + ທ່ານໄດ້ຍົກລະດັບຫ້ອງນີ້. + %s ໄດ້ຍົກລະດັບຫ້ອງນີ້ແລ້ວ. + ຜູ້ໃດຜູ້ຫນຶ່ງ. + ສະມາຊິກຫ້ອງທັງຫມົດ. + ສະມາຊິກຫ້ອງທັງຫມົດ, ຈາກຈຸດທີ່ເຂົາເຈົ້າເຂົ້າຮ່ວມ. + ສະມາຊິກຫ້ອງທັງຫມົດ, ຈາກຈຸດທີ່ເຂົາເຈົ້າໄດ້ຖືກເຊີນ. + ທ່ານໄດ້ສ້າງຂໍ້ຄວາມໃນອະນາຄົດເຫັນໄດ້ຕໍ່ %1$s + %1$s ໄດ້ສ້າງຂໍ້ຄວາມອະນາຄົດເຫັນໄດ້ໂດຍ %2$s + ທ່ານໄດ້ສ້າງປະຫວັດຫ້ອງໃນອະນາຄົດໃຫ້ ເຫັນໄດ້%1$s + %1$s ສ້າງປະຫວັດຫ້ອງໃນອະນາຄົດໃຫ້ ເຫັນໄດ້%2$s + ທ່ານສິ້ນສຸດການໂທ. + %s ວາງສາຍ. + ທ່ານໄດ້ຮັບສາຍ. + %s ຮັບສາຍ. + ທ່ານໄດ້ສົ່ງຂໍ້ມູນເພື່ອຕັ້ງຄ່າການໂທ. + %s ສົ່ງຂໍ້ມູນເພື່ອຕັ້ງຄ່າການໂທ. + ທ່ານໄດ້ໂທດ້ວຍສຽງ. + %s ໂທອອກ. + ທ່ານໄດ້ໂທວິດີໂອ. + %s ໂທດ້ວຍວິດີໂອ. + ທ່ານໄດ້ປ່ຽນຊື່ຫ້ອງເປັນ: %1$s + %1$s ໄດ້ປ່ຽນຊື່ຫ້ອງເປັນ: %2$s + ທ່ານໄດ້ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + %1$s ໄດ້ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + ທ່ານໄດ້ປ່ຽນຫົວຂໍ້ເປັນ: %1$s + %1$s ໄດ້ປ່ຽນຫົວຂໍ້ເປັນ: %2$s + ທ່ານໄດ້ລຶບການສະເເດງຊື່ຂອງເຈົ້າອອກ (ມັນແມ່ນ %1$s) + %1$s ລຶບການສະເເດງຊື່ຂອງເຂົາເຈົ້າອອກ (ມັນແມ່ນ %2$s) + ທ່ານໄດ້ປ່ຽນການສະເເດງຊື່ຂອງທ່ານຈາກ %1$s ເປັນ %2$s + %1$s ໄດ້ປ່ຽນການສະແດງຊື່ຂອງເຂົາເຈົ້າຈາກ %2$s ເປັນ %3$s + ທ່ານກຳນົດການສະເເດງຊື່ຊື່ຂອງທ່ານເປັນ %1$s + %1$s ກຳນົດນົດການສະແດງຊື່ຂອງເຂົາເຈົ້າເປັນ %2$s + ທ່ານໄດ້ປ່ຽນຮູບແທນຕົວຂອງເຈົ້າແລ້ວ + %1$s ໄດ້ປ່ຽນຮູບແທນຕົວຂອງເຂົາເຈົ້າແລ້ວ + ທ່ານໄດ້ຖອນຄຳເຊີນຂອງ %1$s ແລ້ວ + %1$s ຖອນຄຳເຊີນຂອງ %2$s + ທ່ານໄດ້ຫ້າມ%1$s + %1$s ໄດ້ຫ້າມ %2$s + ທ່ານໄດ້ຍົກເລີກການຫ້າມ %1$s + %1$s ໄດ້ຍົກເລີກການຫ້າມ %2$s + ທ່ານໄດ້ລຶບ %1$s ອອກແລ້ວ + %1$sໄດ້ ລຶບອອກ %2$s + ທ່ານໄດ້ປະຕິເສດການເຊີນ + %1$s ໄດ້ປະຕິເສດຄຳເຊີນ + ທ່ານໄດ້ອອກຈາກຫ້ອງ + %1$s ໄດ້ອອກຈາກຫ້ອງ + ທ່ານໄດ້ອອກຈາກຫ້ອງ + %1$s ອອກຈາກຫ້ອງ + ທ່ານໄດ້ເຂົ້າຮ່ວມ + %1$s ໄດ້ເຂົ້າຮ່ວມ + ທ່ານໄດ້ເຂົ້າຮ່ວມຫ້ອງ + %1$s ໄດ້ເຂົ້າຮ່ວມຫ້ອງ + %1$s ໄດ້ເຊີນທ່ານ + ທ່ານໄດ້ເຊີນ %1$s + %1$s ໄດ້ເຊີນ %2$s + ທ່ານໄດ້ຕັ້ງກະທູ້ສົນທະນາ + %1$s ໄດ້ຕັ້ງກະທູ້ສົນທະນາ + ທ່ານໄດ້ສ້າງຫ້ອງ + %1$s ໄດ້ສ້າງຫ້ອງ + ບັດເຊີນຂອງທ່ານ + ການເຊີນຂອງ %s + ບໍ່ພົບການແກ້ໄຂ + ການແກ້ໄຂຂໍ້ຄວາມ + (ດັດແກ້ແລ້ວ) + ໄຟລ໌ %1$s ຖືກດາວໂຫຼດແລ້ວ! + ກຳລັງບີບອັດວິດີໂອ %d%% + ກະທູ້ກຳລັງດຳເນີນຢູ່ພ້ອມກັບຄຸນສົມບັດໃໝ່ທີ່ກຳລັງຈະມາຮອດ ເຊັ່ນ: ການແຈ້ງເຕືອນປັບປຸງໃຫ້ດີຂຶ້ນ. ພວກເຮົາຢາກໄດ້ຍິນຄໍາຕິຊົມຂອງທ່ານ! + app_display_name: + ກຳລັງບີບອັດຮູບ… + ກຳລັງສົ່ງໄຟລ໌ (%1$s / %2$s) + ກຳລັງເຂົ້າລະຫັດໄຟລ໌… + ກຳລັງສົ່ງຮູບຕົວຢ່າງ (%1$s / %2$s) + ກຳລັງເຂົ້າລະຫັດຮູບຕົວຢ່າງ… + ກຳລັງລໍຖ້າ… + ຂໍ້ຄວາມໂດຍກົງ + ສະແດງປະຫວັດຄົບຖ້ວນຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ + ສະແດງເຫດການທີ່ເຊື່ອງໄວ້ໃນທາມລາຍ + ເບຕ້າ + ຄວາມຄິດເຫັນຂອງກະທູ້ Beta + ໃຫ້ຄໍາຄິດເຫັນ + ໃຫ້ຄໍາຄິດເຫັນ + ສົ່ງຄຳຕິຊົມບໍ່ສຳເລັດ (%s) + ຂໍຂອບໃຈ, ຄໍາຄຶດຄໍາເຫັນຂອງທ່ານໄດ້ຖືກສົ່ງສໍາເລັດແລ້ວ + ທ່ານສາມາດຕິດຕໍ່ຂ້ອຍໄດ້ ຖ້າທ່ານມີຄໍາຖາມຕິດຕາມ + ທ່ານກຳລັງໃຊ້ພື້ນທີ່ເວີຊັ້ນເບຕ້າ. ຄໍາຕິຊົມຂອງທ່ານຈະຊ່ວຍແຈ້ງໃຫ້ເວີຊັ້ນຕໍ່ໄປຊາບ. ເເພລັດຟອມແລະຊື່ຜູ້ໃຊ້ຂອງທ່ານຈະຖືກບັນທຶກໄວ້ເພື່ອຊ່ວຍໃຫ້ພວກເຮົາໃຊ້ຄໍາຕິຊົມຂອງທ່ານເທົ່າທີ່ພວກເຮົາສາມາດເຮັດໄດ້. + ຄໍາຕິຊົມ + ຄຳຕິຊົມຂອງພື້ນທີ່ + ເບຕ້າ + ສົ່ງຄຳແນະນຳບໍ່ສຳເລັດ (%s) + ຂອບໃຈ, ຄຳແນະນຳໄດ້ສົ່ງສຳເລັດແລ້ວ + ອະທິບາຍຄໍາແນະນໍາຂອງທ່ານທີ່ນີ້ + ກະລຸນາຂຽນຄໍາແນະນໍາຂອງທ່ານຂ້າງລຸ່ມນີ້. + ໃຫ້ຄໍາແນະນໍາ + ລົງທະບຽນ token + ການຕັ້ງຄ່າລະບົບ + ເວີຊັ້ນ + ຂໍຄວາມຊ່ວຍເຫຼືອໃນການນຳໃຊ້ ${app_name} + ຊ່ວຍເຫຼືອ ແລະ ສະຫນັບສະຫນູນ + ຊ່ວຍເຫຼືອ + ກົດໝາຍ + ຊ່ວຍເຫຼືອ & ກ່ຽວກັບ + ສຽງ & ວິດີໂອ + ຮູບແບບ: + Url: + session_name: + push_key: + app_id: + ບໍ່ມີປະຕູທາງpushທີ່ລົງທະບຽນ + ບໍ່ໄດ້ກໍານົດກົດລະບຽບການຊຸກຍູ້ + ກົດລະບຽບການຊຸກຍູ້ + ຄວາມປອດໄພ & ຄວາມເປັນສ່ວນຕົວ + ຄວາມມັກ + ທົ່ວໄປ + ທ່ານກຳລັງເບິ່ງກະທູ້ນີ້ຢູ່! + ທ່ານກຳລັງເບິ່ງຫ້ອງນີ້ຢູ່ແລ້ວ! + ແຈ້ງການອື່ນໆຂອງພາກສ່ວນທີສາມ + ສະບັບ Matrix SDK + ນຳເຂົ້າກະແຈ e2e ຈາກໄຟລ໌ \"%1$s\". + ເກີດຄວາມຜິດພາດໃນການຮັບເອົາຂໍ້ມູນສຳຮອງກະແຈ + ເກີດຄວາມຜິດພາດໃນການຮັບຂໍ້ມູນໜ້າເຊື່ອຖື + ຫ້ອງໄດ້ຖືກສ້າງຂື້ນ, ແຕ່ການເຊີນບາງອັນບໍ່ໄດ້ຖືກສົ່ງໄປດ້ວຍເຫດຜົນຕໍ່ໄປນີ້:%s + ທຸກຄົນຈະສາມາດເຂົ້າຮ່ວມຫ້ອງນີ້ໄດ້ + ສາທາລະນະ + ການຕັ້ງຄ່າຫ້ອງ + ຫົວຂໍ້ + ຫົວຂໍ້ຫ້ອງ (ທາງເລືອກ) + ຊື່ + ຊື່ຫ້ອງ + ສ້າງ + ຂໍ້ຄວາມໂດຍກົງ + ຫ້ອງ + ຫ້ອງນີ້ບໍ່ສາມາດເບິ່ງຕົວຢ່າງໄດ້. ທ່ານຕ້ອງການເຂົ້າຮ່ວມບໍ\? + ຫ້ອງນີ້ບໍ່ສາມາດເຂົ້າເຖິງໄດ້ໃນເວລານີ້. +\nລອງໃໝ່ໃນພາຍຫຼັງ, ຫຼືຖາມຜູ້ເບິ່ງແຍງຫ້ອງເພື່ອກວດເບິ່ງວ່າທ່ານມີການເຂົ້າເຖິງຫຼືບໍ່. + ຫ້ອງນີ້ບໍ່ສາມາດເບິ່ງຕົວຢ່າງໄດ້ + ຊຸມຊົນທັງໝົດ + ກະລຸນາລໍຖ້າ… + ປ່ຽນເຄືອຂ່າຍ + ບໍ່ມີເຄືອຂ່າຍ. ກະລຸນາກວດເບິ່ງການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງທ່ານ. + ສ້າງພື້ນທີ່ໃຫມ່ + ສ້າງຫ້ອງໃຫມ່ + ເຫດການບໍ່ຖືກຕ້ອງ, ບໍ່ສາມາດສະແດງໄດ້ + ຄວບຄຸມເຫດການໂດຍຜູ້ເບິ່ງແຍງຫ້ອງ + ເຫດການຖືກລຶບໂດຍຜູ້ໃຊ້ + ສະແດງຕົວຍຶດສໍາລັບຂໍ້ຄວາມທີ່ຖືກລົບອອກ + ສະແດງຂໍ້ຄວາມທີ່ຖືກລືບອອກ + ຂໍ້ຄວາມຖືກລຶບ + ການໂຕ້ຕອບ + ເບິ່ງການໂຕ້ຕອບ + ເພີ່ມປະຕິກິລິຍາ + ປະຕິກິລິຍາ + ຫ້ອງຂອງທ່ານຈະຖືກສະແດງຢູ່ທີ່ນີ້. ແຕະ + ຂວາລຸ່ມເພື່ອຊອກຫາອັນທີ່ມີຢູ່ແລ້ວ ຫຼື ເລີ່ມຕົ້ນໂດຍທ່ານເອງ. + ຫ້ອງ + ການສົນທະນາທາງຂໍ້ຄວາມໂດຍກົງຂອງທ່ານຈະຖືກສະແດງຢູ່ທີ່ນີ້. ແຕະ + ຂວາລຸ່ມເພື່ອເລີ່ມຕົ້ນ. + ການສົນທະນາ + ປຸ່ມEnter ຂອງແປ້ນພິມຈະສົ່ງຂໍ້ຄວາມແທນການເພີ່ມຕົວແບ່ງແຖວ + ສົ່ງຂໍ້ຄວາມດ້ວຍການກົດ enter + ເບິ່ງຕົວຢ່າງສື່ກ່ອນສົ່ງ + ສັ່ນເມື່ອກ່າວເຖິງຜູ້ໃຊ້ + ລວມທັງຮູບແທນຕົວ ແລະ ການປ່ຽນແປງການສະແດງຊື່. + ສະແດງບັນຊີ + ການເຊີນ, ລຶບ, ແລະການຫ້າມແມ່ນບໍ່ມີຜົນກະທົບ. + ສະແດງການເຂົ້າຮ່ວມ ແລະ ອອກຈາກເຫດການ + ໃຊ້ຄໍາສັ່ງ /confetti ຫຼືສົ່ງຂໍ້ຄວາມທີ່ມີ ❄️ ຫຼື 🎉 + ສະແດງຜົນການສົນທະນາ + ໃບຮັບຮອງໄດ້ປ່ຽນຈາກອັນທີ່ເຊື່ອຖືໄດ້. ນີ້ແມ່ນຜິດປົກກະຕິ. ຂໍແນະນຳໃຫ້ທ່ານຢ່າຍອມຮັບໃບຮັບຮອງໃໝ່ນີ້. + ທ່ານບໍ່ມີຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ເຊີນໂດຍ %s + ສົ່ງຄຳເຊີນໃຫ້ທ່ານ + ລອງໃໝ່ + ເບິ່ງຢູ່ໃນຫ້ອງ + ຕອບໃນກະທູ້ + ຕອບ + ແກ້ໄຂ + ເບິ່ງຄືວ່າທ່ານກຳລັງພະຍາຍາມເຊື່ອມຕໍ່ຫາ homeserver ອື່ນ. ທ່ານຕ້ອງການອອກຈາກລະບົບບໍ\? + ທ່ານຍັງບໍ່ໄດ້ໃຊ້ເຊີບເວີໃດໆ + ຄວາມຜິດພາດທີ່ບໍ່ຮູ້ຈັກ + %s ຕ້ອງການກວດສອບລະບົບຂອງທ່ານ + ການຮ້ອງຂໍການຢັ້ງຢືນ + ໄດ້ແລ້ວ + ຢືນຢັນແລ້ວ! + ລາຍເຊັນ + ສູດການຄິດໄລ່ + ເວີຊັ້ນ + + ກຳລັງສຳຮອງຂໍ້ມູນ %d ກະແຈ… + + ກະແຈທັງໝົດຖືກສຳຮອງໄວ້ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ + ກຳລັງສຳຮອງຂໍ້ມູນກະແຈຂອງທ່ານ. ອັນນີ້ອາດຈະໃຊ້ເວລາຫຼາຍນາທີ… + ຈັດການການ ສຳຮອງຂໍ້ມູນກະເເຈ + ລະຫັດຂໍ້ຄວາມໃໝ່ທີ່ປອດໄພ + ການສຳຮອງຂໍ້ມູນກະເເຈ + ຂໍ້ຄວາມເຂົ້າລະຫັດຈະບໍ່ສູນເສຍ + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ ແລະຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດ + ການສໍາຮອງທີ່ປອດໄພ + ລຶບການເຂົ້າລະຫັດສຳຮອງຂອງທ່ານອອກຈາກເຊີບເວີບໍ\? ທ່ານຈະບໍ່ສາມາດໃຊ້ລະຫັດກູ້ຄືນຂອງທ່ານເພື່ອອ່ານປະຫວັດຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດໄດ້ອີກຕໍ່ໄປ. + ລຶບການສຳຮອງຂໍ້ມູນ + ກຳລັງກວດສອບສະຖານະການສຳຮອງ + ກຳລັງລຶບການສຳຮອງຂໍ້ມູນ… + ການສຳຮອງຂໍ້ມູນກະເເຈ ໃນລະບົບນີ້, ກູ້ຂໍ້ມູນດ້ວຍປະໂຫຍກຄວາມປອດໄພ ຫຼື ກູ້ຂໍ້ມູນກະແຈຂອງທ່ານດຽວນີ້. + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນບໍ່ໄດ້ຮັບການຢັ້ງຢືນທີ່ຖືກຕ້ອງ %s + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນບໍ່ຖືກຕ້ອງໄດ້ຢືນຢັນຈາກລະບົບແລ້ວ %s + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບທີ່ບໍ່ໄດ້ຜ່ານການຢືນຢັນ %s + ການສຳຮອງຂໍ້ມູນ ມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບທີ່ຢືນຢັນແລ້ວ %s. + ການສຳຮອງຂໍ້ມູນ ມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບນີ້. + ກູ້ຂໍ້ມູນມີລາຍເຊັນຈາກລະບົບທີ່ບໍ່ຮູ້ຈັກກັບ ID %s. + ກະແຈຂອງທ່ານບໍ່ໄດ້ສຳຮອງຂໍ້ມູນຈາກລະບົບນີ້. + ການສຳຮອງຂໍ້ມູນກະເເຈບໍ່ໄດ້ໃຊ້ງານຢູ່ໃນລະບົບນີ້. + ການສຳຮອງຂໍ້ມູນກະເເຈ ໄດ້ຖືກຕັ້ງຄ່າຢ່າງຖືກຕ້ອງສໍາລັບລະບົບນີ້. + ລຶບການສຳຮອງຂໍ້ມູນ + ກູ້ຄືນຈາກການສໍາຮອງຂໍ້ມູນ + ການໂຫຼດລະຫັດການກູ້ຄືນຫຼ້າສຸດບໍ່ສຳເລັດ(%s). + + %d ກະແຈໃໝ່ໄດ້ຖືກເພີ່ມໃສ່ໃນລະບົບນີ້. + + + ກູ້ຄືນການສຳຮອງຂໍ້ມູນດ້ວຍກະແຈ %d ແລ້ວ. + + ກູ້ຂໍ້ມູນຄືນມາ %s ! + ການສຳຮອງຂໍ້ມູນບໍ່ສາມາດຖອດລະຫັດດ້ວຍກະແຈການກູ້ຂໍ້ມູນນີ້ໄດ້: ກະລຸນາກວດສອບວ່າທ່ານໃສ່ລະຫັດການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ. + ກະລຸນາໃສ່ລະຫັດການກູ້ຂໍ້ມູນ + ປົດລັອກປະຫວັດ + ກຳລັງນຳເຂົ້າກະແຈ… + ກຳລັງດາວໂຫຼດກະແຈ… + ກຳລັງກູ້ຄືນລະຫັດຄອມພິວເຕີ… + ການກູ້ຄືນການສໍາຮອງຂໍ້ມູນ: + ການສຳຮອງຂໍ້ມູນບໍ່ສາມາດຖອດລະຫັດດ້ວຍປະໂຫຍກຄວາມປອດໄພນີ້: ກະລຸນາກວດສອບວ່າທ່ານໃສ່ລະຫັດຜ່ານການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ. + ກະແຈການກູ້ຄືນຂອງທ່ານສູນເສຍ\? ທ່ານສາມາດຕັ້ງຄ່າອັນໃໝ່ໄດ້ໃນການຕັ້ງຄ່າ. + ກະລຸນາໃສ່ລະຫັດການກູ້ຄຶນ + ໃຊ້ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານເພື່ອປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ບໍ່ຮູ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນຂອງທ່ານ, ທ່ານສາມາດ %s ໄດ້. + ໃຊ້ລະຫັດການກູ້ຄືນຂອງທ່ານ + ໃຊ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນຂອງທ່ານເພື່ອປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ກຳລັງດຶງເອົາເວີຊັນສຳຮອງ… + ທ່ານອາດຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມຂອງທ່ານຖ້າທ່ານອອກຈາກລະບົບຫຼືສູນເສຍອຸປະກອນນີ້. + ທ່ານແນ່ໃຈບໍ່\? + ຄວາມຜິດພາດທີ່ບໍ່ຄາດຄິດ + ລະຫັດການກູ້ຄືນ + ພວກເຮົາ <b>ບໍ່</b> ແບ່ງປັນຂໍ້ມູນໃຫ້ພາກສ່ວນທີສາມ + ພວກເຮົາ <b>ບໍ່</b> ບັນທຶກ ຫຼື ບັນທຶກຂໍ້ມູນໜ້າປົກບັນຊີໃດໆ + ທີ່ນີ້ + ຊ່ວຍພວກເຮົາລະບຸບັນຫາ ແລະປັບປຸງ ${app_name} ໂດຍການແບ່ງປັນຂໍ້ມູນການນຳໃຊ້ທີ່ບໍ່ເປີດເຜີຍຊື່. ເພື່ອເຂົ້າໃຈວິທີໃຊ້ຫຼາຍອຸປະກອນ, ພວກເຮົາຈະສ້າງຕົວກຳນົດແບບສຸ່ມ, ແບ່ງປັນໂດຍອຸປະກອນຂອງທ່ານ. +\n +\nທ່ານສາມາດອ່ານເງື່ອນໄຂທັງໝົດຂອງພວກເຮົາໄດ້ %s. + ຊ່ວຍປັບປຸງ ${app_name} + ${app_name} ລວບລວມການວິເຄາະທີ່ບໍ່ເປີດເຜີຍຊື່ເພື່ອໃຫ້ພວກເຮົາປັບປຸງແອັບພລິເຄຊັນ. + ສົ່ງຂໍ້ມູນການວິເຄາະ + ການວິເຄາະ + ຈັດການການຕັ້ງຄ່າການຄົ້ນພົບຂອງທ່ານ. + ການຄົ້ນພົບ + ປິດການນຳໃຊ້ບັນຊີຂອງຂ້ອຍ + ປິດການນຳໃຊ້ບັນຊີ + ນີ້ຈະປ່ຽນແທນ ຫຼືປະໂຫຍກປັດຈຸບັນຂອງທ່ານ. + ສ້າງກະແຈຄວາມປອດໄພໃໝ່ ຫຼືຕັ້ງປະໂຫຍກຄວາມປອດໄພໃໝ່ສຳລັບການສຳຮອງຂໍ້ມູນທີ່ມີຢູ່ແລ້ວຂອງທ່ານ. + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດໂດຍການສໍາຮອງການເຂົ້າລະຫັດຢູ່ໃນເຊີບເວິຂອງທ່ານ. + ຕັ້ງຄ່າໃນອຸປະກອນນີ້ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພຄືນໃໝ່ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ + ການສໍາຮອງທີ່ປອດໄພ + ເພີ່ມປຸ່ມໃສ່ຕົວຂຽນຂໍ້ຄວາມເພື່ອເປີດແປ້ນພິມ emoji + ສະແດງແປ້ນພິມ emoji + ການສ້າງລະຫັດການກຸ້ຂໍ້ມູນໂດຍໃຊ້ປະໂຫຍກລະຫັດຜ່ານ, ຂະບວນການນີ້ສາມາດໃຊ້ເວລາຫຼາຍວິນາທີ. + ແບ່ງປັນລະຫັດການກູ້ຂໍ້ມູນກັບ… + ກະລຸນາສ້າງສໍາເນົາ + ຢຸດ + ແທນທີ່ + ເບິ່ງຄືວ່າທ່ານມີການຕັ້ງຄ່າການສຳຮອງລະຫັດຈາກລະບົບອື່ນຢູ່ກ່ອນແລ້ວ. ທ່ານຕ້ອງການແທນທີ່ມັນກັບອັນທີ່ທ່ານກຳລັງສ້າງບໍ\? + ມີຂໍ້ມູນສຳຮອງຢູ່ໃນເຊີບເວີຂອງທ່ານຢູ່ກ່ອນແລ້ວ + ລະຫັດການກູ້ຂໍ້ມູນໄດ້ຖືກບັນທຶກໄວ້. + ບັນທຶກເປັນໄຟລ໌ + ແບ່ງປັນ + ບັນທຶກລະຫັດການກູ້ຂໍ້ມູນ + ຂ້ອຍໄດ້ສ້າງສຳເນົາ + ສຳເລັດແລ້ວ + ຮັກສາກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພຫຼາຍ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ (ຫຼືບ່ອນປອດໄພ) + ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານເປັນຕາໜ່າງຄວາມປອດໄພ - ທ່ານສາມາດໃຊ້ມັນເພື່ອກູ້ການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານຄືນມາໄດ້ ຖ້າທ່ານລືມປະໂຫຍກລະຫັດຜ່ານຂອງທ່ານ. +\nຮັກສາກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພຫຼາຍ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ (ຫຼືບ່ອນປອດໄພ) + ກະແຈຂອງທ່ານກຳລັງຖືກສຳຮອງໄວ້. + ຄວາມສໍາເລັດ ! + (ຂັ້ນສູງ) ຕັ້ງຄ່າດ້ວຍລະຫັດການກູ້ຂໍ້ມູນ + ຫຼື, ຮັບປະກັນການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍກະແຈການກູ້ຂໍ້ມູນ, ບັນທຶກມັນໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ. + ການສ້າງສໍາຮອງຂໍ້ມູນ + ກຳນົດລະຫັດຜ່ານ + ພວກເຮົາຈະເກັບສຳເນົາລະຫັດທີ່ເຂົ້າລະຫັດໄວ້ຢູ່ໃນເຊີບເວີຂອງທ່ານ. ປົກປ້ອງການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍປະໂຫຍກລະຫັດຜ່ານເພື່ອຮັກສາມັນໃຫ້ປອດໄພ. +\n +\nເພື່ອຄວາມປອດໄພສູງສຸດ, ອັນນີ້ຄວນຈະແຕກຕ່າງຈາກລະຫັດຜ່ານບັນຊີຂອງທ່ານ. + ຮັບປະກັນການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍລະຫັດຜ່ານ. + ສົ່ງອອກກະແຈດ້ວຍຕົນເອງ + ຢືນຢັນໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ໃນລະບົບອື່ນຂອງທ່ານ: + ຢືນຢັນ + ip ທີ່ບໍ່ຮູ້ຈັກ + ຢັ້ງຢືນແລ້ວ + ບໍ່ມີການຍືນຍັນ + + %1$d/%2$d ນຳເຂົ້າກະແຈສຳເລັດແລ້ວ. + + ຢ່າສົ່ງຂໍ້ຄວາມເຂົ້າລະຫັດໄປຫາລະບົບທີ່ບໍ່ໄດ້ຢືນຢັນຈາກລະບົບນີ້. + ເຂົ້າລະຫັດໄປຫາລະບົບທີ່ຢືນຢັນເທົ່ານັ້ນ + ນໍາເຂົ້າ + ນໍາເຂົ້າລະຫັດຈາກໄຟລ໌ໃນບ່ອນຈັດເກັບ + ນຳເຂົ້າກະແຈຫ້ອງ + ນຳເຂົ້າກະແຈຫ້ອງ E2E + ຈັດການການສໍາຮອງກະແຈ + ການກູ້ຄືນຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ສົ່ງອອກກະແຈສຳເລັດແລ້ວ + ກະລຸນາສ້າງລະຫັດຜ່ານເພື່ອເຂົ້າລະຫັດທີ່ສົ່ງອອກ. ທ່ານຈະຕ້ອງໃສ່ລະຫັດຜ່ານດຽວກັນເພື່ອໃຫ້ສາມາດນໍາເຂົ້າລະຫັດໄດ້. + ສົ່ງອອກ + ສົ່ງອອກກະແຈໄປຍັງບ່ອນເກັບໄຟລ໌ + ສົ່ງອອກກະແຈຫ້ອງ + ສົ່ງອອກກະແຈຫ້ອງ E2E + ລະຫັດລະບົບ + ID ລະບົບ + ຊື່ສາທາລະນະ + ການຖອດລະຫັດຜິດພາດ + ຫົວຂໍ້ + ບໍ່ໄດ້ກຳນົດທີ່ຢູ່ຫຼັກ + ກຳນົດທີ່ຢູ່ຫຼັກ + ລັກສະນະການທົດລອງເຫຼົ່ານີ້ແມ່ນອາດໃຊ້ງານບໍ່ໄດ້. ໃຊ້ດ້ວຍຄວາມລະມັດລະວັງ. + ຫ້ອງທົດລອງ + ເວີຊັ້ນຫ້ອງ + ID ພາຍໃນຫ້ອງນີ້ + ຂັ້ນສູງ + + %d ຜູ້ໃຊ້ທີ່ຖືກຫ້າມ + + ຫ້າມຜູ້ໃຊ້ + ພື້ນທີ່ ຫຼືຫ້ອງອື່ນໆທີ່ທ່ານອາດບໍ່ຮູ້ + ພື້ນທີ່ທີ່ທູກຄົນຮູ້ວ່າມີຫ້ອງນີ້ + ຕັດສິນໃຈວ່າໃຜສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມຫ້ອງນີ້ໄດ້. + ແຕະເພື່ອແກ້ໄຂພຶ້ນທີ່ + ເລືອກພຶ້ນທີ່ + ຕັດສິນໃຈວ່າພື້ນທີ່ໃດສາມາດເຂົ້າເຖິງຫ້ອງນີ້ໄດ້. ຖ້າພື້ນທີ່ຖືກເລືອກ ສະມາຊິກຈະສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມຊື່ຫ້ອງໄດ້. + ພື້ນທີ່ທີ່ສາມາດເຂົ້າເຖິງໄດ້ + ອະນຸຍາດໃຫ້ສະມາຊິກໃນພຶ້ນທີຊອກຫາ ແລະເຂົ້າເຖິງ. + ສະມາຊິກຂອງພຶ້ນທີ່ %s ສາມາດຊອກຫາ, ເບິ່ງຕົວຢ່າງ ແລະເຂົ້າຮ່ວມໄດ້. + ທຸກຄົນຢູ່ໃນພື້ນທີ່ທີ່ມີຫ້ອງນີ້ສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມໄດ້. ມີແຕ່ຜູ້ເບິ່ງແຍງຫ້ອງນີ້ທີ່ສາມາດເພີ່ມໃສ່ພື້ນທີ່ໄດ້. + ສະມາຊິກຂອງພື້ນທີ່ເທົ່ານັ້ນ + ທຸກຄົນສາມາດຊອກຫາພື້ນທີ່ ແລະ ເຂົ້າຮ່ວມໄດ້ + ທຸກຄົນສາມາດຊອກຫາຫ້ອງ ແລະ ເຂົ້າຮ່ວມໄດ້ + ສາທາລະນະ + ສະເພາະຄົນທີ່ຖືກເຊີນສາມາດຊອກຫາ ແລະ ເຂົ້າຮ່ວມໄດ້ + ສ່ວນຕົວ (ເຊີນເທົ່ານັ້ນ) + ສ່ວນຕົວ + ການຕັ້ງຄ່າການເຂົ້າເຖິງທີ່ບໍ່ຮູ້ຈັກ (%s) + ທຸກຄົນສາມາດເຄາະຫ້ອງໃດກໍ່ໄດ້, ສະມາຊິກສາມາດຍອມຮັບ ຫຼື ປະຕິເສດກໍ່ໄດ້ + ສະມາຊິກເທົ່ານັ້ນ (ນັບຕັ້ງແຕ່ພວກເຂົາເຂົ້າຮ່ວມ) + ສະມາຊິກເທົ່ານັ້ນ (ນັບຕັ້ງແຕ່ພວກເຂົາຖືກເຊີນ) + ສະມາຊິກເທົ່ານັ້ນ (ຕັ້ງແຕ່ຈຸດທີ່ເລືອກຂອງທາງເລືອກນີ້) + ຜູ້ໃດຜູ້ຫນຶ່ງ + ບໍ່ສາມາດດຶງຂໍ້ມູນການເບິ່ງເຫັນລາຍການຫ້ອງປະຈຸບັນໄດ້ (%1$s). + ເຜີຍແຜ່ຫ້ອງນີ້ຕໍ່ສາທາລະນະຢູ່ໃນລາຍການຫ້ອງຂອງ %1$s ບໍ\? + ຍົກເລີກການເຜີຍແຜ່ທີ່ຢູ່ນີ້ + ເຜີຍແຜ່ທີ່ຢູ່ນີ້ + ເພີ່ມທີ່ຢູ່ຂອງບ່ອນຈັດເກັບ + ຫ້ອງນີ້ບໍ່ມີທີ່ຢູ່ບ່ອນຈັດເກັບ + ກໍານົດທີ່ຢູ່ສໍາລັບຫ້ອງນີ້ເພື່ອໃຫ້ຜູ້ໃຊ້ສາມາດຊອກຫາຫ້ອງນີ້ຜ່ານ homeserver ຂອງທ່ານ (%1$s) + ທີ່ຢູ່ບ່ອນຈັດເກັບ + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ໃໝ່ (ເຊັ່ນ: #alias: server) + ຍັງບໍ່ມີທີ່ຢູ່ທີ່ເຜີຍແຜ່ອື່ນເທື່ອ. + ບໍ່ມີທີ່ຢູ່ທີ່ພິມເຜີຍແຜ່ອື່ນໆເທື່ອ, ເພີ່ມທີ່ຢູ່ຂ້າງລຸ່ມນີ້. + ລຶບທີ່ຢູ່ \"%1$s\" ບໍ\? + ຍົກເລີກການເຜີຍແຜ່ທີ່ຢູ່ \"%1$s\" ບໍ\? + ເຜີຍແຜ່ + ເຜີຍແຜ່ທີ່ຢູ່ໃໝ່ດ້ວຍຕົນເອງ + ທີ່ຢູ່ອື່ນໆທີ່ເຜີຍແຜ່: + ນີ້ແມ່ນທີ່ຢູ່ຫຼັກ + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ສາມາດນໍາໃຊ້ໂດຍຜູ້ໃດຜູ້ຫນຶ່ງໃນເຊີບເວີການເຂົ້າຮ່ວມຫ້ອງຂອງທ່ານ. ເພື່ອເຜີຍແຜ່ທີ່ຢູ່, ຈໍາເປັນຕ້ອງຕັ້ງເປັນທີ່ຢູ່ຊ່ອງເກັບກ່ອນ. + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ + ເບິ່ງ ແລະ ຈັດການທີ່ຢູ່ຂອງພື້ນທີ່ນີ້. + ທີ່ຢູ່ຂອງພື້ນທີ່ + ເບິ່ງ ແລະ ຈັດການທີ່ຢູ່ຂອງຫ້ອງນີ້, ແລະການເບິ່ງເຫັນຢູ່ໃນລາຍຊື່ຫ້ອງ. + ທີ່ຢູ່ຫ້ອງ + ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມ + ການເຂົ້າເຖິງພື້ນທີ່ + ການເຂົ້າເຖິງຫ້ອງ + ໃຜສາມາດເຂົ້າເຖິງໄດ້ແນ່\? + ການປ່ຽນແປງຜູ້ທີ່ອ່ານປະຫວັດໄດ້ຈະມີຜົນກັບຂໍ້ຄວາມໃນອະນາຄົດຢູ່ໃນຫ້ອງນີ້ເທົ່ານັ້ນ. ການເບິ່ງເຫັນປະຫວັດທີ່ມີຢູ່ແລ້ວຈະບໍ່ປ່ຽນແປງ. + ຜູ້ທີ່ສາມາດອ່ານປະຫວັດໄດ້\? + ການອ່ານປະຫວັດຫ້ອງ + ຕັ້ງຄ່າບັນຊີ + ທ່ານສາມາດຈັດການການແຈ້ງເຕືອນໃນ %1$s. + ກະລຸນາສັງເກດວ່າການກ່າວເຖິງ & ການແຈ້ງເຕືອນຄໍາສໍາຄັນບໍ່ສາມາດໃຊ້ໄດ້ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໃນໂທລະສັບມືຖື. + ຈ້ງໃຫ້ຂ້າພະເຈົ້າຊາບເພື່ອ + ຫົວຂໍ້ + ຖ້າເປີດໃຊ້ງານແລ້ວ, ທ່ານຈະສະເເດງສະຖານະແບບອອບໄລນ໌ຕໍ່ກັບຜູ້ໃຊ້ອື່ນສະເໝີ, ເຖິງແມ່ນວ່າຈະໃຊ້ແອັບພລິເຄຊັນກໍຕາມ. + ໂໝດອອບໄລນ໌ + ປະກົດຕົວ + ຕະຫຼອດໄປ + 1 ເດືອນ + 1 ອາທິດ + 3 ມື້ + ໄຫວພິບ + ຫຼິ້ນສຽງ shutter + ເລືອກ + ແຫຼ່ງສື່ + ເລືອກ + ການບີບອັດ + ສື່ມວນຊົນ + ເລືອກປະເທດ + ຈັດການອີເມວ ແລະເບີໂທລະສັບທີ່ເຊື່ອມຕໍ່ກັບບັນຊີ Matrix ຂອງທ່ານ + ອີເມວ ແລະເບີໂທລະສັບ + ສະແດງຂໍ້ຄວາມທັງໝົດຈາກ %s ບໍ\? +\n +\nກະລຸນາຮັບຊາບວ່າຄຳສັ່ງນີ້ຈະປິດເປີດແອັບຄືນໃໝ່ ແລະ ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ລະຫັດຜ່ານຂອງທ່ານໄດ້ຮັບການປັບປຸງ + ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ + ອັບເດດລະຫັດຜ່ານບໍ່ສຳເລັດ + ລະຫັດຜ່ານໃໝ່ + ລະຫັດປັດຈຸບັນ + ປ່ຽນລະຫັດຜ່ານ + ລະຫັດຜ່ານ + ເບີໂທລະສັບນີ້ຖືກໃຊ້ແລ້ວ. + ທີ່ຢູ່ອີເມວນີ້ຖືກໃຊ້ແລ້ວ. + ກະລຸນາກວດເບິ່ງອີເມວຂອງທ່ານແລະກົດໃສ່ການເຊື່ອມຕໍ່. ເມື່ອສິ່ງນີ້ສຳເລັດແລ້ວ, ກົດສືບຕໍ່. + ເລືອກພາສາ + ພາສາ + ການໂຕ້ຕອບຜູ້ໃຊ້ + ເປີດໃຊ້ \'ການອະນຸຍາດການເຊື່ອມໂຍງ\' ໃນການຕັ້ງຄ່າເພື່ອເຮັດສິ່ງນີ້. + ການເຊື່ອມໂຍງຖືກປິດໃຊ້ງານ + ເຊີບເວີນີ້ບໍ່ມີນະໂຍບາຍໃດໆ. + ຫ້ອງສະຫມຸດພາກສ່ວນທີສາມ + ນະໂຍບາຍເຊີບເວີຂອງທ່ານ + ນະໂຍບາຍ homeserver ຂອງທ່ານ + ນະໂຍບາຍ ${app_name} + ຜູ້ຈັດການການເຊື່ອມໂຍງ + ອະນຸຍາດໃຫ້ການເຊື່ອມໂຍງ + ຂໍ້ມູມເຊີບເວີ + Homeserver + ເຂົ້າສູ່ລະບົບເປັນ + ການຢືນຢັນ + %1$s @ %2$s + ເຫັນຄັ້ງສຸດທ້າຍ + ອັບເດດຊື່ສາທາລະນະ + ຊື່ສາທາລະນະ + ID + ທ່ານສາມາດປິດສິ່ງນີ້ໄດ້ທຸກເວລາໃນການຕັ້ງຄ່າ + ກົດໃສ່ໃບຕອບຮັບເພື່ອອ່ານລາຍລະອຽດ. + ສະແດງໃບຕອບຮັບການອ່ານ + ສະແດງເວລາໃນຮູບແບບ 12 ຊົ່ວໂມງ + ສະແດງເວລາຂອງຂໍ້ຄວາມທັງໝົດ + ຈັດຮູບແບບຂໍ້ຄວາມໂດຍໃຊ້ syntax markdown ກ່ອນທີ່ພວກມັນຈະຖືກສົ່ງໄປ. ອັນນີ້ອະນຸຍາດໃຫ້ຈັດຮູບແບບພິເສດເຊັ່ນ: ການໃຊ້ເຄື່ອງໝາຍດາວເພື່ອສະແດງຂໍ້ຄວາມຕົວອຽງ. + ການຈັດຮູບແບບ Markdown + ໃຫ້ຜູ້ໃຊ້ອື່ນຮູ້ວ່າທ່ານກຳລັງພິມຢູ່. + ສົ່ງການແຈ້ງເຕືອນໃນການພິມ + ເບິ່ງຕົວຢ່າງລິ້ງພາຍໃນການສົນທະນາເມື່ອ homeserver ຂອງທ່ານຮອງຮັບຄຸນສົມບັດນີ້. + ຕົວຢ່າງ URL ແບບInline + ປັກໝຸດຫ້ອງດ້ວຍຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ປັກໝຸດຫ້ອງດ້ວຍການແຈ້ງເຕືອນທີ່ພາດ + ຈໍສະແດງຜົນຫນ້າທໍາອິດ + ປື້ມໂທລະສັບ + ການອະນຸຍາດຕິດຕໍ່ + ຕິດຕໍ່ພື້ນທີ່ຈັດເກັບ + ເປົ້າໝາຍການແຈ້ງເຕືອນ + ການຈັດການເຂົ້າລະຫັດລັບ + ການເຂົ້າລະຫັດລັບ + ໃຊ້ຕົວຈັດການການເຊື່ອມໂຍງເພື່ອຈັດການ bots, bridges, widget ແລະຊຸດສະຕິກເກີ. +\nຜູ້ຈັດການການເຊື່ອມໂຍງຂໍ້ມູນການຕັ້ງຄ່າ, ແລະສາມາດດັດແປງ widget, ສົ່ງການເຊື້ອເຊີນຫ້ອງແລະກໍານົດລະດັບພະລັງງານໃນນາມຂອງທ່ານ. + ການເຊື່ອມໂຢງ + ແອັບພລິເຄຊັນກຳລັງໄດ້ຮັບ PUSH + ຄໍາຮ້ອງສະຫມັກແມ່ນລໍຖ້າສໍາລັບການຊຸກຍູ້ + ທົດສອບ Push + ການລົງທະບຽນ FCM token ກັບ homeserver ບໍ່ສຳເລັດ: +\n%1$s + FCM token ສຳເລັດການລົງທະບຽນກັບ homeserver. + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ ${app_name}. ມັນສາມາດເກີດຂຶ້ນຍ້ອນເຫດຜົນຫຼາຍຢ່າງ. ບາງທີມັນອາດຈະໃຊ້ໄດ້ຖ້າຫາກທ່ານລອງໃຫມ່ໃນພາຍຫຼັງ, ທ່ານຍັງສາມາດກວດເບິ່ງວ່າບໍລິການ Google Play ບໍ່ໄດ້ຖືກຈໍາກັດໃນການນໍາໃຊ້ຂໍ້ມູນໃນການຕັ້ງຄ່າລະບົບ, ຫຼື ວ່າໂມງອຸປະກອນຂອງທ່ານແມ່ນຖືກຕ້ອງ, ຫຼືສາມາດເກີດຂື້ນໃນ ROM ແບບກໍານົດເອງ. + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ ${app_name} ແລະອີງຕາມ Google, ຂໍ້ຜິດພາດນີ້ຊີ້ໃຫ້ເຫັນວ່າອຸປະກອນມີຫຼາຍເເອັບທີ່ລົງທະບຽນກັບ FCM ຫຼາຍເກີນໄປ. ຂໍ້ຜິດພາດເກີດຂື້ນພຽງແຕ່ໃນກໍລະນີທີ່ມີແອັບຈພນວນຫຼາຍ, ດັ່ງນັ້ນຈຶ່ງບໍ່ສົ່ງຜົນກະທົບຕໍ່ຜູ້ໃຊ້ໂດຍສະເລ່ຍ. + ໃບຢັ້ງຢືນໄດ້ປ່ຽນຈາກອັນທີ່ເຊື່ອຖືກ່ອນໜ້ານີ້ ມາເປັນອັນທີ່ບໍ່ໜ້າເຊື່ອຖືໄດ້. ເຊີບເວີອາດຈະໄດ້ຕໍ່ອາຍຸໃບຢັ້ງຢືນແລ້ວ. ຕິດຕໍ່ຜູ້ເບິ່ງແຍງເໍີບເວີສໍາລັບລາຍນິ້ວທີ່ຕ້ອງການ. + ລາຍການຫ້ອງ + ຄຳເຕືອນ + ຮັບການຊຸກຍູ້ບໍ່ສຳເລັດ. ການແກ້ໄຂອາດຈະເປັນການຕິດຕັ້ງແອັບພລິເຄຊັນໃຫມ່. + ຂັ້ນສູງ + ອື່ນໆ + ຜູ້ໃຊ້ບໍ່ສົນໃຈ + ການແຈ້ງເຕືອນ + ການຕັ້ງຄ່າຜູ້ໃຊ້ + ລ້າງcacheມີເດຍ + ລ້າງ cache + ຮັກສາສື່ + ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ + ລິຂະສິດ + ແຈ້ງການພາກສ່ວນທີສາມ + ຂໍ້ຕົກລົງ & ເງື່ອນໄຂ + ເວີຊັ້ນ olm + ເວີຊັ້ນ + + %d ວິນາທີ + + ມັນເປັນ spam + ບໍ່ມີໄຟລ໌ຢູ່ໃນຫ້ອງນີ້ + %1$s ທີ່ %2$s + ໄຟລ໌ + ບໍ່ມີສື່ຢູ່ໃນຫ້ອງນີ້ + ສື່ມວນຊົນ + %1$d ຂອງ%2$d + ບໍ່ສາມາດຈັດການຂໍ້ມູນການແບ່ງປັນໄດ້ + ໝູນ ແລະຕັດ + ສະຖານທີ່ + ການສໍາຫຼວດ + ສະຕິກເກີ + ຄັງຮູບ + ກ້ອງຖ່າຍຮູບ + ຕິດຕໍ່ + ໄຟລ໌ + ເພີ່ມຮູບພາບຈາກ + ເກີດຄວາມຜິດພາດຂຶ້ນໃນຂະນະທີ່ກຳລັງດຶງໄຟລ໌ແນບມາ. + ໄຟລ໌ໃຫຍ່ເກີນທີ່ຈະອັບໂຫລດ. + + %d ຜູ້ໃຊ້ອ່ານແລ້ວ + + %s ອ່ານແລ້ວ + %1$s ແລະ %2$s ອ່ານແລ້ວ + %1$s, %2$s ແລະ %3$s ອ່ານແລ້ວ + + %1$s, %2$s ແລະ %3$d ຜູ້ອື່ນໆອ່ານ + + ໄປທີ່ດ້ານລຸ່ມ + ປິດປ້າຍການກສຳຮອງຂໍ້ມູນກະເເຈ + ສ້າງຫ້ອງໃຫມ່ + ສ້າງການສົນທະນາໃຫມ່ໂດຍກົງ + ປິດເມນູສ້າງຫ້ອງ… + ເປີດເມນູສ້າງຫ້ອງ + ເປີດຕົວນໍາທາງ + ສົ່ງໄຟລ໌ແນບ + ເບິ່ງຄືວ່າເຊີບເວີໃຊ້ເວລາດົນເກີນໄປໃນການຕອບສະໜອງ, ສິ່ງນີ້ອາດເກີດຈາກການສັນຍານການເຊື່ອມຕໍ່ບໍ່ສະຖຽນ ຫຼື ຂໍ້ຜິດພາດກັບເຊີບເວີ. ກະລຸນາລອງອີກຄັ້ງໃນອີກໄລຍະໜຶ່ງ. + ກະລຸນາລອງໃໝ່ອີກຄັ້ງເມື່ອທ່ານຍອມຮັບຂໍ້ກຳນົດ ແລະເງື່ອນໄຂຂອງ homeserver ຂອງທ່ານ. + ບັນທຶກລະອຽດ ຈະຊ່ວຍໃຫ້ນັກພັດທະນາສະຫນອງບັນທຶກເພີ່ມເຕີມເມື່ອທ່ານສົ່ງ RageShake. ເຖິງແມ່ນວ່າໃນເວລາທີ່ເປີດໃຊ້, ແອັບພລິເຄຊັນຈະບໍ່ບັນທຶກເນື້ອຫາຂໍ້ຄວາມ ຫຼື ຂໍ້ມູນສ່ວນຕົວອື່ນໆກໍ່ຕາມ. + ເປີດໃຊ້ບັນທຶກລາຍລະອຽດ. + ເຫັນດີກັບເງື່ອນໄຂການໃຫ້ບໍລິການຂອງ ເຊີບເວີ(%s) ເພື່ອອະນຸຍາດໃຫ້ຕົວທ່ານສາມາດຄົ້ນຫາໄດ້ໂດຍທີ່ຢູ່ອີເມວ ຫຼື ເບີໂທລະສັບ. + ຕອນນີ້ທ່ານກຳລັງແບ່ງປັນທີ່ຢູ່ອີເມວ ຫຼື ເບີໂທລະສັບໃນເຊີບເວີ %1$s. ທ່ານຈະຕ້ອງເຊື່ອມຕໍ່ຄືນໃໝ່ກັບ %2$s ເພື່ອຢຸດການແບ່ງປັນອີເມວ ຫຼື ເບີໂທລະສັບ. + ການຢືນຢັນລະຫັດບໍ່ຖືກຕ້ອງ. + ລະຫັດ + ຂໍ້ຄວາມຖືກສົ່ງໄປຫາ %s ແລ້ວ. ກະລຸນາໃສ່ລະຫັດຢືນຢັນ. + ເຊີບເວີທີ່ທ່ານເລືອກບໍ່ມີເງື່ອນໄຂການບໍລິການໃດໆ. ດຳເນີນການຕໍ່ຫາກທ່ານໄວ້ວາງໃຈເຈົ້າຂອງການບໍລິການ + ເຊີບເວີບໍ່ມີເງື່ອນໄຂໃນການໃຫ້ບໍລິການ + ກະລຸນາໃສ່ url ເຊີບເວີ + ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີໄດ້ + ໃສ່ URL ເຊີບເວີ + ທ່ານຕົກລົງເຫັນດີທີ່ຈະສົ່ງຂໍ້ມູນນີ້ບໍ\? + ເພື່ອຄົ້ນຫາຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວ, ທ່ານຈໍາເປັນຕ້ອງສົ່ງຂໍ້ມູນການຕິດຕໍ່ (ອີເມວ ແລະ ເບີໂທລະສັບ) ໄປຫາເຊີບເວີຂອງທ່ານ. ພວກເຮົາຄັດ ຂໍ້ມູນຂອງທ່ານກ່ອນທີ່ຈະສົ່ງເພື່ອຄວາມເປັນສ່ວນຕົວ. + ສົ່ງອີເມວ ແລະເບີໂທລະສັບໄປຫາ %s + ໃຫ້ການຍິນຍອມ + ຍົກເລີກການຍິນຍອມຂອງຂ້ອຍ + ລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານເປັນສ່ວນຕົວ. ເພື່ອຄົ້ນຫາຜູ້ໃຊ້ໃນລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານ, ພວກເຮົາຕ້ອງການໄດ້ຮັບອະນຸຍາດຈາກທ່ານເພື່ອສົ່ງຂໍ້ມູນການຕິດຕໍ່ໄປຫາເຊີບຂອງທ່ານ. + ທ່ານໄດ້ໃຫ້ການຍິນຍອມທີ່ຈະສົ່ງອີເມວຂອງທ່ານ ແລະ ເບີໂທລະສັບໄປຫາເຊີບເວີນີ້ເພື່ອຄົ້ນຫາຜູ້ໃຊ້ອື່ນໆຈາກຜູ້ຕິດຕໍ່ຂອງທ່ານ. + ສົ່ງອີເມວ ແລະ ເບີໂທລະສັບ + ພວກເຮົາໄດ້ສົ່ງອີເມວການຢືນຢັນໃຫ້ທ່ານໄປຫາ %s, ກະລຸນາກວດສອບອີເມວຂອງທ່ານກາອນ ແລະ ກົດໃສ່ການເຊື່ອມຕໍ່ການຢືນຢັນ + ພວກເຮົາໄດ້ສົ່ງອີເມວຢືນຢັນໃຫ້ທ່ານຫາ %s, ກວດສອບອີເມວຂອງທ່ານ ແລະ ກົດໃສ່ການເຊື່ອມຕໍ່ການຢືນຢັນ + ສາມາດຄົ້ນພົບເບີໂທລະສັບໄດ້ + ການຕັດການເຊື່ອມຕໍ່ຈາກເຊີບເວີຂອງທ່ານຈະເຮັດໃຫ້ຜູ້ໃຊ້ອື່ນຄນຫາທ່ານໄດ້ ແລະ ທ່ານຈະບໍ່ສາມາດເຊີນຄົນອື່ນຜ່ານທາງອີເມລ໌ ຫຼື ໂທລະສັບໄດ້. + ຕົວເລືອກການຄົ້ນພົບຈະປາກົດຂຶ້ນເມື່ອທ່ານໄດ້ເພີ່ມເບີໂທລະສັບ. + ຕົວເລືອກການຄົ້ນພົບຈະປາກົດຂຶ້ນເມື່ອທ່ານໄດ້ເພີ່ມອີເມວ. + ສາມາດຄົ້ນພົບທີ່ຢູ່ອີເມວໄດ້ + ໃນປັດຈຸບັນທ່ານບໍ່ໄດ້ໃຊ້ເຊີບເວີ. ເພື່ອຄົ້ນຫາແລະສາມາດຄົ້ນພົບໄດ້ໂດຍຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວ, ກໍານົດຄ່າຂ້າງລຸ່ມນີ້. + ຕອນນີ້ທ່ານກຳລັງໃຊ້ %1$s ເພື່ອຄົ້ນຫາ ແລະສາມາດຄົ້ນພົບໄດ້ໂດຍຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວທີ່ທ່ານຮູ້ຈັກ. + ບໍ່ມີນະໂຍບາຍສະໜອງໃຫ້ໂດຍເຊີບເວີ + ເຊື່ອງນະໂຍບາຍເຊີບເວີ + ສະແດງນະໂຍບາຍເຊີບ + ປ່ຽນເຊີບເວີ + ເປີດການຕັ້ງຄ່າ ການຄົ້ນພົບ + ຕັ້ງຄ່າເຊີບເວີ + ຕັດການເຊື່ອມຕໍ່ເຊີບເວີ + ຂໍ້ມູນເຊີບເວີ + ໃຊ້ Bots, bridges,widgets ແລະຊຸດສະຕິກເກີ + ໃຫ້ຜູ້ອື່ນສາມາດຄົ້ນຫາໄດ້ + ເງື່ອນໄຂການໃຫ້ບໍລິການ + ເບິ່ງປະຫວັດການແກ້ໄຂ + ຄຳແນະນຳ + ຜູ້ໃຊ້ທີ່ຮູ້ຈັກ + ກຳລັງສ້າງຫ້ອງ… + ລະຫັດ QR + ເພີ່ມດ້ວຍລະຫັດ QR + ສຳເນົາລິ້ງໃສ່ຄລິບບອດແລ້ວ + ເພີ່ມແຖບສະເພາະສໍາລັບການແຈ້ງເຕືອນທີ່ຍັງບໍ່ໄດ້ອ່ານໃນຫນ້າຈໍຫຼັກ. + ເປີດໃຊ້ງານການປັດເພື່ອຕອບກັບໃນທາມລາຍ + ຊອກຫາຊື່ + ຄົ້ນຫາດ້ວຍຊື່, ID ຫຼື mail + ຊື່ ຫຼື ID (#example:matrix.org) + ເບິ່ງລາຍການຫ້ອງ + ສົ່ງຂໍ້ຄວາມໃໝ່ໂດຍກົງ + ສ້າງຫ້ອງໃຫມ່ + ບໍ່ສາມາດຊອກຫາສິ່ງທີ່ທ່ານກໍາລັງຊອກຫາ\? + ກັ່ນຕອງການສົນທະນາ… + ທ່ວງເວລາລະຫວ່າງSync + ໝົດເວລາການຮ້ອງຂໍການຊິງຄ໌ + ເລີ່ມຕົ້ນໃນການboot + ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນກ່ຽວກັບຂໍ້ຄວາມທີ່ເຂົ້າມາເມື່ອແອັບຯຢູ່ໃນພື້ນຫຼັງ. + ບໍ່ມີການຊິງຄ໌ພື້ນຫຼັງ + ${app_name} ຈະຊິງຄ໌ໃນພື້ນຫຼັງເປັນໄລຍະໆຕາມເວລາທີ່ຊັດເຈນ (ກຳນົດໄດ້). +\nນີ້ຈະສົ່ງຜົນກະທົບຕໍ່ການໃຊ້ວິທະຍຸ ແລະແບັດເຕີຣີ, ຈະມີການແຈ້ງເຕືອນແບບຖາວອນທີ່ສະແດງວ່າ ${app_name} ກຳລັງຟັງເຫດການ. + ປັບໃຫ້ເຫມາະສໍາລັບເວລາທີ່ແທ້ຈິງ + ${app_name} ຈະຊິ້ງຂໍ້ມູນໃນພື້ນຫຼັງເພື່ອຮັກສາຊັບພະຍາກອນທີ່ຈຳກັດຂອງອຸປະກອນ (ແບັດເຕີຣີ). +\nອີງຕາມສະຖານະຊັບພະຍາກອນອຸປະກອນຂອງທ່ານ, ການຊິງຄ໌ອາດຈະຖືກເລື່ອນໂດຍລະບົບປະຕິບັດການ. + ເຫມາະສໍາລັບເເບັດເຕີລີ + ຮູບແບບການຊິງຄ໌ພື້ນຫຼັງ + ການຊິ້ງຂໍ້ມູນພື້ນຫຼັງ + ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນສໍາລັບການກ່າວເຖິງ & ຄໍາສໍາຄັນຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດຢູ່ໃນມືຖື. + ການຍົກລະດັບຫ້ອງ + ຂໍ້ຄວາມຈາກ bot + ໂທເຊີນ + ການເຊີນດເຂົ້າຫ້ອງ + ຄໍາສໍາຄັນ + \@room + ເຂົ້າລະຫັດຂໍ້ຄວາມກຸ່ມ + ຂໍ້ຄວາມກຸ່ມ + ເຂົ້າລະຫັດຂໍ້ຄວາມໂດຍກົງ + ຂໍ້ຄວາມໂດຍກົງ + ຊື່ຜູ້ໃຊ້ຂອງຂ້ອຍ + ຊື່ສະແດງຂອງຂ້ອຍ + ຂໍ້ຄວາມທີ່ມີ @room + ຂໍ້ຄວາມທີ່ສົ່ງໂດຍ bot + ເມື່ອຂ້ອຍຖືກເຊີນເຂົ້າຫ້ອງ + ເປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບບັນຊີນີ້ + ສຽງແຈ້ງເຕືອນ + ບໍ່ສົນໃຈການເພີ່ມປະສິດທິພາບ + ຖ້າຫາກຜູ້ໃຊ້ປະອຸປະກອນບໍ່ໄດ້ສຽບປັກອຸດປະກອນ ແລະ ຢຸດຢູ່ໃນໄລຍະໜຶ່ງ, ໂດຍທີ່ໜ້າຈໍປິດຢູ່, ອຸປະກອນຈະເຂົ້າສູ່ໂໝດ Doze ແລະ ປ້ອງກັນບໍ່ໃຫ້ແອັບເຂົ້າເຖິງເຄື່ອຂ່າຍ ແລະ ເລື່ອນວຽກ, ການຊິງຄ໌ ແລະ ການເຕືອນມາດຕະຖານຂອງເຂົາເຈົ້າ. + ${app_name} ບໍ່ໄດ້ຮັບຜົນກະທົບຈາກການເພີ່ມປະສິດທິພາບແບັດເຕີລີ. + ການເພີ່ມປະສິດທິພາບແບັດເຕີລີ + ປິດການນຳໃຊ້ຂໍ້ຈຳກັດ + ຂໍ້ຈຳກັດໃນພື້ນຫຼັງຖືກເປີດໃຊ້ສຳລັບ ${app_name}. +\nວຽກທີ່ແອັບພະຍາຍາມເຮັດຈະຖືກຈຳກັດຢ່າງແຮງໃນຂະນະທີ່ມັນຢູ່ໃນພື້ນຫຼັງ, ແລະ ອັນນີ້ອາດຈະສົ່ງຜົນກະທົບຕໍ່ການແຈ້ງເຕືອນ. +\n%1$s + ການຈຳກັດພື້ນຫຼັງຖືກປິດການນຳໃຊ້ສຳລັບ ${app_name}. ການທົດສອບນີ້ຄວນຈະດໍາເນີນການໂດຍໃຊ້ອິນເຕີເນັດມືຖື (ບໍ່ມີ WIFI). +\n%1$s + ກວດເບິ່ງຂໍ້ຈໍາກັດໃນພື້ນຫລັງ + ເປີດໃຊ້ເລີ່ມຕົ້ນ boot + ການບໍລິການຈະບໍ່ເລີ່ມຕົ້ນເມື່ອອຸປະກອນຖືກເລີ່ມຕົ້ນໃຫມ່, ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນຈົນກວ່າ ${app_name} ຈະເປີດຄັ້ງດຽວ. + ການບໍລິການຈະເລີ່ມເມື່ອອຸປະກອນຖືກເລີ່ມຕົ້ນໃຫມ່. + ເລີ່ມຕົ້ນ boot + ການແຈ້ງເຕືອນໄດ້ຖືກກົດແລ້ວ! + ກະລຸນາກົດໃສ່ການແຈ້ງເຕືອນ. ຖ້າທ່ານບໍ່ເຫັນການແຈ້ງເຕືອນ, ກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າລະບົບ. + ການສະແດງການແຈ້ງເຕືອນ + ທ່ານກຳລັງເບິ່ງການແຈ້ງເຕືອນ! ກົດທີ່ຂ້ອຍ! + (ຂັ້ນສູງ) + ເລີ່ມຕົ້ນການນໍາໃຊ້ການສໍາຮອງຂໍ້ມູນກະແຈ + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ແມ່ນປອດໄພດ້ວຍການເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງ. ພຽງແຕ່ທ່ານ ແລະຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈເພື່ອອ່ານຂໍ້ຄວາມເຫຼົ່ານີ້. +\n +\nສຳຮອງຂໍ້ມູນກະແຈຂອງທ່ານຢ່າງປອດໄພເພື່ອຫຼີກເວັ້ນການສູນເສຍພວກມັນ. + ຂໍ້ຄວາມເຂົ້າລະຫັດບໍ່ເຄີຍສູນເສຍ + ກະລຸນາລຶບລະຫັດຜ່ານຖ້າທ່ານຕ້ອງການ ${app_name} ເພື່ອສ້າງລະຫັດການກູ້ຂໍ້ມູນ. + ປະໂຫຍກລະຫັດຜ່ານອ່ອນເກີນໄປ + ກະລຸນາໃສ່ລະຫັດຜ່ານ + ລະຫັດຜ່ານບໍ່ກົງກັນ + ໃສ່ລະຫັດຜ່ານ + ຢືນຢັນລະຫັດຜ່ານ + ສ້າງລະຫັດຜ່ານ + ບໍ່ພົບ Google Play Services APK ທີ່ຖືກຕ້ອງ. ການແຈ້ງເຕືອນອາດຈະບໍ່ໃຊ້ງານຢ່າງຖືກຕ້ອງ. + %d+ + %1$s: %2$s + ຫຼຸດລົງ + ຂະຫຍາຍ + ຂໍອະໄພ, ເກີດຄວາມຜິດພາດຂຶ້ນ + ກະລຸນາ %s ເພື່ອສືບຕໍ່ໃຊ້ບໍລິການນີ້. + ກະລຸນາ %s ເພື່ອເພີ່ມຂີດຈຳກັດນີ້. + homeserver ນີ້ຮອດຂີດຈຳກັດຜູ້ໃຊ້ປະຈຳເດືອນແລ້ວ. + " homeserver ນີ້ຮອດຂີດຈຳກັດຜູ້ໃຊ້ລາຍເດືອນຂອງມັນແລ້ວ ດັ່ງນັ້ນ <b>ຜູ້ໃຊ້ບາງຄົນຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້</b>." + homeserver ນີ້ ມີຊັບພະຍາກເກີນຂີດຈຳກັດໃດໜຶ່ງ. + ເຊີບເວີ homeserver ນີ້ເກີນຂີດຈຳກັດໃນຊັບພະຍາກອນຂອງມັນ ດັ່ງນັ້ນ <b>ຜູ້ໃຊ້ບາງຄົນຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້</b>. + ຕິດຕໍ່ຜູ້ເບິ່ງແຍງການບໍລິການຂອງທ່ານ + ກົດທີ່ນີ້ເພື່ອເບິ່ງຂໍ້ຄວາມເກົ່າ + ສືບຕໍ່ການສົນທະນາອື່ນໃນຫ້ອງນີ້ + ການສົນທະນາສືບຕໍ່ຢູ່ທີ່ນີ້ + ຫ້ອງນີ້ໄດ້ຖືກປ່ຽນແທນ ແລະບໍ່ມີການເຄື່ອນໄຫວອີກຕໍ່ໄປ. + ກະລຸນາໃສ່ລະຫັດຜ່ານຂອງທ່ານ. + ກະລຸນາໃສ່ຊື່ຜູ້ໃຊ້. + ປິດການນຳໃຊ້ບັນຊີ + ກະລຸນາລືມຂໍ້ຄວາມທັງໝົດທີ່ຂ້ອຍໄດ້ສົ່ງໄປເມື່ອບັນຊີຂອງຂ້ອຍຖືກປິດການນຳໃຊ້ (ຄຳເຕືອນ: ອັນນີ້ຈະເຮັດໃຫ້ຜູ້ໃຊ້ໃນອະນາຄົດເຫັນການສົນທະນາທີ່ບໍ່ຄົບຖ້ວນ) + ສິ່ງນີ້ຈະເຮັດໃຫ້ບັນຊີຂອງທ່ານບໍ່ສາມາດນຳໃຊ້ໄດ້ຢ່າງຖາວອນ. ທ່ານຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້, ແລະບໍ່ມີໃຜຈະສາມາດລົງທະບຽນ ID ຜູ້ໃຊ້ດຽວກັນໄດ້. ນີ້ຈະເຮັດໃຫ້ບັນຊີຂອງທ່ານອອກຈາກຫ້ອງທັງຫມົດທີ່ໄດ້ເຂົ້າຮ່ວມ, ແລະມັນຈະເອົາລາຍລະອຽດບັນຊີຂອງທ່ານອອກຈາກເຊີບເວີຂອງທ່ານ. <b>ການດໍາເນີນການນີ້ບໍ່ສາມາດປີ້ນຄືນໄດ້</b>. +\n +\nການປິດໃຊ້ງານບັນຊີຂອງທ່ານ <b>ບໍ່ໄດ້ເປັນຄ່າເລີ່ມຕົ້ນເຮັດໃຫ້ພວກເຮົາລືມຂໍ້ຄວາມທີ່ທ່ານໄດ້ສົ່ງໄປ</b>. ຖ້າທ່ານຕ້ອງການໃຫ້ພວກເຮົາລືມຂໍ້ຄວາມຂອງທ່ານ, ກະລຸນາຫມາຍໃສ່ໃນກ່ອງຂ້າງລຸ່ມນີ້. +\n +\nການເບິ່ງເຫັນຂໍ້ຄວາມໃນ Matrix ແມ່ນຄ້າຍຄືກັນກັບອີເມວ. ການລືມຂໍ້ຄວາມຂອງພວກເຮົາຫມາຍຄວາມວ່າຂໍ້ຄວາມທີ່ທ່ານໄດ້ສົ່ງໄປຈະບໍ່ຖືກແບ່ງປັນກັບຜູ້ໃຊ້ໃຫມ່ຫຼືບໍ່ໄດ້ລົງທະບຽນ, ແຕ່ຜູ້ໃຊ້ທີ່ລົງທະບຽນແລ້ວທີ່ມີການເຂົ້າເຖິງຂໍ້ຄວາມເຫຼົ່ານີ້ຈະຍັງຄົງມີການເຂົ້າເຖິງສໍາເນົາຂອງພວກເຂົາ. + ປິດການນຳໃຊ້ບັນຊີ + ກວດເບິ່ງດຽວນີ້ + ເພື່ອສືບຕໍ່ນຳໃຊ້ %1$s homeserver ທ່ານຕ້ອງທົບທວນຄືນ ແລະ ຕົກລົງເຫັນດີກັບຂໍ້ກຳນົດ ແລະເງື່ອນໄຂ. + ຮູບແທນຕົວ + ເຫດຜົນ: %1$s + ທ່ານໄດ້ຖືກຫ້າມຈາກ %1$s ໂດຍ %2$s + ທ່ານໄດ້ຖືກລຶບອອກຈາກ %1$s ໂດຍ %2$s + ເຊີນ + ຫ້ອງ + ຫນ້າທໍາອິ + ສ້າງ + ຂໍ້ຄວາມເຂົ້າລະຫັດ + ສຽງດັງ + ງຽບ + ປິດ + Markdown ໄດ້ຖືກປິດໃຊ້ງານ. + ເປີດໃຊ້ Markdown ແລ້ວ. + ສະແດງຂໍ້ມູນກ່ຽວກັບຜູ້ໃຊ້ + ເພື່ອແກ້ໄຂການຈັດການແອັບ Matrix + ເປີດ/ປິດ markdown + ປ່ຽນຮູບແທນຕົວຂອງທ່ານຢູ່ໃນຫ້ອງປັດຈຸບັນນີ້ເທົ່ານັ້ນ + ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງປັດຈຸບັນ + ປ່ຽນຊື່ສະແດງຂອງທ່ານໃນຫ້ອງປັດຈຸບັນເທົ່ານັ້ນ + ປ່ຽນຊື່ສະແດງຂອງທ່ານ + ລຶບຜູ້ໃຊ້ທີ່ມີ ID ໃຫ້ອອກຈາກຫ້ອງນີ້ + ກໍານົດຫົວຂໍ້ຫ້ອງ + ອອກຈາກຫ້ອງ + ເຂົ້າຮ່ວມຫ້ອງຕາມທີ່ຢູ່ໄດ້ລະບຸໃຫ້ + ເຊີນຜູ້ໃຊ້ທີ່ມີ id ໃຫ້ກັບຫ້ອງປະຈຸບັນ + ກຳນົດຊື່ຫ້ອງ + Deops ຜູ້ໃຊ້ທີ່ມີ ID + ກໍານົດລະດັບພະລັງງານຂອງຜູ້ໃຊ້ + ຢຸດເຊົາການບໍ່ສົນໃຈຜູ້ໃຊ້, ສະແດງຂໍ້ຄວາມຂອງພວກເຂົາຕໍ່ໄປ + ບໍ່ສົນໃຈຜູ້ໃຊ້, ເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າໂດຍທ່ານ + ຍົກເລີກການຫ້າມຜູ້ໃຊ້ທີ່ມີ ID + ຫ້າມຜູ້ໃຊ້ທີ່ມີ ID ທີ່ກຳນົດໃຫ້ + ສະແດງການປະຕິບັດ + ຄຳສັ່ງ \"%s\" ຖືກຮັບຮູ້ແຕ່ບໍ່ຮອງຮັບໃນກະທູ້. + ຄໍາສັ່ງ \"%s\" ຕ້ອງການຕົວກໍານົດການເພີ່ມເຕີມ, ຫຼື ບາງພາລາມິເຕີບໍ່ຖືກຕ້ອງ. + ຄຳສັ່ງທີ່ບໍ່ຮູ້ຈັກ: %s + ຄໍາສັ່ງຜິດພາດ + ບໍ່ສົນໃຈ + ການຮ້ອງຂໍການແບ່ງປັນທີ່ສໍາຄັນ + ແບ່ງປັນ + ເລີ່ມການຢັ້ງຢືນ + ລະບົບທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນກໍາລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. +\nຊື່ລະບົບ: %1$s +\nເຫັນຄັ້ງສຸດທ້າຍ: %2$s +\nຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບອື່ນ, ບໍ່ສົນໃຈຄໍາຮ້ອງຂໍນີ້. + ລະບົບທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນຂອງທ່ານ \'%s\' ກໍາລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. + ລະບົບໃໝ່ກຳລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. +\nຊື່ລະບົບ: %1$s +\nເຫັນຄັ້ງສຸດທ້າຍ: %2$s +\nຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບອື່ນ, ບໍ່ສົນໃຈຄໍາຮ້ອງຂໍນີ້. + ທ່ານໄດ້ເພີ່ມລະບົບໃໝ່ \'%s\', ເຊິ່ງກຳລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. + ເພື່ອສືບຕໍ່, ທ່ານຈໍາເປັນຕ້ອງຍອມຮັບເງື່ອນໄຂຂອງການບໍລິການນີ້. + ເລີ່ມກ້ອງລະບົບແທນໜ້າຈໍກ້ອງແບບກຳນົດເອງ. + ໃຊ້ກ້ອງຖ່າຍຮູບລູ້ນດັ່ງເດີມ + ບໍ່ມີ widget ທີ່ໃຊ້ງານຢູ່ + ຈັດການການເຊື່ອມໂຍງ + ເພີ່ມແອັບ Matrix + ບໍ່ມີparameter ທີ່ຕ້ອງການ. + ບໍ່ເຫັນຫ້ອງ %s. + ບໍ່ມີ user_id ໃນການຮ້ອງຂໍ. + ບໍ່ມີ room_id ໃນການຮ້ອງຂໍ. + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເຮັດສິ່ງນັ້ນຢູ່ໃນຫ້ອງນີ້. + ທ່ານບໍ່ໄດ້ຢູ່ໃນຫ້ອງນີ້. + ລະດັບພະລັງງານຈະຕ້ອງເປັນຈຳນວນບວກ. + ສົ່ງຄຳຮ້ອງຂໍບໍ່ສຳເລັດ. + ບໍ່ສາມາດສ້າງ widget ໄດ້. + ອ່ານສື່ທີ່ມີການປ້ອງກັນ DRM + ໃຊ້ໄມໂຄຣໂຟນ + ໃຊ້ກ້ອງຖ່າຍຮູບ + ບລັອກທັງໝົດ + ອະນຸຍາດ + widget ນີ້ຕ້ອງການໃຊ້ຊັບພະຍາກອນຕໍ່ໄປນີ້: + ອອກຈາກກອງປະຊຸມປະຈຸບັນ ແລະປ່ຽນໄປຫາກອງປະຊຸມອື່ນບໍ\? + ຂໍອະໄພ, ເກີດຄວາມຜິດພາດຂຶ້ນໃນຂະນະທີ່ພະຍາຍາມເຂົ້າຮ່ວມກອງປະຊຸມ + ຂໍອະໄພ, ການປະຊຸມການໂທດ້ວຍ Jitsi ບໍ່ຮອງຮັບໃນອຸປະກອນເກົ່າ (ອຸປະກອນທີ່ມີ Android OS ຕ່ຳກວ່າ 6.0) + ID ຫ້ອງ + ໄອດີວິດເຈັດ + ຫົວຂໍ້ຂອງທ່ານ + ID ຜູ້ໃຊ້ຂອງທ່ານ + URL ຮູບແທນຕົວຂອງທ່ານ + ຊື່ສະແດງຂອງທ່ານ + ຖອນສິດການເຂົ້າເຖິງສໍາລັບຂ້ອຍ + ເປີດໃນ browser + ໂຫຼດວິດເຈັດຄືນໃໝ່ + ໂຫຼດ widget ບໍ່ສຳເລັດ. +\n%s + ການໃຊ້ອາດຈະແບ່ງປັນຂໍ້ມູນກັບ %s: + ການໃຊ້ອາດຈະຕັ້ງ cookies ແລະແບ່ງປັນຂໍ້ມູນກັບ %s: + widget ນີ້ຖືກເພີ່ມໂດຍ: + ໂຫຼດ Widget + ວິດເຈັດ + ວິດເຈັດທີ່ໃຊ້ຢູ່ + ເບິ່ງ + + %d ວິດເຈັດທີ່ໃຊ້ຢູ່ + + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບວິດເຈັດອອກຈາກຫ້ອງນີ້\? + ໃຫຍ່ + ໃຫຍ່ທີ່ສຸດ + ໃຫຍ່ກວ່າ + ໃຫຍ່ + ປົກກະຕິ + ນ້ອຍ + ນ້ອຍໆ + ຂະໜາດຕົວອັກສອນ + %1$s: %2$s%3$s + %1$s: %2$s + ** ການສົ່ງ ບໍ່ສໍາເລັດ- ກະລຸນາເປີດຫ້ອງ + ຂ້ອຍ + ເຊີນໃໝ່ + ຂໍ້ຄວາມໃໝ່ + ຫ້ອງ + ເຫດການໃໝ່ + %1$s ແລະ %2$s + %1$s ໃນ %2$s ແລະ %3$s + %1$s ໃນ %2$s + + %d ການແຈ້ງເຕືອນ + + + %1$s: %2$d ຂໍ້ຄວາມ + + + %d ການເຊີນ + + + %d ຫ້ອງ + + + %d ຂໍ້ຄວາມແຈ້ງເຕືອນທີ່ຍັງບໍ່ໄດ້ອ່ານ + + ເຊີບເວີນີ້ມີຢູ່ໃນລາຍຊື່ແລ້ວ + ບໍ່ສາມາດຊອກຫາເຊີບເວີນີ້ ຫຼື ລາຍຊື່ຫ້ອງໄດ້ + ໃສ່ຊື່ຂອງເຊີບເວີໃໝ່ທີ່ທ່ານຕ້ອງການສຳຫຼວດ. + ເພີ່ມເຊິບເວີໃຫມ່ + ເຊີບເວີຂອງທ່ານ + ທັງໝົດ %s ຫ້ອງ + ຫ້ອງທັງໝົດຢູ່ໃນເຊີບເວີ %s + ຊື່ເຊີບເວີ + ເລືອກລາຍການຫ້ອງ + ຖ້າລະຫັດບໍ່ກົງກັນ, ຄວາມປອດໄພຂອງການສື່ສານຂອງທ່ານອາດຈະຖືກທໍາລາຍ. + ຂໍ້ຄວາມໃນກຸ່ມສົນທະນາ + ຂໍ້ຄວາມການສົນທະນາຫນຶ່ງຕໍ່ຫນຶ່ງ + ຂໍ້ຄວາມທີ່ມີຊື່ຜູ້ໃຊ້ຂອງຂ້ອຍ + ຂໍ້ຄວາມທີ່ມີຊື່ສະແດງຂອງຂ້ອຍ + ເມື່ອມີການຍົກລະດັບຫ້ອງ + ຂໍ້ຄວາມເຂົ້າລະຫັດໃນການສົນທະນາກຸ່ມ + ຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດໃນການສົນທະນາແບບຫນຶ່ງຕໍ່ຫນຶ່ງ + ເລືອກສີ LED, ການສັ່ນ, ສຽງ… + ຕັ້ງຄ່າການແຈ້ງເຕືອນງຽບ + ຕັ້ງຄ່າການແຈ້ງເຕືອນການໂທ + ຕັ້ງຄ່າການແຈ້ງເຕືອນສຽງລົບກວນ + ປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບລະບົບນີ້ + ຫ້ອງນີ້ກຳລັງຕິດຕັ້ງເວີຊັ້ນຫ້ອງ %s, ເຊິ່ງ homeserver ນີ້ໄດ້ເຮັດເຄື່ອງໝາຍບໍ່ສະຖຽນໄວ້. + ທ່ານຕ້ອງໄດ້ຮັບການອະນຸຍາດເພື່ອຍົກລະດັບຫ້ອງ + ອັບເດດພຶ້ນທີ່ອັດຕະໂນມັດ + ເຊີນຜູ້ໃຊ້ອັດຕະໂນມັດ + ທ່ານຈະຍົກລະດັບຫ້ອງນີ້ຈາກ %1$s ເປັນ %2$s. + ການຍົກລະດັບຫ້ອງແມ່ນເປັນການດຳເນີນການຂັ້ນສູງ ແລະ ປົກກະຕິແລ້ວແມ່ນແນະນຳເມື່ອຫ້ອງບໍ່ສະຖຽນເນື່ອງຈາກມີຂໍ້ບົກພ່ອງ, ຄຸນສົມບັດທີ່ຂາດຫາຍໄປ ຫຼື ຊ່ອງໂຫວ່ດ້ານຄວາມປອດໄພ. +\nປົກກະຕິແລ້ວນີ້ມີຜົນຕໍ່ການການປະມວນຜົນຫ້ອງຢູ່ໃນເຊີບເວີ. + ຍົກລະດັບຫ້ອງສ່ວນຕົວ + ຍົກລະດັບຫ້ອງສາທາລະນະ + ຕ້ອງການຍົກລະດັບ + ຍົກລະດັບ + ກະລຸນາລໍຖ້າ, ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ເຂົ້າຮ່ວມຫ້ອງແທນ + ບໍ່ມີຊື່ຫ້ອງ + ບາງຫ້ອງອາດຈະຖືກເຊື່ອງໄວ້ເພາະວ່າເປັນສ່ວນຕົວ ແລະ ທ່ານຕ້ອງໄດ້ຮັບການເຊີນ + ບາງຫ້ອງອາດຈະຖືກເຊື່ອງໄວ້ເພາະວ່າເປັນສ່ວນຕົວ ແລະ ທ່ານຕ້ອງການການເຊີນ. +\nທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເພີ່ມຫ້ອງ. + ພື້ນທີ່ນີ້ບໍ່ມີຫ້ອງ + ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານສຳລັບຂໍ້ມູນເພີ່ມເຕີມ + ເບິ່ງຄືວ່າ homeserver ຂອງທ່ານຍັງບໍ່ຮອງຮັບ ພື້ນທີ່ ເທື່ອ + ຮູ້ສຶກວ່າທົດລອງ\? +\nທ່ານສາມາດເພີ່ມພື້ນທີ່ທີ່ມີຢູ່ໄປຍັງອີກພຶ້ນທີ່ໜຶ່ງ. + ຫ້ອງທັງໝົດທີ່ທ່ານຢູ່ຈະຖືກສະແດງຢູ່ໃນໜ້າທຳອິດ. + ສະແດງຫ້ອງທັງໝົດໃນໜ້າທຳອິດ + ຈັດການຫ້ອງ ແລະ ພື້ນທີ່ຕ່າງໆ + ໝາຍວ່າບໍ່ໄດ້ແນະນຳ + ໝາຍຕາມທີ່ແນະນຳ + ແນະນຳ + ຈັດການຫ້ອງ + ຊອກຫາຄົນທີ່ບໍ່ໄດ້ຢູ່ໃນ %s ບໍ\? + %s ເຊີນທ່ານ + ໝາຍເຫດ: ແອັບຈະຖືກເປີດຄືນໃໝ່ + ເປີດໃຊ້ຂໍ້ຄວາມກະທູ້ + ລະບົບຂອງທ່ານຈະສົ່ງບັນທຶກອັດຕະໂນມັດເມື່ອເກີດຄວາມຜິດພາດທີ່ບໍ່ສາມາດຖອດລະຫັດໄດ້ + ລາຍງານຄວາມຜິດພາດໃນການຖອດລະຫັດອັດຕະໂນມັດ. + ທ່ານໄດ້ຮັບເຊີນ + ພື້ນທີ່ເປັນວິທີໃໝ່ໃນການຈັດກຸ່ມຫ້ອງ ແລະ ຄົນ. + ເພີ່ມພື້ນທີ່ໃສ່ບ່ອນໃດກໍໄດ້ທີ່ທ່ານຈັດການ. + ເພີ່ມພື້ນທີ່ທີ່ມີຢູ່ + ເພີ່ມຫ້ອງທີ່ມີຢູ່ + ເພີ່ມຫ້ອງ ແລະພື້ນທີ່ທີ່ມີຢູ່ + ເລືອກສິ່ງທີ່ຕ້ອງອອກໄປ + ອອກຈາກຫ້ອງ ແລະພື້ນທີ່ສະເພາະ… + ຢ່າອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ໃດໆ + ອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ທັງໝົດ + ທ່ານພຽງຜູ້ທີ່ເບິ່ງແຍງລະບົບພື້ນທີ່ນີ້. ການອອກຈາກລະບົບນັ້ນຫມາຍຄວາມວ່າບໍ່ມີໃຜຄວບຄຸມມັນ. + ທ່ານຈະບໍ່ສາມາດເຂົ້າຮ່ວມຄືນໃໝ່ໄດ້ເວັ້ນເສຍແຕ່ວ່າທ່ານໄດ້ຮັບເຊີນຄືນໃໝ່. + ທ່ານເປັນຄົນດຽວຢູ່ທີ່ນີ້. ຖ້າທ່ານອອກໄປ, ບໍ່ມີໃຜຈະສາມາດເຂົ້າຮ່ວມໃນອະນາຄົດ, ລວມທັງທ່ານ. + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກ %s\? + ອອກໄປ + ເພີ່ມຫ້ອງ + ສຳຫຼວດຫ້ອງ + + %d ຄົນທີ່ທ່ານຮູ້ຈັກໄດ້ເຂົ້າຮ່ວມຢູ່ກ່ອນແລ້ວ + + ການຄົ້ນພົບ (%s) + ສຳເລັດການຕັ້ງຄ່າ + ເຊີນທາງອີເມວ, ຊອກຫາລາຍຊື່ຜູ້ຕິດຕໍ່ ແລະ ອື່ນໆອີກ… + ສຳເລັດການຕັ້ງຄ່າການຄົ້ນຫາ. + ໃນປັດຈຸບັນນີ້ທ່ານບໍ່ໄດ້ໃຊ້ເຊີບເວີ. ເພື່ອເຊີນເພື່ອນຮ່ວມທີມ ແລະ ສາມາດຄົ້ນຫາໄດ້ໂດຍພວກເຂົາ,ກຳນົດຄ່າຂ້າງລຸ່ມນີ້. + ບໍ່ສາມາດເຂົ້າເຖິງນາມແຝງນີ້ໄດ້ໃນເວລານີ້. +\nລອງໃໝ່ໃນພາຍຫຼັງ, ຫຼື ຖາມຜູ້ເບິ່ງແຍງຫ້ອງເພື່ອກວດເບິ່ງວ່າທ່ານມີການເຂົ້າເຖິງຫຼືບໍ່. + ເຂົ້າຮ່ວມເລີຍ + ສິ້ນສຸດການໂທດ້ວຍວິດີໂອ • %1$s + ສິ້ນສຸດການໂທ • %1$s + ການໂທວິດີໂອ + ໂທດ້ວຍສຽງ + ໂທວິດີໂອເຂົ້າມາ + ສາຍໂທເຂົ້າ + ໂທກັບ + ການໂທນີ້ສິ້ນສຸດລົງ + %1$s ປະຕິເສດການໂທນີ້ + ທ່ານໄດ້ປະຕິເສດການໂທນີ້ + ຍົກເລີກການປ່ຽນແປງ + ມີການປ່ຽນແປງທີ່ບໍ່ໄດ້ບັນທຶກໄວ້. ຍົກເລີກການປ່ຽນແປງບໍ\? + ບໍ່ທັນໄດ້ສ້າງຫ້ອງ. ຍົກເລີກການສ້າງຫ້ອງບໍ\? + ການເຊື່ອມຕໍ່ບໍ່ຖືກຕ້ອງ + ບໍ່ໄດ້ສະແກນລະຫັດ QR! + ລະຫັດ QR ບໍ່ຖືກຕ້ອງ (URI ບໍ່ຖືກຕ້ອງ)! + DM ຕົວເອງບໍ່ໄດ້! + ແບ່ງປັນຂໍ້ຄວາມ + ບໍ່ພົບຫ້ອງນີ້. ໃຫ້ແນ່ໃຈວ່າມີຢູ່. + ບໍ່ສາມາດເປີດຫ້ອງທີ່ຖືກຫ້າມໄດ້. + ປ່ຽນລະຫັດ PIN ປັດຈຸບັນຂອງທ່ານ + ປ່ຽນລະຫັດ PIN + ຕ້ອງໃຊ້ລະຫັດ PIN ທຸກໆຄັ້ງທີ່ທ່ານເປີດ ${app_name}. + ຕ້ອງໃຊ້ລະຫັດ PIN ຫຼັງຈາກບໍ່ໄດ້ໃຊ້ ${app_name}ເປັນເວລາ 2 ນາທີ. + ຕ້ອງການລະຫັດ PIN ຫຼັງຈາກ 2 ນາທີ + ສະແດງສະເພາະແຕ່ຈໍານວນຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານໃນການແຈ້ງເຕືອນເທົ່ານັ້ນ. + ສະແດງລາຍລະອຽດເຊັ່ນ: ຊື່ຫ້ອງ ແລະ ເນື້ອໃນຂໍ້ຄວາມ. + ສະແດງເນື້ອຫາໃນການແຈ້ງເຕືອນ + ລະຫັດ PIN ເປັນວິທີດຽວທີ່ຈະປົດລັອກ ${app_name}. + ເປີດໃຊ້ biometrics ສະເພາະອຸປະກອນ ເຊັ່ນ: ລາຍນິ້ວມື ແລະ ການຮັບຮູ້ໃບໜ້າ. + ເປີດໃຊ້ biometrics + ຖ້າທ່ານຕ້ອງການຕັ້ງຄ່າຄືນລະຫັດ PIN ຂອງທ່ານ, ແຕະ ລືມລະຫັດ PIN ເພື່ອອອກຈາກລະບົບ ແລະຕັ້ງຄ່າຄືນໃໝ່. + ເປີດໃຊ້ງານ PIN + ຕັ້ງຄ່າການປ້ອງກັນ + ປົກປ້ອງການເຂົ້າເຖິງໂດຍໃຊ້ ລະຫັດ PIN ແລະ biometrics. + ປົກປ້ອງການເຂົ້າເຖິງ + ເພື່ອຕັ້ງຄ່າລະຫັດ PIN ຂອງທ່ານ, ທ່ານຈະຕ້ອງເຂົ້າສູ່ລະບົບໃໝ່ ແລະສ້າງລະຫັດ PIN ໃໝ່. + ລະຫັດ PIN ໃໝ່ + ຕັ້ງຄ່າ PIN ຄືນໃໝ່ + ລືມ PIN ບໍ\? + ໃສ່ PIN ຂອງທ່ານ + ການກວດສອບ PIN ບໍ່ສຳເລັດ, ກະລຸນາແຕະລະຫັດໃໝ່. + ຢືນຢັນ PIN + ເລືອກ PIN ເພື່ອຄວາມປອດໄພ + ມີຂໍ້ຜິດພາດຫຼາຍເກີນໄປ, ທ່ານໄດ້ອອກຈາກລະບົບແລ້ວ + ຄໍາເຕືອນ! ຄວາມພະຍາຍາມຄັ້ງສຸດທ້າຍກ່ອນອອກຈາກລະບົບ! + + %d ການລົງທະບຽນ + + + ລະຫັດຜິດ, ພະຍາຍາມອີກ %dຄັ້ງ + + ກວດເບິ່ງການຕັ້ງຄ່າຂອງທ່ານເພື່ອເປີດໃຊ້ການແຈ້ງເຕືອນແບບ push + ການແຈ້ງເຕືອນແບບ Pushຖືກປິດໃຊ້ງານ + ຜູ້ໃຊ້ UnBan ບໍ່ສຳເລັດ + ຫ້າມໂດຍ %1$s + ຍົກເລີກການເຊີນໄປຫາ %1$s ບໍ\? + ຍົກເລີກຄຳເຊີນ + ຄົ້ນຫາລາຍຊື່ຕິດຕໍ່ໃນ Matrix + ບັນຊີຕິດຕໍ່ + ປື້ມບັນທຶກການຕິດຕໍ່ຂອງທ່ານຫວ່າງເປົ່າ + ກຳລັງດຶງຂໍ້ມູນລາຍຊື່ຜູ່ຕິດຕໍ່ຂອງທ່ານ… + ບັນທຶກລະຫັດການກູ້ຄືນໃນ + ຮຽນຮູ້ເພີ່ມເຕີມ + ໄດ້ແລ້ວ + ພວກເຮົາຮູ້ສຶກຕື່ນເຕັ້ນທີ່ຈະປະກາດວ່າພວກເຮົາໄດ້ປ່ຽນຊື່! ແອັບຂອງທ່ານໄດ້ອັບເດດແລ້ວ ແລະ ທ່ານເຂົ້າສູ່ລະບົບບັນຊີຂອງທ່ານແລ້ວ. + Riot ດຽວນີ້ເປັນອົງປະກອບແລ້ວ! + ກຳລັງລໍຖ້າປະຫວັດການເຂົ້າລະຫັດ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເນື່ອງຈາກຜູ້ສົ່ງບໍ່ໄດ້ສົ່ງກະແຈໃຫ້ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເພາະວ່າລະບົບຂອງທ່ານບໍ່ໄດ້ຮັບຄວາມໄວ້ວາງໃຈຈາກຜູ້ສົ່ງ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເພາະວ່າທ່ານໄດ້ຖືກບັລອກໂດຍຜູ້ສົ່ງ + ເນື່ອງຈາກການເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ, ທ່ານອາດຕ້ອງລໍຖ້າໃຫ້ຂໍ້ຄວາມຂອງຜູ້ໃດຜູ້ໜຶ່ງມາຮອດ ເພາະວ່າກະແຈການເຂົ້າລະຫັດບໍ່ຖືກສົ່ງໃຫ້ທ່ານຢ່າງຖືກຕ້ອງ. + ກຳລັງລໍຖ້າຂໍ້ຄວາມນີ້, ອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ + ກຳນົດຮູບແທນຕົວ + ທ່ານໄດ້ປ່ຽນການຕັ້ງຄ່າຫ້ອງສຳເລັດແລ້ວ + ຫົວຂໍ້ + ຊື່ຫ້ອງ + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພຂອງທ່ານອີກຄັ້ງເພື່ອຢືນຢັນ. + ປະໂຫຍກຄວາມປອດໄພ + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພທີ່ທ່ານຮູ້, ໃຊ້ເພື່ອຮັບປະກັນຄວາມລັບຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ກໍານົດປະໂຫຍກຄວາມປອດໄພ + ເກັບຮັກສາກະແຈຄວາມປອດໄພຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ, ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ ຫຼືບ່ອນປອດໄພ. + ບັນທຶກກະແຈຄວາມປອດໄພຂອງທ່ານ + ກະລຸນາໃສ່ປະໂຫຍກລັບທີ່ທ່ານຮູ້, ແລະສ້າງລະຫັດສໍາລັບການສໍາຮອງຂໍ້ມູນ. + ໃຊ້ປະໂຫຍກຄວາມປອດໄພ + ສ້າງກະແຈຄວາມປອດໄພເພື່ອເກັບຮັກສາໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ ຫຼືບ່ອນປອດໄພ. + ໃຊ້ກະແຈຄວາມປອດໄພ + ຕັ້ງຄ່າ + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດໂດຍການສໍາຮອງລະຫັດຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ການສຳຮອງຂໍ້ມູນທີ່ປອດໄພ + ເລີ່ມກ້ອງຖ່າຍຮູບ + ຢຸດກ້ອງຖ່າຍຮູບ + ເປີດສຽງໄມໂຄຣໂຟນ + ປິດສຽງໄມໂຄຣໂຟນ + ເປີດການສົນທະນາ + ບົດບາດ + ກໍານົດພາລະບົດບາດ + ສົ່ງ + ກະລຸນາໃສ່ URL ຂອງເຊີບເວີ + ອີກທາງເລືອກ, ທ່ານສາມາດໃສ່ URL ຂອງເຊີບເວີອື່ນໆ + ໃຊ້ %1$s + homeserver ຂອງທ່ານ (%1$s) ສະເຫນີໃຫ້ນໍາໃຊ້ %2$s ສໍາລັບ ເຊີບເວີຂອງທ່ານ + ຍັງບໍ່ມີການຍິນຍອມຂອງຜູ້ໃຊ້. + ບໍ່ມີການເຊື່ອມໂຍງໃນປະຈຸບັນກັບຕົວລະບຸນີ້. + ສະມາຄົມດັ່ງກ່າວບໍ່ສຳເລັດ. + ເພື່ອຄວາມເປັນສ່ວນຕົວຂອງທ່ານ, ${app_name} ຮອງຮັບແຕ່ການສົ່ງອີເມວ ແລະ ເບີໂທລະສັບຜູ້ໃຊ້ເທົ່ານັ້ນ. + ກ່ອນອື່ນກະລຸນາຍອມຮັບເງື່ອນໄຂຂອງເຊີບເວີໃນການຕັ້ງຄ່າ. + ກະລຸນາກຳນົດຄ່າເຊີບເວີກ່ອນ. + ການດໍາເນີນງານນີ້ບໍ່ສາມາດເຮັດໄດ້. homeserver ແມ່ນລ້າສະໄຫມ. + ເຊີບເວີນີ້ລ້າສະໄໝແລ້ວ. ${app_name} ຮອງຮັບ API V2 ເທົ່ານັ້ນ. + ຕັດການເຊື່ອມຕໍ່ຈາກເຊີບເວີ %s ບໍ\? + ເປີດເງື່ອນໄຂຂອງ %s + ກຳລັງໂຫຼດພາສາທີ່ມີ… + ພາສາອື່ນໆທີ່ມີຢູ່ + ພາສາປັດຈຸບັນ + ແບ່ງປັນລະຫັດນີ້ກັບຜູ້ຄົນເພື່ອໃຫ້ເຂົາເຈົ້າສາມາດສະແກນໄດ້ ເພື່ອເພີ່ມທ່ານ ແລະເລີ່ມການສົນທະນາ. + ລະຫັດຂອງຂ້ອຍ + ຂໍ້ມູນບັນຊີ + ແບ່ງປັນລະຫັດຂອງຂ້ອຍ + ສະແກນ QR code + ພວກເຮົາບໍ່ສາມາດເຊີນຜູ້ໃຊ້ໄດ້. ກະລຸນາກວດເບິ່ງຜູ້ໃຊ້ທີ່ທ່ານຕ້ອງການເຊີນແລ້ວລອງໃໝ່. + + ສົ່ງຄຳເຊີນໄປໃຫ້ %1$s ແລະ ອີກ %2$d ຄົນ + + ບໍ່ແມ່ນລະຫັດ QR matrix ທີ່ຖືກຕ້ອງ + ສົ່ງຄຳເຊີນໄປຫາ %1$s ແລະ %2$s + ສົ່ງຄຳເຊີນໄປໃຫ້ %1$s + 🔐️ ເຂົ້າຮ່ວມກັບຂ້ອຍໃນ ${app_name} + ສະບາຍດີ, ລົມກັບຂ້ອຍຢູ່ ${app_name}: %s + ເຊີນເພື່ອນ + ເຊີນຜູ້ໃຊ້ + ກຳລັງເຊີນຜູ້ໃຊ້… + ເຊີນ + ເພີ່ມຄົນ + ເພີ່ມສະມາຊິກ + ພວກເຮົາບໍ່ສາມາດສ້າງ DM ຂອງທ່ານໄດ້. ກະລຸນາກວດເບິ່ງຜູ້ໃຊ້ທີ່ທ່ານຕ້ອງການເຊີນແລ້ວລອງໃໝ່. + ລິ້ງທີ່ %1$s ກຳລັງພາທ່ານໄປຫາເວັບໄຊອື່ນ: %2$s. +\n +\nທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການສືບຕໍ່\? + ກວດເບິ່ງລິ້ງນີ້ຄືນໃໝ່ + homeserver ບໍ່ຍອມຮັບຊື່ຜູ້ໃຊ້ທີ່ມີພຽງແຕ່ຕົວເລກ. + ກະລຸນາເລືອກລະຫັດຜ່ານ. + ກະລຸນາເລືອກຊື່ຜູ້ໃຊ້. + ການຕັ້ງຄ່າ Cross Signing ບໍ່ສຳເລັດ + ຢືນຢັນຕົວຕົນຂອງທ່ານໂດຍການຢືນຢັນການເຂົ້າສູ່ລະບົບນີ້, ໃຫ້ສິດການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ. + ຢືນຢັນຕົວຕົນຂອງທ່ານໂດຍການກວດສອບການເຂົ້າສູ່ລະບົບນີ້ ໂດຍລະບົບອຶ່ນຂອງທ່ານ, ທີ່ໃຫ້ສິດການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ. + ຢືນຢັນແບບໂຕ້ຕອບໂດຍອີໂມຈິ + ຢືນຢັນການເຂົ້າສູ່ລະບົບ + ຢືນຢັນຂໍ້ຄວາມດ້ວຍຕົນເອງ + ຢືນຢັນການເຂົ້າສູ່ລະບົບໃໝ່ທີ່ເຂົ້າເຖິງບັນຊີຂອງທ່ານ: %1$s + ຢັ້ງຢືນທຸກລະບົບຂອງທ່ານເພື່ອໃຫ້ແນ່ໃຈວ່າບັນຊີ ແລະ ຂໍ້ຄວາມຂອງທ່ານປອດໄພ + ກວດເບິ່ງບ່ອນທີ່ທ່ານເຂົ້າສູ່ລະບົບ + ເຂົ້າລະຫັດໂດຍອຸປະກອນທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນ + ບໍ່ໄດ້ເຂົ້າລະຫັດ + ສົ່ງຫິມະຕົກ ❄️ + ສົ່ງ confetti 🎉 + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດດ້ວຍຫິມະຕົກ + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດດ້ວຍ confetti + + ສະແດງ %d ອຸປະກອນທີ່ທ່ານສາມາດກວດສອບໄດ້ໃນຕອນນີ້ + + ທ່ານຈະປິດເປີດໃໝ່ໂດຍບໍ່ມີປະຫວັດ, ບໍ່ມີຂໍ້ຄວາມ, ອຸປະກອນທີ່ເຊື່ອຖືໄດ້ ຫຼື ຜູ້ໃຊ້ທີ່ເຊື່ອຖືໄດ້ + ຖ້າຫາກວ່າທ່ານຕັ້ງຄ່າທັງຫມົດ + ເຮັດສິ່ງນີ້ກໍ່ຕໍ່ເມືອທ່ານບໍ່ມີອຸປະກອນອື່ນທີ່ທ່ານສາມາດຢືນຢັນອຸປະກອນນີ້ດ້ວຍ. + ຕັ້ງຄ່າຄືນທຸກຢ່າງ + ລືມ ຫຼື ສູນເສຍຕົວເລືອກການກູ້ຂໍ້ມູນທັງໝົດບໍ\?ຕັ້ງຄ່າຄືນທຸກຢ່າງ + ການເຂົ້າເຖິງບ່ອນຈັດເກັບຂໍ້ມູນທີ່ປອດໄພບໍ່ສຳເລັດ + ເລືອກກະແຈການກູ້ຄືນຂອງທ່ານ, ຫຼື ປ້ອນລະຫັດດ້ວຍຕົນເອງໂດຍການພິມ ຫຼື ວາງຈາກ clipboard ຂອງທ່ານ + ໃຊ້ລະຫັດການກູ້ຂໍ້ມູນ + ໃຊ້ %1$s ຂອງທ່ານ ຫຼືໃຊ້ %2$s ຂອງທ່ານເພື່ອສືບຕໍ່. + ຮອງຮັບສະເພາະຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ + ບັງຄັບໃຫ້ຍົກເລີກຈາກລະບົບກຸ່ມຂາອອກໃນປະຈຸບັນໃນຫ້ອງທີ່ເຂົ້າລະຫັດ + ໃຊ້ ${app_name} ລ່າສຸດໃນອຸປະກອນອື່ນຂອງທ່ານ: + ຫຼື ລູກຄ້າ Matrix ທີ່ສາມາດcross-signing ໄດ້ + ${app_name} iOS +\n${app_name} Android + ${app_name} Web +\n${app_name} Desktop + ໃຊ້ ${app_name} ລ່າສຸດໃນອຸປະກອນອື່ນຂອງທ່ານ, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} ສໍາລັບ Android, ຫຼື ລູກຄ້າ Matrix ອື່ນທີ່ສາມາດ cross-signing ໄດ້ + ຕັ້ງລະຫັດຜ່ານບັນຊີໃໝ່… + ບໍ່ສາມາດບັນທຶກໄຟລ໌ມີເດຍໄດ້ + ການເປີດໃຊ້ງານການຕັ້ງຄ່ານີ້ຈະເພີ່ມ FLAG_SECURE ໃສ່ກິດຈະກຳທັງໝົດ. ການຕັ້ງຄ່າຄືນໃໝ່ແອັບພລິເຄຊັນເພື່ອໃຫ້ການປ່ຽນແປງ. + ປ້ອງກັນພາບໜ້າຈໍຂອງແອັບພລິເຄຊັນ + ລະຫັດສຳຮອງການກູ້ຄືນກະແຈ + ບໍ່ຮູ້ລະຫັດສຳຮອງຂອງທ່ານ, ທ່ານສາມາດ %s ໄດ້. + ໃຊ້ລະຫັດການກູ້ຄືນ ກະເເຈ/ລະຫັດສຳຮອງຂອງທ່ານ + ໃສ່ລະຫັດການສຳຮອງຂໍ້ມູນຂອງທ່ານເພື່ອດຳເນີນການຕໍ່ໄປ. + ການເກັບຮັກສາລະຫັດລັບໃນ SSSS + ກຳລັງສ້າງລະຫັດ SSSS ຈາກລະຫັດການກູ້ຂໍ້ມູນ + ກຳລັງສ້າງລະຫັດ SSSS ຈາກປະໂຫຍດລະຫັດຜ່ານ (%s) + ກຳລັງສ້າງລະຫັດ SSSS ຈາກຂໍ້ຄວາມປະໂຫຍກລະຫັດຜ່ານ + ກຳລັງຂໍລະຫັດເສັ້ນໂຄ້ງ + ກຳລັງກວດສອບລະຫັດສຳຮອງ (%s) + ກຳລັງກວດສອບລະຫັດສຳຮອງ + ກະລຸນາໃສ່ລະຫັດການກູ້ຂໍ້ມູນ + ບໍ່ແມ່ນລະຫັດການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງ + ໃຊ້ File + ໃສ່ %s ຂອງທ່ານເພື່ອສືບຕໍ່ + ຢັ້ງຢືນຕົວທ່ານເອງ ແລະ ຄົນອື່ນເພື່ອຮັກສາການສົນທະນາຂອງທ່ານໃຫ້ປອດໄພ + ມີການຍົກລະດັບການເຂົ້າລະຫັດ + ຂໍ້ຄວາມ… + ບັນຊີນີ້ຖືກປິດການໃຊ້ງານແລ້ວ. + ຊື່ຜູ້ໃຊ້ ແລະ/ຫຼືລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ. ລະຫັດຜ່ານທີ່ປ້ອນເຂົ້າເລີ່ມຕົ້ນຫຼືລົງທ້າຍດ້ວຍການຍະຫວ່າງ, ກະລຸນາກວດສອບມັນ. + ສົ່ງຂໍ້ຄວາມເປັນຂໍ້ຄວາມທຳມະດາ, ໂດຍບໍ່ມີການຕີຄວາມໝາຍວ່າເປັນເຄື່ອງໝາຍmarkdown + ແກ້ໄຂບັນຫາ + ການຕັ້ງຄ່າການແຈ້ງເຕືອນ + ການນໍາເຂົ້າລະຫັດບໍ່ສຳເລັດ + ກຳລັງລໍຖ້າ %s… + ເກືອບຈະແລ້ວ! ກຳລັງລໍຖ້າການຢືນຢັນ… + ເກືອບຈະ! ອຸປະກອນອື່ນສະແດງຫມາຍຖືກບໍ\? + "ຫົວຂໍ້: " + ເພີ່ມຫົວຂໍ້ + %s ເພື່ອແຈ້ງໃຫ້ຄົນຮູ້ວ່າຫ້ອງນີ້ແມ່ນຫຍັງ. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງປະຫວັດຂໍ້ຄວາມໂດຍກົງຂອງທ່ານກັບ %s. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງການສົນທະນານີ້. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງ %s. + ທ່ານໄດ້ເຂົ້າຮ່ວມແລ້ວ. + %s ເຂົ້າຮ່ວມ. + ທ່ານສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ. + %s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ. + ບໍ່ຮອງຮັບການເຂົ້າລະຫັດທີ່ໃຊ້ໃນຫ້ອງນີ້ + ການເຂົ້າລະຫັດຖືກຕັ້ງຄ່າຜິດ + ບໍ່ໄດ້ເປີດໃຊ້ງານການເຂົ້າລະຫັດ + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. ສຶກສາເພີ່ມເຕີມ ແລະ ຢັ້ງຢືນຜູ້ໃຊ້ໃນໂປຣໄຟລ໌ຂອງເຂົາເຈົ້າ. + ເປີດໃຊ້ງານການເຂົ້າລະຫັດແລ້ວ + ຖ້າທ່ານຍົກເລີກຕອນນີ້, ທ່ານອາດຈະສູນເສຍຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດ ຖ້າຫາກທ່ານສູນເສຍການເຂົ້າເຖິງການເຂົ້າສູ່ລະບົບຂອງທ່ານ. +\n +\nນອກນັ້ນທ່ານຍັງສາມາດຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ & ຈັດການກະແຈຂອງທ່ານໃນການຕັ້ງຄ່າ. + + %1$dຜູ້ຄົນ + + ການແຈ້ງເຕືອນ + ການຕັ້ງຄ່າ + ການຕັ້ງຄ່າຫ້ອງ + ການດຳເນິນການຂອງຜູ້ເບິ່ງແຍງ + ເພີ່ມເຕີມ + ສຶກສາເພີ່ມເຕີມ + ການກູ້ຄືນການເຂົ້າລະຫັດ + ຄວາມປອດໄພ + ຂໍ້ຄວາມນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. +\n +\nຂໍ້ຄວາມຂອງທ່ານຖືກລັອກໄວ້ຢ່າງປອດໄພ ແລະ ມີພຽງແຕ່ທ່ານ ແລະ ຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈສະເພາະເພື່ອປົດລັອກພວກມັນ. + ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງເພື່ອກູ້ຄືນການເຂົ້າລະຫັດໃຫ້ເປັນສະຖານະທີ່ຖືກຕ້ອງ. + ການເຂົ້າລະຫັດໄດ້ກຳນົດຄ່າຜິດ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. +\n +\nຂໍ້ຄວາມຂອງທ່ານຖືກລັອກໄວ້ຢ່າງປອດໄພ ແລະ ມີພຽງແຕ່ທ່ານ ແລະ ຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈສະເພາະເພື່ອປົດລັອກພວກມັນ. + ຂໍ້ຄວາມນີ້ບໍ່ໄດ້ເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຸຖິງປາຍທາງ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ບໍ່ໄດ້ຖືກເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງ. + ກຳລັງລໍຖ້າ %s… + ຢືນຢັນແລ້ວ %s + ຢືນຢັນ %s + ກວດສອບໂດຍການປຽບທຽບ emojis + ຢັ້ງຢືນໂດຍການປຽບທຽບ emoji ແທນ + ຖ້າທ່ານບໍ່ໄດ້ຢູ່ໃນຕໍ່ໜ້າ, ໃຫ້ປຽບທຽບ emoji ແທນ + ບໍ່ສາມາດສະແກນໄດ້ + ສະແກນດ້ວຍອຸປະກອນນີ້ + ສະແກນລະຫັດຂອງພວກເຂົາ + ສະແກນລະຫັດດ້ວຍອຸປະກອນອື່ນຂອງທ່ານ ຫຼື ສະຫຼັບ ແລະສະແກນດ້ວຍອຸປະກອນນີ້ + ສະແກນລະຫັດກັບອຸປະກອນຂອງຜູ້ໃຊ້ອື່ນເພື່ອຢືນຢັນກັນຢ່າງປອດໄພ + ຢືນຢັນລະບົບນີ້ + ການຮ້ອງຂໍການຢັ້ງຢືນ + ສົ່ງການຢັ້ງຢືນແລ້ວ + ທ່ານຍອມຮັບ + %s ຍອມຮັບ + ທ່ານໄດ້ຍົກເລີກ + %s ຖືກຍົກເລີກ + ກຳລັງລໍຖ້າ… + ແບ່ງປັນສະຖານທີ່ປັດຈຸບັນຂອງເຂົາເຈົ້າ + ແບ່ງປັນສະຖານທີ່ຂອງເຂົາເຈົ້າ + ສະຫຼຸບການຢັ້ງຢືນ + ຕອບສະໜອງດ້ວຍ: %s + ການສໍາຫຼວດ + ສະຕິກເກີ + ໄຟລ໌ + ສຽງ + ສຽງ + ຮູບພາບ. + ວິດີໂອ. + "ຫນຶ່ງໃນຕໍ່ໄປນີ້ອາດຈະຖືກທໍາລາຍ: +\n +\n - homeserver ຂອງທ່ານ +\n - homeserver ທີ່ຜູ້ໃຊ້ທີ່ທ່ານກໍາລັງກວດສອບແມ່ນໄດ້ເຊື່ອມຕໍ່ກັບ +\n - ຂອງທ່ານ, ຫຼື ການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງຜູ້ໃຊ້ອື່ນໆ +\n - ຂອງທ່ານ, ຫຼື ອຸປະກອນຂອງຜູ້ໃຊ້ອື່ນ" + ບໍ່ປອດໄພ + ບໍ່ກົງກັນ + ກົງກັນ + ເຂົ້າສູ່ລະບົບທີ່ບໍ່ໜ້າເຊື່ອຖື + ໂດເມນອີເມວຂອງທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ລົງທະບຽນຢູ່ໃນເຊີບເວີນີ້ + ກຳລັງສ້າງພື້ນທີ່… + ກຳລັງສ້າງຫ້ອງ… + ບໍ່ອະນຸຍາດໃຫ້ໃຊ້ອັກສອນບາງຕົວ + ກະລຸນາລະບຸທີ່ຢູ່ຫ້ອງ + ທີ່ຢູ່ນີ້ຖືກໃຊ້ແລ້ວ + ທີ່ຢູ່ຂອງພຶ້ນທີ່ + ທ່ານອາດຈະເປີດໃຊ້ງານຖ້າຫ້ອງຈະຖືກໃຊ້ເພື່ອຮ່ວມມືກັບທີມງານພາຍໃນໃນເຊີບເວີຂອງທ່ານເທົ່ານັ້ນ. ອັນນີ້ບໍ່ສາມາດປ່ຽນແປງໄດ້ໃນພາຍຫຼັງ. + ຫ້າມຜູ້ໃດຜູ້ໜຶ່ງທີ່ບໍ່ໄດ້ເປັນສ່ວນຫນຶ່ງຂອງ %s ຈາກການເຂົ້າຮ່ວມຫ້ອງນີ້ + ເຊື່ອງຂັ້ນສູງ + ສະແດງຂັ້ນສູງ + ເມື່ອເປີດໃຊ້ແລ້ວ, ການເຂົ້າລະຫັດບໍ່ສາມາດຖືກປິດໃຊ້ງານໄດ້. + ເປີດໃຊ້ການເຂົ້າລະຫັດ + ເພິ່ມ( ͡° ͜ʖ ͡°) ຂ້າງໜ້າຂໍ້ຄວາມທຳມະດາ + ເພິ່ມ ¯\\_(ツ)_/¯ຂ້າງໜ້າຂໍ້ຄວາມທໍາມະດາ + ສະແດງຂໍ້ມູນທີ່ເປັນປະໂຫຍດເພື່ອຊ່ວຍແກ້ບັນຫາແອັບພລິເຄຊັນ + ສະແດງຂໍ້ມູນການແກ້ໃຂໃນໜ້າຈໍ + ${app_name} ອາດຈະຂັດຂ້ອງເລື້ອຍໆເມື່ອມີຄວາມຜິດພາດທີ່ບໍ່ຄາດຄິດເກີດຂຶ້ນ + ບໍ່ສຳເລັດ + ສະແດງສະເພາະຜົນໄດ້ຮັບທຳອິດ, ພິມຕົວອັກສອນເພີ່ມເຕີມ… + ລະບົບອື່ນໆ + ລະບົບປະຈຸບັນ + ການຕັ້ງຄ່າ + ກວດພົບການສັ່ນ! + ສັ່ນໂທລະສັບຂອງທ່ານເພື່ອທົດສອບເກນການກວດຫາ + ການກວດຫາ + Rageshake + ໂໝດຜູ້ພັດທະນາເປີດໃຊ້ຄຸນສົມບັດທີ່ເຊື່ອງໄວ້ ແລະ ອາດຈະເຮັດໃຫ້ແອັບພລິເຄຊັນມີຄວາມໝັ້ນຄົງໜ້ອຍລົງ. ສໍາລັບນັກພັດທະນາເທົ່ານັ້ນ! + ຮູບແບບນັກພັດທະນາ + ຕັ້ງຄ່າຂັ້ນສູງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ… + ຄຳອະທິບາຍສັ້ນເກີນໄປ + ລິ້ງ matrix.to ຂອງທ່ານບໍ່ຖືກຕ້ອງ + ລະບົບປັດຈຸບັນແມ່ນສໍາລັບຜູ້ໃຊ້ %1$s ແລະ ທ່ານໃຫ້ຂໍ້ມູນປະຈໍາຕົວສໍາລັບຜູ້ໃຊ້ %2$s. ອັນນີ້ບໍ່ຮອງຮັບໂດຍ ${app_name}. +\nກະລຸນາລຶບຂໍ້ມູນກ່ອນ, ຈາກນັ້ນເຂົ້າສູ່ລະບົບອີກຄັ້ງໃນບັນຊີອື່ນ. + ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ປອດໄພເວັ້ນເສຍແຕ່ວ່າທ່ານເຂົ້າສູ່ລະບົບເພື່ອກູ້ຄືນການເຂົ້າລະຫັດຂອງທ່ານ. + ລຶບລ້າງຂໍ້ມູນທັງໝົດທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ບໍ\? +\nເຂົ້າສູ່ລະບົບອີກຄັ້ງເພື່ອເຂົ້າເຖິງຂໍ້ມູນບັນຊີ ແລະ ຂໍ້ຄວາມຂອງທ່ານ. + ລຶບລ້າງຂໍ້ມູນ + ລຶບລ້າງຂໍ້ມູນທັງໝົດ + ຄຳເຕືອນ: ຂໍ້ມູນສ່ວນຕົວຂອງທ່ານ (ລວມທັງກະແຈການເຂົ້າລະຫັດ) ຍັງຖືກເກັບໄວ້ໃນອຸປະກອນນີ້. +\n +\nລຶບລ້າງຫາກທ່ານໃຊ້ອຸປະກອນນີ້ແລ້ວ, ຫຼືຕ້ອງການເຂົ້າສູ່ລະບົບບັນຊີອື່ນ. + ລຶບຂໍ້ມູນສ່ວນຕົວ + ລະຫັດຜ່ານ + ເຂົ້າສູ່ລະບົບ + ເຂົ້າສູ່ລະບົບເພື່ອກູ້ການເຂົ້າລະຫັດທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ເທົ່ານັ້ນ. ທ່ານຕ້ອງການໃຫ້ພວກເຂົາອ່ານຂໍ້ຄວາມທີ່ປອດໄພທັງໝົດຂອງທ່ານໃນອຸປະກອນໃດກໍໄດ້. + ຜູ້ເບິ່ງແຍງລະບົບ homeserver (%1$s) ຂອງທ່ານໄດ້ນຳທ່ານອອກຈາກລະບົບບັນຊີ %2$s (%3$s). + ເຂົ້າສູ່ລະບົບ + ທ່ານອອກຈາກລະບົບແລ້ວ + ເຂົ້າສູ່ລະບົບອີກຄັ້ງ + ອາດເກີດຈາກເຫດຜົນຕ່າງໆ: +\n +\n• ທ່ານໄດ້ປ່ຽນລະຫັດຜ່ານຂອງທ່ານໃນລະບົບອື່ນ. +\n +\n• ທ່ານໄດ້ລຶບລະບົບນີ້ອອກຈາກລະບົບອື່ນແລ້ວ. +\n +\n• ຜູ້ເບິ່ງແຍງລະບົບເຊີບເວີຂອງທ່ານໄດ້ຍົກເລີກການເຂົ້າເຖິງຂອງທ່ານດ້ວຍເຫດຜົນດ້ານຄວາມປອດໄພ. + ທ່ານອອກຈາກລະບົບແລ້ວ + ເຫັນໂດຍ + ບໍ່ສາມາດຊອກຫາ homeserver ທີ່ຖືກຕ້ອງໄດ້. ກະລຸນາກວດເບິ່ງຕົວລະບຸຂອງທ່ານ + ນີ້ບໍ່ແມ່ນຕົວລະບຸຜູ້ໃຊ້ທີ່ຖືກຕ້ອງ. ຮູບແບບທີ່ຄາດໄວ້: \'@user:homeserver.org\' + ຖ້າທ່ານບໍ່ຮູ້ລະຫັດຜ່ານຂອງທ່ານ, ໃຫ້ກັບຄືນໄປຕັ້ງຄ່າໃໝ່. + Matrix ID + ຖ້າທ່ານຕັ້ງບັນຊີຢູ່ໃນ homeserver, ໃຊ້ Matrix ID ຂອງທ່ານ (ເຊັ່ນ: @user:domain.com) ແລະລະຫັດຜ່ານຂ້າງລຸ່ມນີ້. + ເຂົ້າສູ່ລະບົບດ້ວຍ Matrix ID + ເຂົ້າສູ່ລະບົບດ້ວຍ Matrix ID + + ສົ່ງຄຳຮ້ອງຂໍຫຼາຍເກີນໄປ. ທ່ານສາມາດລອງໃໝ່ໃນອີກ %1$d ວິນາທີ… + + homeserverນີ້ກຳລັງໃຊ້ເວີຊັນເກົ່າ. ຂໍໃຫ້ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານຍົກລະດັບເວີຊັ້ນ. ທ່ານສາມາດສືບຕໍ່ໄດ້, ແຕ່ບາງຄຸນສົມບັດອາດຈະເຮັດວຽກບໍ່ຖືກຕ້ອງ. + homeserver ລ້າສະໄຫມ + ລະຫັດທີ່ໃສ່ນັ້ນບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດສອບ. + ພວກເຮົາຫາກໍສົ່ງອີເມວຫາ %1$s. +\nກະລຸນາກົດໃສ່ການເຊື່ອມຕໍ່ທີ່ມີເພື່ອສືບຕໍ່ການສ້າງບັນຊີ. + ກະລຸນາກວດເບິ່ງອີເມວຂອງທ່ານ + ຍອມຮັບເງື່ອນໄຂເພື່ອສືບຕໍ່ + ກະລຸນາດໍາເນີນການ captcha + ເລືອກ homeserver ແບບກຳນົດເອງ + ເລືອກບໍລິການ Element Matrix + ເລືອກ matrix.org + ທ່ານຍັງບໍ່ໄດ້ສ້າງບັນຊີເທື່ອ. +\n +\nຢຸດຂະບວນການລົງທະບຽນ\? + ຄຳເຕືອນ + ຊື່ຜູ້ໃຊ້ນັ້ນຖືກໃຊ້ແລ້ວ + ຕໍ່ໄປ + ລະຫັດຜ່ານ + ຊື່ຜູ້ໃຊ້ + ຊື່ຜູ້ໃຊ້ ຫຼື ອີເມລ + ລົງທະບຽນສູງສຸດ %1$s + ເບີໂທລະສັບເບິ່ງຄືວ່າບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດເບິ່ງມັນ + ບີໂທລະສັບສາກົນຕ້ອງເລີ່ມຕົ້ນດ້ວຍ \'+\' + ກະລຸນາໃຊ້ຮູບແບບສາກົນ (ເບີໂທລະສັບຕ້ອງຂຶ້ນຕົ້ນດ້ວຍ \'+\') + ຕໍ່ໄປ + ສົ່ງອີກຄັ້ງ + ໃສ່ລະຫັດ + ພວກເຮົາຫາກໍສົ່ງລະຫັດໄປໃຫ້ %1$s. ປ້ອນໃສ່ທາງລຸ່ມເພື່ອຢັ້ງຢືນວ່າມັນແມ່ນທ່ານ. + ຢືນຢັນເບີໂທລະສັບ + ຕໍ່ໄປ + ເບີໂທລະສັບ (ທາງເລືອກ) + ເບີໂທລະສັບ + ກະລຸນາໃຊ້ຮູບແບບສາກົນ. + ກຳນົດເບີໂທລະສັບເພື່ອອະນຸຍາດໃຫ້ຄົນທີ່ທ່ານຮູ້ຈັກຄົ້ນຫາທ່ານ. + ຕັ້ງເບີໂທລະສັບ + ເບິ່ງຄືວ່າທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ + ຕໍ່ໄປ + ອີເມວ (ທາງເລືອກ) + ອີເມວ + ຕັ້ງອີເມວເພື່ອກູ້ຄືນບັນຊີຂອງທ່ານ. ພາຍຫຼັງ, ທ່ານສາມາດເລືອກໃຫ້ຄົນທີ່ທ່ານຮູ້ຈັກຄົ້ນຫາທ່ານທາງອີເມວຂອງທ່ານ. + ຕັ້ງທີ່ຢູ່ອີເມວ + ລະຫັດຜ່ານຂອງທ່ານຍັງບໍ່ທັນມີການປ່ຽນແປງ. +\n +\nຢຸດຂະບວນການປ່ຽນລະຫັດຜ່ານບໍ\? + ຄຳເຕືອນ + ກັບຄືນສູ່ເຂົ້າສູ່ລະບົບ + ທ່ານໄດ້ອອກຈາກລະບົບທັງໝົດ ແລະຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນ push ອີກຕໍ່ໄປ. ເພື່ອເປີດໃຊ້ການແຈ້ງເຕືອນຄືນໃໝ່, ກະລຸນາເຂົ້າສູ່ລະບົບອີກຄັ້ງໃນແຕ່ລະອຸປະກອນ. + ລະຫັດຜ່ານຂອງທ່ານໄດ້ຖືກຕັ້ງຄ່າແລ້ວ. + ຄວາມສໍາເລັດ! + ຂ້ອຍໄດ້ຢືນຢັນທີ່ຢູ່ອີເມວຂອງຂ້ອຍແລ້ວ + ແຕະທີ່ລິ້ງເພື່ອຢືນຢັນລະຫັດຜ່ານໃໝ່ຂອງທ່ານ. ເມື່ອທ່ານໄດ້ປະຕິບັດຕາມການເຊື່ອມຕໍ່ທີ່ມີ, ໃຫ້ກົດສ່ຂ້າງລຸ່ມນີ້. + ອີເມລ໌ຢັ້ງຢືນໄດ້ຖືກສົ່ງໄປຫາ %1$s. + ກວດເບິ່ງກ່ອງຈົດໝາຍຂອງທ່ານ + ອີເມວນີ້ບໍ່ໄດ້ເຊື່ອມຕໍ່ກັບບັນຊີໃດໆ + ສືບຕໍ່ + ການປ່ຽນລະຫັດຜ່ານຂອງທ່ານຈະຕັ້ງຄ່າການເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງໃນທຸກລະບົບຂອງທ່ານ, ເຮັດໃຫ້ປະຫວັດການສົນທະນາທີ່ເຂົ້າລະຫັດໄວ້ບໍ່ສາມາດອ່ານໄດ້. ຕັ້ງຄ່າການສໍາຮອງກະແຈ ຫຼືສົ່ງອອກກະແຈຫ້ອງຂອງທ່ານຈາກລະບົບອື່ນກ່ອນທີ່ຈະຕັ້ງຄ່າລະຫັດຜ່ານຂອງທ່ານ. + ເຕືອນໄພ! + ລະຫັດຜ່ານໃໝ່ + ອີເມວ + ຕໍ່ໄປ + ອີເມວຢືນຢັນຈະຖືກສົ່ງໄປຫາກ່ອງຂໍ້ຄວາມຂອງທ່ານເພື່ອຢືນຢັນການຕັ້ງລະຫັດຜ່ານໃໝ່ຂອງທ່ານ. + ຕັ້ງລະຫັດຜ່ານໃໝ່ໃນ %1$s + ອີເມວນີ້ບໍ່ໄດ້ເຊື່ອມໂຍງກັບບັນຊີໃດໆ. + ທ່ານຖືກຈັບໄດ້ໝົດແລ້ວ! + ແອັບພລິເຄຊັນບໍ່ສາມາດສ້າງບັນຊີຢູ່ໃນ homeserver ນີ້. +\n +\nທ່ານຕ້ອງການລົງທະບຽນໂດຍໃຊ້ web client ບໍ\? + ຂໍອະໄພ, ເຊີບເວີນີ້ບໍ່ຮັບບັນຊີໃໝ່. + ແອັບພລິເຄຊັນບໍ່ສາມາດເຂົ້າສູ່ລະບົບ homeserver ນີ້ໄດ້. homeserver ຮອງຮັບປະເພດການເຂົ້າສູ່ລະບົບຕໍ່ໄປນີ້: %1$s. +\n +\nທ່ານຕ້ອງການເຂົ້າສູ່ລະບົບໂດຍໃຊ້ web client ບໍ\? + ເກີດຄວາມຜິດພາດຂຶ້ນໃນເວລາໂຫລດໜ້າ: %1$s (%2$d) + ໃສ່ທີ່ຢູ່ຂອງເຊີບເວີທີ່ທ່ານຕ້ອງການໃຊ້ + ໃສ່ທີ່ຢູ່ຂອງ Modular Element ຫຼື Server ທີ່ທ່ານຕ້ອງການໃຊ້ + ການເປັນເຈົ້າພາບເປັນທີ່ນິຍົມສໍາລັບອົງການຈັດຕັ້ງ + ທີ່ຢູ່ + ທີ່ຢູ່ບໍລິການ Element Matrix + ລ້າງປະຫວັດ + ສືບຕໍ່ກັບລະບົບປະຕູດຽວ(SSO) + ເຂົ້າສູ່ລະບົບ + ລົງທະບຽນ + ເຂົ້າສູ່ລະບົບ %1$s + ເຊື່ອມຕໍ່ກັບເຊີບເວີທີ່ກໍາຫນົດເອງ + ເຊື່ອມຕໍ່ກັບການບໍລິການ Element Matrix + ເຊື່ອມຕໍ່ຫາ %1$s + ສືບຕໍ່ + ລະບົບປະຕູດຽວ + ເຂົ້າສູ່ລະບົບດ້ວຍ %s + ເຂົ້າຮ່ວມ ພື້ນທີ່ + ສ້າງພື້ນທີ່ + ຂ້າມໄປດຽວນີ້ + ເຂົ້າຮ່ວມພື້ນທີ່ຂອງຂ້ອຍ %1$s%2$s + ເຂົາເຈົ້າຈະບໍ່ເປັນສ່ວນຫນຶ່ງຂອງ %s + ພຽງແຕ່ຫ້ອງນີ້ + ພວກເຂົາຈະສາມາດສຳຫຼວດ %s + ເຊີນເຂົ້າຮ່ວມ %s + ແບ່ງປັນລິ້ງ + ເຊີນໂດຍຊື່ຜູ້ໃຊ້ ຫຼື ທາງໄປສະນີ + ເຊີນຜ່ານອີເມລ໌ + ມີພຽງທ່ານໃນປັດຈຸບັນ. %s ຈະດີກວ່າຖ້າໃຊ້ຮ່ວມກັບຄົນອື່ນ. + ເຊີນ %s + ເຊີນຄົນ + ເຊີນຄົນເຂົ້າມາໃນພື້ນທີ່ຂອງທ່ານ + ລາຍລະອຽດ + ກຳລັງສ້າງພື້ນທີ່… + ສຸ່ມ + ທົ່ວໄປ + ໃຫ້ສ້າງຫ້ອງສໍາລັບພວກເຂົາແຕ່ລະຄົນ. ທ່ານສາມາດເພີ່ມເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ, ລວມທັງລາຍການທີ່ມີຢູ່ແລ້ວ. + ເຈົ້າກຳລັງເຮັດວຽກຫຍັງແດ່\? + ໃຫ້ແນ່ໃຈວ່າຄົນທີ່ມີສິດເຂົ້າເຖິງບໍລິສັດ %s. ທ່ານສາມາດເຊີນເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ. + ໃຜເປັນເພື່ອນຮ່ວມທີມຂອງທ່ານ\? + ພວກເຮົາຈະສ້າງຫ້ອງສໍາລັບພວກເຂົາ. ທ່ານສາມາດເພີ່ມເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ. + ການສົນທະນາໃດນຶ່ງທີ່ທ່ານຕ້ອງການມີຢູ່ໃນ %s\? + ຕັ້ງຊື່ເພື່ອສືບຕໍ່. + ເພີ່ມລາຍລະອຽດບາງຢ່າງເພື່ອຊ່ວຍໃຫ້ຜູ້ອຶ່ນລະບຸຕົວຕົນໄດ້. ທ່ານສາມາດປ່ຽນສິ່ງເຫຼົ່ານີ້ໄດ້ທຸກເວລາ. + ເພີ່ມລາຍລະອຽດບາງຢ່າງເພື່ອຊ່ວຍໃຫ້ໂດດເດັ່ນ. ທ່ານສາມາດປ່ຽນສິ່ງເຫຼົ່ານີ້ໄດ້ທຸກເວລາ. + ສ້າງພື້ນທີ່ + ເຊີນເທົ່ານັ້ນ, ທີ່ດີສຸດສຳລັບຕົວທ່ານເອງ ຫຼື ທີມງານ + ສ່ວນຕົວ + ເປີດໃຫ້ທຸກຄົນ, ທີ່ດີສຸດສຳລັບຊຸມຊົນ + ສາທາລະນະ + ພື້ນທີ່ສ່ວນຕົວສຳລັບ ທ່ານ ແລະ ເພື່ອນຮ່ວມທີມ + ຂ້ອຍ ແລະ ເພື່ອນຮ່ວມທີມ + ພື້ນທີ່ສ່ວນຕົວເພື່ອຈັດລະບຽບຫ້ອງຂອງທ່ານ + ພຽງແຕ່ຂ້ອຍ + ໃຫ້ແນ່ໃຈວ່າບຸກຄົນທີ່ມີສິດເຂົ້າເຖິງ %s. + ທ່ານກໍາລັງເຮັດວຽກກັບໃຜ\? + ເພື່ອເຂົ້າຮ່ວມພື້ນທີ່ທີ່ມີຢູ່, ທ່ານຕ້ອງໄດ້ຮັບເຊີນ. + ທ່ານສາມາດປ່ຽນສິ່ງນີ້ໃນພາຍຫຼັງ + ທ່ານຕ້ອງການສ້າງພື້ນທີ່ປະເພດໃດ\? + ພື້ນທີ່ສ່ວນຕົວຂອງທ່ານ + ພື້ນທີ່ສາທາລະນະຂອງທ່ານ + ເພີ່ມພື້ນທີ່ + ພື້ນທີ່ສ່ວນຕົວ + ພື້ນທີ່ສາທາລະນະ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ສົ່ງທັງໝົດຢູ່ໃນຫ້ອງນີ້\? + ລຶບຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ສົ່ງ + ສົ່ງຂໍ້ຄວາມບໍ່ສຳເລັດ + ທ່ານຕ້ອງການຍົກເລີກການສົ່ງຂໍ້ຄວາມບໍ\? + ລຶບຂໍ້ຄວາມທີ່ບໍ່ສຳເລັດທັງໝົດອອກ + ບໍ່ສຳເລັດ + ສົ່ງແລ້ວ + ກຳລັງສົ່ງ + ຍົກລະດັບຫ້ອງເປັນເວີຊັນໃໝ່ + ອອກຈາກຫ້ອງດ້ວຍ id (ຫຼື ຫ້ອງປະຈຸບັນຖ້າເປັນ null) + ເຂົ້າຮ່ວມພື້ນທີ່ກັບ id ທີ່ກຳນົດໃຫ້ + ເພີ່ມໃສ່ພື້ນທີ່ທີ່ກຳນົດໃຫ້ + ສ້າງພື້ນທີ່ + ເນື້ອໃນການນັດໝາຍ + ສົ່ງການນັດໝາຍຂອງລັດ! + ສົ່ງນັດໝາຍ! + ເຫດການຜິດປົກກະຕິ + ບໍ່ມີປະເພດຂໍ້ຄວາມ + ບໍ່ມີເນື້ອຫາ + ເນື້ອໃນການນັດໝາຍ + ສະຖານະຂອງກະເເຈ + ປະເພດ + ສົ່ງນັດໝາຍຂອງລັດແບບກຳນົດເອງ + ແກ້ໄຂເນື້ອຫາ + ການນັດໝາຍຂອງລັດ + ສົ່ງນັດມາຍຂອງລັດ + ສົ່ງນັດໝາຍທີ່ກຳນົດເອງ + ສຳຫຼວດສະຖານະຫ້ອງ + ເຄື່ອງມືພັດທະນາ + ບໍ່ສາມາດໃຊ້ໄດ້ + ອອຟລາຍ + ອອນລາຍ + ຫ້ອງສາທາລະນະ + ເບິ່ງໃບຕອບຮັບການອ່ານ + ບໍ່ໄດ້ແຈ້ງເຕືອນ + ແຈ້ງເຕືອນໂດຍບໍ່ມີສຽງ + ແຈ້ງເຕືອນດ້ວຍສຽງ + ບໍ່ໄດ້ສົ່ງຂໍ້ຄວາມເນື່ອງຈາກຄວາມຜິດພາດ + ບໍ່ໄດ້ເລືອກ + ກວດແລ້ວ + ເປິດຕົວເລືອກ Emoji + ເປີດຕົວເລືອກ Emoji + ລະດັບຄວາມໄວ້ວາງໃຈໄດ້ + ລະດັບຄວາມເຊື່ອໝັ້ນຂອງຄຳເຕືອນ + ລະດັບຄວາມເຊື່ອຖືໃນເບື້ອງເລີ່ມຕົ້ນ + ເລືອກແລ້ວ + ວິດີໂອ + ມີສະບັບຮ່າງທີ່ຍັງບໍ່ໄດ້ສົ່ງ + ບາງຂໍ້ຄວາມຍັງບໍ່ໄດ້ສົ່ງໄປ + ລຶບຮູບແທນຕົວ + ປ່ຽນຮູບແທນຕົວ + ຮູບພາບ + ນໍາເຂົ້າລະຫັດຈາກໄຟລ໌ + ເປີດ widget + ພາບໜ້າຈໍ + ການຢືນຢັນບໍ່ສຳເລັດ + ${app_name} ຮ້ອງຂໍໃຫ້ທ່ານໃສ່ຂໍ້ມູນປະຈໍາຕົວຂອງທ່ານເພື່ອດໍາເນີນການນີ້. + ຕ້ອງການພິສູດການຢືນຢັນຄືນໃໝ່ + ເລື່ອນເພື່ອວາງສາຍ + ບຸກຄົນທີ່ບໍ່ຮູ້ຈັກ + ໂອນໄປ %1$s + ໃຫ້ຄໍາປຶກສາກັບ %1$s + ຜູ້ໃຊ້ + ເກີດຄວາມຜິດພາດໃນລະຫວ່າງການໂອນສາຍ + ໂອນ + ເຊື່ອມຕໍ່ + ປຶກສາກ່ອນ + %1$s ແຕະເພື່ອກັບຄືນ + ການໂທເຂົ້າ (%1$s) · + + %1$d ສາຍໂທເຂົ້າ. + + ການໂທເຂົ້າ (%1$s) + ກີດຄວາມຜິດພາດໃນການຊອກຫາເບີໂທລະສັບ + ປຸ່ມກົດ + ບໍ່ຮັບສາຍ + ສາຍວິດີໂອທີ່ບໍ່ໄດ້ຮັບ + ສາຍບໍ່ໄດ້ຮັບ + ປະຕິເສດການໂທວິດີໂອ + ການໂທດ້ວຍສຽງຖືກປະຕິເສດ + ສະໝັກດ້ວຍ %s + ສືບຕໍ່ດ້ວຍ %s + ຫຼື + ການຕັ້ງຄ່າແບບກຳນົດເອງ ແລະຂັ້ນສູງ + ອື່ນໆ + ສຶກສາເພີ່ມເຕີມ + ການເປັນເຈົ້າພາບເປັນທີ່ນິຍົມສໍາລັບອົງການຈັດຕັ້ງ + ເຂົ້າຮ່ວມຫຼາຍລ້ານຄົນໄດ້ຟຣີໃນເຊີບເວີສາທາລະນະທີ່ໃຫຍ່ທີ່ສຸດ + ເຊັ່ນກຽວກັບອີເມລ໌, ບັນຊີມີອັນດຽວ, ເຖິງແມ່ນວ່າທ່ານສາມາດລົມກັບໃຜກໍຕາມ + ເລືອກເຊີບເວີ + ຂ້ອຍມີບັນຊີຢູ່ແລ້ວ + ສ້າງບັນຊີ + ເລີ່ມຕົ້ນ + ຂະຫຍາຍ ແລະ ປັບແຕ່ງປະສົບການຂອງທ່ານ + ຮັກສາການສົນທະນາສ່ວນຕົວດ້ວຍການເຂົ້າລະຫັດ + ສົນທະນາກັບຄົນໂດຍກົງຫຼືເປັນກຸ່ມ + ເປັນການສົນທະນາຂອງທ່ານ. ເປັນເຈົ້າຂອງ ຂອງມັນ. + ຂ້າມຂັ້ນຕອນນີ້ + ບັນທຶກ ແລະ ສືບຕໍ່ + ບັນທຶກຄວາມມັກຂອງທ່ານໄວ້. + ທ່ານພ້ອມແລ້ວ! + ໄປກັນເລີຍ + ທ່ານສາມາດປ່ຽນອັນນີ້ໄດ້ທຸກເວລາ. + ເພີ່ມຮູບໂປຣໄຟລ໌ + ທ່ານສາມາດປ່ຽນອັນນີ້ໃນພາຍຫຼັງ + ຊື່ສະແດງ + ສຳເນົາໃສ່ບ່ອນຈັດເກັບຂໍ້ມູນຄລາວສ່ວນຕົວຂອງທ່ານ + ບັນທຶກມັນໄວ້ໃນກະແຈ USB ຫຼື ໄດຣຟ໌ສຳຮອງ + ພິມອອກ ແລະ ເກັບຮັກສາໄວ້ບ່ອນທີ່ປອດໄພ + %2$s & %1$s ຂອງທ່ານໄດ້ຕັ້ງຄ່າແລ້ວ. +\n +\nຮັກສາຄວາມປອດໄພ! ທ່ານຈະຕ້ອງປົດລັອກຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ ແລະ ຂໍ້ມູນທີ່ປອດໄພຖ້າຫາກວ່າທ່ານສູນເສຍການເຂົ້າຮ່ວມທັງຫມົດຂອງທ່ານ. + ການຕັ້ງຄ່າສໍາຮອງຂໍ້ມູນກະແຈ + ການຊິ້ງຂໍ້ມູນລະຫັດດ້ວຍຕົນເອງ + ກຳລັງຊິ້ງລະຫັດຜູ້ໃຊ້ + ກຳລັງຊິ້ງລະຫັດຫຼັກ + ກໍານົດລະຫັດເລີ່ມຕົ້ນ SSSS + ກຳລັງສ້າງກະແຈທີ່ປອດໄພຈາກປະໂຫຍກລະຫັດຄວາມປອດໄພ + ການເຜີຍແຜ່ລະຫັດທີ່ສ້າງຂຶ້ນ + ຈົບ + ຮັກສາຄວາມປອດໄພ + ທ່ານສຳເລັດແລ້ວ! + ຕັ້ງຄ່າການກູ້ຂໍ້ມູນ. + ອັນນີ້ອາດຈະໃຊ້ເວລາຫຼາຍວິນາທີ, ກະລຸນາລໍຖ້າ. + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພທີ່ທ່ານຮູ້, ໃຊ້ເພື່ອຮັບປະກັນຄວາມລັບຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ຢ່າໃຊ້ລະຫັດຜ່ານບັນຊີຂອງທ່ານ. + ໃສ່ %s ຂອງທ່ານເພື່ອສືບຕໍ່. + ກະແຈຂໍ້ຄວາມ + ປະໂຫຍກລະຫັດຜ່ານການກູ້ຄືນ + ຍົກເລີກການຢັ້ງຢືນແລ້ວ + ການຢັ້ງຢືນໄດ້ຖືກຍົກເລີກ. ທ່ານສາມາດເລີ່ມຕົ້ນການຢັ້ງຢືນອີກຄັ້ງ. + ຫນຶ່ງໃນຕໍ່ໄປນີ້ອາດຈະຖືກໂຈມຕີ: +\n +\n- ລະຫັດຜ່ານຂອງທ່ານ +\n- homeserverຂອງທ່ານ +\n- ອຸປະກອນນີ້, ຫຼື ອຸປະກອນອື່ນໆ +\n- ການເຊື່ອມຕໍ່ອິນເຕີເນັດອຸປະກອນການນໍາໃຊ້ +\n +\nພວກເຮົາແນະນຳໃຫ້ທ່ານປ່ຽນລະຫັດຜ່ານ ແລະ ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໃນການຕັ້ງຄ່າທັນທີ. + ທ່ານຈະບໍ່ຖືກຢືນຢັນ %1$s (%2$s) ຖ້າຫາກທ່ານຍົກເລີກດຽວນີ້. ເລີ່ມຕົ້ນອີກຄັ້ງໃນໂປຣໄຟລ໌ຜູ້ໃຊ້ຂອງພວກເຂົາ. + ຖ້າຫາກທ່ານຍົກເລີກ, ທ່ານຈະບໍ່ສາມາດອ່ານຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຢູ່ໃນອຸປະກອນໃຫມ່ຂອງທ່ານໄດ້, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະບໍ່ເຊື່ອມັນ + ຖ້າຫາກທ່ານຍົກເລີກ, ທ່ານຈະບໍ່ສາມາດອ່ານຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ໃນອຸປະກອນນີ້ໄດ້, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະບໍ່ເຊື່ອຖື + ບັນຊີຂອງທ່ານອາດຈະຖືກໂຈມຕີ + ນີ້ບໍ່ແມ່ນຂ້ອຍ + ໃຊ້ລະບົບນີ້ເພື່ອຢືນຢັນໃໝ່ຂອທ່ານ, ໃຫ້ເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້. + ເຂົ້າສູ່ລະບົບໃໝ່. ນີ້ແມ່ນທ່ານບໍ\? + ໂຫຼດຫນ້າຈໍຄືນ + ປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ກວດສອບການສົ່ງອອກ + ການຮ້ອງຂໍທີ່ສຳຄັນ + ${app_name} Android + ລະຫັດແມ່ນທັນສະໄຫມແລ້ວ! + ເຫດການຖືກກວດສອບໂດຍຜູ້ເບິ່ງແຍງຫ້ອງ, ເຫດຜົນ: %1$s + ເຫດການທີ່ລຶບໂດຍຜູ້ໃຊ້, ເຫດຜົນ: %1$s + ເຫດຜົນສໍາລັບການແກ້ໄຂ + ລວມທັງເຫດຜົນ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບ (ລຶບ) ເຫດການນີ້ອອກ\? ກະລຸນາຮັບຊາບວ່າຖ້າຫາກທ່ານລຶບຊື່ຫ້ອງ ຫຼື ການປ່ຽນຫົວຂໍ້, ມັນສາມາດຍົກເລີກການປ່ຽນແປງໄດ້. + ຢືນຢັນການລຶບອອກ + ສົ່ງສື່ຕາມຂະຫນາດຕົ້ນສະບັບ + + ສົ່ງວິດີໂອຕາມຂະຫນາດຕົ້ນສະບັບ + + + ສົ່ງຮູບພາບຕາມຂະຫນາດຕົ້ນສະບັບ + + ທ່ານຕ້ອງການສົ່ງໄຟລ໌ແນບນີ້ໄປຫາ %1$s ບໍ\? + ລຶບອອກ… + ບໍ່ສາມາດຊອກຫາຄວາມລັບຢູ່ໃນບ່ອນຈັດເກັບ + ຖ້າທ່ານບໍ່ສາມາດເຂົ້າເຖິງລະບົບທີ່ມີຢູ່ແລ້ວ + ໃຊ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນ ຫຼື ໃຊ້ກະເເຈ + ລຶບຂໍ້ມູນບັນຊີປະເພດ %1$s ບໍ\? +\n +\nໃຊ້ຢ່າງລະມັດລະວັງ, ມັນອາດຈະນໍາໄປສູ່ພຶດຕິກໍາທີ່ບໍ່ຄາດຄິດ. + ເຄື່ອງມືພັດທະນາ + ເປີດໂໝດຢູ່ໃນຍົນແລ້ວ + ຂາດການເຊື່ອມຕໍ່ກັບເຊີບເວີ + ບໍ່ + ແມ່ນແລ້ວ + ເກືອບຈະແລ້ວ! %s ສະແດງເຄື່ອງໝາຍຖືກບໍ\? + ລະຫັດ QR + ຕັ້ງຄ່າກະເເຈຄືນໃໝ່ + ເລີ່ມຕົ້ນ CrossSigning + ຂໍ້ຄວາມທີ່ສົ່ງໄປຫາ ແລະ ກັບຈະຕິດເຄື່ອງໝາຍໄວ້ຈົນກວາຜູ້ໃຊ້ນີ້ຈະໄວ້ວາງໃຈ. ອີກທາງເລືອກໜຶ່ງ, ທ່ານສາມາດກວດສອບດ້ວຍຕົນເອງ. + %1$s (%2$s) ເຂົ້າສູ່ລະບົບໂດຍໃຊ້ລະບົບໃໝ່: + ລະບົບນີ້ເຊື່ອຖືໄດ້ສໍາລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ ເພາະວ່າ %1$s (%2$s) ໄດ້ຢັ້ງຢືນແລ້ວ: + ເຊື່ອຖືບໍ່ໄດ້ + ເຊື່ອຖືໄດ້ + ລະບົບ + ການຮັບລະບົບບໍ່ສຳເລັດ + ຄຳເຕືອນ + ຢັ້ງຢືນແລ້ວ + ຢືນຢັນ + ໃຊ້ລະບົບທີ່ມີຢູ່ແລ້ວເພື່ອກວດສອບ, ໃຫ້ເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດ. + ຢືນຢັນການເຂົ້າສູ່ລະບົບນີ້ + + %dລະບົບທີ່ໃຊ້ງານຢູ່ + + ຢືນຢັນລະບົບນີ້ເພື່ອກຳນົດເຄື່ອງໝາຍທີ່ເຊື່ອຖືໄດ້ ແລະໃຫ້ສິດເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້. ຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບນີ້ ບັນຊີຂອງທ່ານອາດຈະຖືກໂຈມຕີ: + ລະບົບນີ້ຖືກເຊື່ອຖືໄດ້ສຳລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ ເພາະວ່າທ່ານໄດ້ຢັ້ງຢືນແລ້ວ: + ບໍ່ມີຂໍ້ມູນການເຂົ້າລະຫັດລັບ + ບໍ່ໝັ້ນຄົງ + ໝັ້ນຄົງ + ເວີຊັນເລີ່ມຕົ້ນ + ເວີຊັ້ນຫ້ອງ 👓 + ບໍ່ຮູ້ຂອບເຂດຈໍາກັດ. + homeserver ຂອງທ່ານຍອມຮັບໄຟລ໌ແນບ (ໄຟລ໌, ມີເດຍ, ແລະ ອື່ນໆ) ທີ່ມີຂະຫນາດເຖິງ %s. + ຂີດຈຳກັດການອັບໂຫລດໄຟລ໌ເຊີບເວີ + ເວີຊັ້ນເຊີບເວີ + ຊື່ເຊີບເວີ + ອອກຈາກລະບົບນີ້ + ຈັດການ ລະບົບ + ສະແດງລະບົບທັງໝົດ + ລະບົບທີ່ໃຊ້ງານ + ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານໄດ້ປິດການນຳໃຊ້ການເຂົ້າລະຫັດແແຕ່ຕົ້ນທາງຮອດປາຍທາງໂດຍຄ່າເລີ່ມຕົ້ນໃນຫ້ອງສ່ວນຕົວ ແລະ ຂໍ້ຄວາມໂດຍກົງ. + Cross-Signing ບໍ່ໄດ້ເປີດໃຊ້ງານ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ. +\nກະເເຈຄີບໍ່ໜ້າເຊື່ອຖືໄດ້ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ +\nກະເເຈແມ່ນໜ້າເຊື່ອຖືໄດ້. +\nກະແຈສ່ວນຕົວບໍ່ຮູ້ຈັກ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ +\nກະແຈສ່ວນຕົວຢູ່ໃນອຸປະກອນ. + Cross-Signing + ຕອນນີ້ລະບົບໃໝ່ຂອງທ່ານໄດ້ຮັບການຢັ້ງຢືນແລ້ວ.ມີການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດຂອງທ່ານ, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະເຫັນວ່າເປັນທີ່ເຊື່ອຖືໄດ້. + ຂໍ້ຄວາມຜູ້ໃຊ້ນີ້ແມ່ນເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ ແລະ ບໍ່ສາມາດອ່ານໄດ້ໂດຍພາກສ່ວນທີສາມ. + ເມື່ອເປີດໃຊ້ແລ້ວ, ການເຂົ້າລະຫັດລັບຂອງຫ້ອງບໍ່ສາມາດປິດໃຊ້ງານໄດ້. ຂໍ້ຄວາມທີ່ສົ່ງຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດບໍ່ສາມາດເຫັນໄດ້ ເຊີບເວີບໍ່ສາມາດເບິ່ງເບິ່ງຂໍ້ຄວາມທີ່ສົ່ງໃນຫ້ອງທີ່ເຂົ້າລະຫັດ, ມີແຕ່ຜູ້ເຂົ້າຮ່ວມຂອງຫ້ອງເທົ່ານັ້ນ. ການເປີດໃຊ້ການເຂົ້າລະຫັດອາດຈະປ້ອງກັນບໍ່ໃຫ້ bots ແລະ bridgesຈໍານວນຫຼາຍເຮັດວຽກຢ່າງຖືກຕ້ອງ. + ປຽບທຽບລະຫັດທີ່ສະແດງຢູ່ໃນໜ້າຈໍຂອງຜູ້ໃຊ້ອື່ນ. + ປຽບທຽບ emoji ທີ່ເປັນເອກະລັກ, ໃຫ້ແນ່ໃຈວ່າພວກມັນປາກົດຢູ່ໃນລໍາດັບດຽວກັນ. + ເພື່ອຄວາມປອດໄພ, ດຳເນິນການແບບນີ້ດ້ວຍຕົນເອງ ຫຼື ໃຊ້ວິທີອື່ນເພື່ອຕິດຕໍ່ສື່ສານ. + ເພື່ອຄວາມປອດໄພ, ກະລຸນາຢັ້ງຢືນ %s ໂດຍການກວດສອບລະຫັດຄັ້ງດຽວ. + ເປີດໃຊ້ງານການເຂົ້າລະຫັດ + ເປີດໃຊ້ການເຂົ້າລະຫັດບໍ\? + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເປີດໃຊ້ການເຂົ້າລະຫັດໃນຫ້ອງນີ້. + ເປີດການເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ… + ບັນນາທິການຂໍ້ຄວາມ + ທາມລາຍ + ສົ່ງ emote ທີ່ກຳນົດເປັນສີຮຸ້ງ + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດເປັນສີຮຸ້ງ + ລະບົບນີ້ບໍ່ສາມາດແບ່ງປັນການຢືນຢັນນີ້ກັບລະບົບອື່ນຂອງທ່ານໄດ້. +\nການຢັ້ງຢືນຈະຖືກບັນທຶກໄວ້ໃນເຄື່ອງ ແລະ ແບ່ງປັນໃນເວີຊັນຂອງແອັບໃນອະນາຄົດ. + ບໍ່ສົນໃຈ + ${app_name} ປະສົບບັນຫາໃນເວລາສະແດງເນື້ອຫາຂອງເຫດການດ້ວຍ id \'%1$s\' + ${app_name} ບໍ່ໄດ້ຈັດການເຫດການຂອງປະເພດ \'%1$s\' + ໄປເພື່ອອ່ານ + ຂໍ້ຄວາມໂດຍກົງ + ກຳນົດເອງ (%1$d) ໃນ %2$s + ຄ່າເລີ່ມຕົ້ນໃນ %1$s + ຜູ້ຄວບຄຸມໃນ %1$s + ຜູ້ເບິ່ງແຍງລະບົບໃນ %1$s + ຜູ້ໃຊ້ + ເຊີນ + ກຳນົດເອງ + ຜູ້ຄວບຄຸມ + ຜູ້ເບິ່ງແຍງລະບົບ + ລົບລ້າງສີ nick + ກຳລັງອອກຈາກຫ້ອງ… + ອອກໄປ + ອອກຈາກຫ້ອງ + ອັບໂຫຼດ + ສິ່ງນີ້ຈະຖືກສະແດງເມື່ອທ່ານສົ່ງຂໍ້ຄວາມ. + ເລືອກຊື່ທີ່ຈະສະແດງ + ບັນຊີຂອງທ່ານ%sໄດ້ຖືກສ້າງຂື້ນແລ້ວ. + ຊົມເຊີຍ! + ພາຂ້ອຍກັບບ້ານ + ປັບແຕ່ງໂປຣໄຟລ໌ + ເຊື່ອມຕໍ່ກັບເຊີບເວີ + ຊອກການເຂົ້າຮ່ວມເຊີບເວີທີ່ມີຢູ່ແລ້ວບໍ\? + ຂ້າມຄໍາຖາມນີ້ + ບໍ່ແນ່ໃຈເທື່ອ\?ທາ່ນສາມາດ %s + ຊຸມຊົນ + ທີມງານ + ຫມູ່ເພື່ອນແລະຄອບຄົວ + ພວກເຮົາຈະຊ່ວຍໃຫ້ທ່ານເຊື່ອມຕໍ່ໄດ້. + ທ່ານຈະສົນທະນາກັບໃຜຫຼາຍທີ່ສຸດ\? + ${app_name} ແມ່ນດີຫຼາຍສຳລັບບ່ອນເຮັດວຽກ. ໄດ້ຮັບຄວາມໄວ້ວາງໃຈໂດຍອົງການຈັດຕັ້ງທີ່ປອດໄພທີ່ສຸດຂອງໂລກ. + ເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ ແລະ ບໍ່ຕ້ອງການເບີໂທລະສັບ. ບໍ່ມີການໂຄສະນາ ຫຼືການຂຸດຄົ້ນຂໍ້ມູນ. + ເລືອກບ່ອນທີ່ຈະເກັບການສົນທະນາຂອງທ່ານ, ໃຫ້ທ່ານຄວບຄຸມແລະເປັນເອກະລາດ. ເຊື່ອມຕໍ່ຜ່ານ Matrix. + ການສື່ສານທີ່ປອດໄພ ແລະ ເປັນເອກະລາດທີ່ເຮັດໃຫ້ທ່ານມີຄວາມເປັນສ່ວນຕົວໃນລະດັບດຽວກັນກັບການສົນທະນາແບບເຫັນໜ້າຢູ່ໃນເຮືອນຂອງທ່ານເອງ. + ຂໍ້ຄວາມສໍາລັບທີມງານຂອງທ່ານ. + ການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ. + ທ່ານຄວບຄຸມໄດ້. + ເປັນເຈົ້າຂອງບົດສົນທະນາຂອງທ່ານ. + ຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ທ່ານເປັນຜູ້ເຊີນນີ້ເທົ່ານັ້ນ. + %1$s ເຊີນນີ້ເທົ່ານັ້ນ. + ທ່ານໄດ້ເຊີນຫ້ອງເທົ່ານັ້ນ. + %1$s ກຳນົດຫ້ອງຖືກເຊີນເທົ່ານັ້ນ. + ທ່ານໄດ້ເປີດຫ້ອງສາທາລະນະໃຫ້ກັບທູຸກຄົນທີ່ຮູ້ຈັກລິ້ງ. + %1$s ໄດ້ກຳນົດຫ້ອງນີ້ໃຫ້ເປັນສາທາລະນະກັບກັບທຸກຄົນທີ່ຮູ້ຈັກລິ້ງ. + ລອງກົດໃສ່ຫ້ອງເພື່ອເບິ່ງຕົວເລືອກເພີ່ມເຕີມ + ທ່ານຍັງບໍ່ໄດ້ສົນໃຈຜູ້ໃຊ້ໃດໆ + ພິມຄໍາຫຼັກເພື່ອຊອກຫາການໂຕ້ຕອບ. + ຜູ້ເຮັດໃຫ້ເຊື່ອມເສຍ + ສົ່ງຂໍ້ຄວາມທີ່ໃຫ້ໄວ້ເປັນ spoiler + ທ່ານບໍ່ມີການປ່ຽນແປງ + %1$s ບໍ່ມີການປ່ຽນແປງ + ການຕັ້ງຄ່າຫ້ອງ + ອອກຈາກຫ້ອງ + ລຶບອອກຈາກບູລິມະສິດຕໍ່າ + ຕື່ມລຳດັບບຸລິມະສິດຕໍ່າ + ລຶບອອກຈາກລາຍການທີ່ມັກ + ເພີ່ມໃສ່ລາຍການທີ່ມັກ + ການຕັ້ງຄ່າ + ປິດສຽງ + ກ່າວເຖິງເທົ່ານັ້ນ + ຂໍ້ຄວາມທັງໝົດ + ຂໍ້ຄວາມທັງໝົດ (ສຽງດັງ) + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ເນື້ອຫານີ້ຖືກລາຍງານວ່າບໍ່ເໝາະສົມ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ລາຍງານວ່າບໍ່ເໝາະສົມ + ເນື້ອຫານີ້ຖືກລາຍງານວ່າເປັນສະແປມ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ລາຍງານວ່າເປັນ spam + ເນື້ອຫານີ້ໄດ້ຖືກລາຍງານ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ເນື້ອໃນທີ່ໄດ້ລາຍງານ + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ລາຍງານ + ເຫດຜົນໃນການລາຍງານເນື້ອຫານີ້ + ລາຍງານເນື້ອຫານີ້ + ລາຍງານທີ່ກຳນົດເອງ… + ບໍ່ເຫມາະສົມ + diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml index 60426daa42..f308a32924 100644 --- a/vector/src/main/res/values-lv/strings.xml +++ b/vector/src/main/res/values-lv/strings.xml @@ -1526,7 +1526,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Turpināt Izvēlieties zvana signālu: Ienākoša zvana signāls - Izmantot noklusējuma $ {app_name} zvana signālu ienākošajiem zvaniem + Izmantot noklusējuma ${app_name} zvana signālu ienākošajiem zvaniem Pirms zvana uzsākšanas lūgt apstiprinājumu Novērst nejaušu zvanu SSL kļūda: Peer identitāte nav pārbaudīta. @@ -2102,4 +2102,4 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lai sūtītu balss ziņojumus, piešķiriet Mikrofona atļauju. Telpas Uztver paziņojumus - \ No newline at end of file + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 80f819157e..f567323fef 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1244,7 +1244,9 @@ \nOm te voorkomen dat hij/zij opnieuw toetreedt, kun je hem/haar ook verbannen. Reden voor verwijdering Weet u zeker dat u uitnodiging voor deze persoon wilt annuleren\? - Het niet meer negeren van deze persoon zal al zijn/haar berichten opnieuw doen weergeven. + Als u deze persoon niet negeert, worden alle berichten van deze persoon opnieuw weergegeven. +\n +\nHoud er rekening mee dat deze actie de app opnieuw zal starten en dat dit enige tijd kan duren. Door deze persoon te negeren worden zijn/haar berichten verwijderd uit gesprekken die jullie delen. \n \nU kunt deze actie op elk moment ongedaan maken in de algemene instellingen. @@ -2457,4 +2459,32 @@ %d server ACL\'s veranderen %d server ACL\'s wijzigingen + Live locatie laden… + 8 uur + 1 uur + 15 minuten + Deel uw live locatie voor + (%1$s) + %1$s (%2$s) + %1$s kan niet worden afgeluisterd + Pauze %1$s + Luister %1$s + %1$d minuten %2$d seconden + %1$s, %2$s, %3$s + Hun live locatie gedeeld + ${app_name} is ook geweldig voor op de werkplek. Het wordt vertrouwd door \'s werelds veiligste organisaties. + BÈTA + Discussies zijn werk in uitvoering met nieuwe, opwindende aankomende functies, zoals verbeterde meldingen. We horen graag uw feedback! + Discussies Beta-feedback + Geef feedback + BÈTA + Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de applicatie gebruikt. + Offline modus + Aanwezigheid + Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. %sWilt u toch discussies inschakelen\? + Discussies bèta + Discussies helpen uw gesprekken on-topic te houden en gemakkelijk bij te houden. %s Als u discussies inschakelt, wordt de app vernieuwd. Bij sommige accounts kan dit langer duren. + Discussies bèta + Leer meer + Probeer het uit \ No newline at end of file diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 9c1e9a9264..f6b714bdc2 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -1523,7 +1523,9 @@ Wykop użytkownika Czy na pewno anulować zaproszenie dla tego użytkownika\? Anuluj zaproszenie - Zaprzestanie ignorowania użytkownika spowoduje ponowne wyświetlanie wszystkich jego wiadomości. + Zaprzestanie ignorowania użytkownika spowoduje ponowne wyświetlanie wszystkich jego wiadomości. +\n +\nAplikacja uruchomi się ponownie aby zsynchronizować wiadomości, może to potrwać kilka chwil. Przestań ignorować użytkownika Zignorowanie tego użytkownika usunie wszystkie jego wiadomości z pokojów, które współdzielicie. \n @@ -2550,4 +2552,17 @@ Udostępnili swoją lokalizację Powiadom cały pokój Użytkownicy + BETA + Opinia o wątkach + Przekaż opinię + BETA + Jeśli włączone, inni zobaczą twój status jako offline, nawet gdy używasz aplikacji. + Tryb offline + Statusy + Twój serwer nie obsługuje obecnie wątków, co może powodować niestabilność tej funkcji. Niektóre wiadomości w wątkach mogą nie być poprawnie widoczne. %sCzy chcesz je włączyć pomimo tego\? + Wątki (Beta) + Dzięki wątkom Twoje rozmowy są zorganizowane i łatwe do śledzenia. %sWłączenie wątków uruchomi ponownie aplikację. Może to zająć więcej czasu dla niektórych kont. + Wątki (Beta) + Dowiedz się więcej + Wypróbuj \ No newline at end of file diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index f682a123f4..21ea05a47c 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -757,7 +757,9 @@ \n \nVocê pode reverter esta ação a qualquer momento nas configurações gerais. Designorar usuária(o) - Designorar esta(e) usuária(o) vai mostrar todas as mensagens dela(e) de novo. + Designorar esta(e) usuária(o) vai mostrar todas as mensagens dela(e) de novo. +\n +\nNote que esta ação vai recomeçar o app e pode levar algum tempo. Cancelar convite Você tem certeza que você quer cancelar o convite para esta(e) usuária(o)\? Expulsar usuária(o) @@ -2110,9 +2112,9 @@ Chamada de áudio perdida %d chamadas de áudio perdidas - Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão permanecer nesta sala arquivada. - Qualquer pessoa em um espaço pai vai ser capaz de achar e se juntar a esta sala - não precisa convidar manualmente todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. - Qualquer pessoa em %s vai ser capaz de achar e se juntar a esta sala - não precisa convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. + Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão ficar nesta sala arquivada. + Qualquer pessoa em um espaço pai vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. + Qualquer pessoa em %s vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. Mensagem de Voz (%1$s) Não dá para responder ou editar enquanto mensagem de voz está ativa Não dá para gravar uma mensagem de voz @@ -2388,7 +2390,7 @@ Com quem você vai fazer chat mais\? Você já está visualizando esta thread! Visualizar Em Sala - Responder Em Thread + Responder em thread O comando \"%s\" é reconhecido mas não suportado em threads. De uma Thread Dica: Toque longo numa mensagem e use “%s”. @@ -2457,4 +2459,32 @@ Me leve para casa Personalizar perfil Desabilitar + Carregando localização ao vivo… + 8 horas + 1 hora + 15 minutos + Compartilhar sua localização ao vivo por + (%1$s) + %1$s (%2$s) + Incapaz de tocar %1$s + Pausar %1$s + Tocar %1$s + %1$d minutos %2$d segundos + %1$s, %2$s, %3$s + Compartilhou a localização ao vivo dela(e) + ${app_name} também é ótimo para o lugar de trabalho. Ele é confiado pelas organizações mais seguras do mundo. + BETA + Threads são um trabalho em progresso com novas funcionalidades próximas emocionantes, tais como notificações melhoradas. Nós adoraríamos ouvir seu feedback! + Feedback de Threads Beta + Dar Feedback + BETA + Se habilitado, você sempre vai aparecer offline para outras(os) usuárias(os), mesmo quando usando o aplicativo. + Modo offline + Presença + Seu servidorcasa não atualmente suporta threads, então esta funcionalidade pode ser inconfiável. Algumas mensagens de thread podem não estar confiavelmente disponíveis. %sVocê quer habilitar threads mesmo assim\? + Threads Beta + Threads ajudam manThreads ajudam manter suas conversas em-tópico e fáceis de rastrear. %sHabilitar threads vai refrescar o app. Isto pode tomar mais tempo para algumas contas. + Threads Beta + Saber mais + Teste aí \ No newline at end of file diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index ff1089caa0..a2c1404d2f 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -751,7 +751,9 @@ \n \nTúto akciu môžete kedykoľvek zmeniť späť vo všeobecných nastaveniach. Neignorovať používateľa - Zrušením ignorovania si opäť zobrazíte všetky správy od tohoto používateľa. + Zrušením ignorovania tohto používateľa sa opäť zobrazia všetky správy od neho. +\n +\nUpozorňujeme, že táto akcia spôsobí reštart aplikácie a môže chvíľu trvať. Zrušiť pozvanie Ste si istí, že chcete zrušiť pozvanie tohoto používateľa\? Vykázať používateľa @@ -2504,4 +2506,32 @@ Zober ma domov Prispôsobiť profil Zakázať + %1$s, %2$s, %3$s + Prítomnosť + Načítavanie polohy v reálnom čase… + Zdieľajte svoju polohu v reálnom čase na + 8 hodín + 1 hodinu + 15 minút + (%1$s) + %1$s (%2$s) + Nie je možné prehrať %1$s + Pozastaviť %1$s + Prehrať %1$s + %1$d minút %2$d sekúnd + Zdieľali svoju polohu v reálnom čase + ${app_name} je skvelý aj na pracovisku. Dôverujú mu najbezpečnejšie organizácie na svete. + BETA + Na vláknach sa neustále pracuje a pripravujú sa nové, zaujímavé funkcie, ako napríklad vylepšené oznámenia. Radi si vypočujeme vašu spätnú väzbu! + Spätná väzba na Beta vlákna + Poskytnite spätnú väzbu + BETA + Ak je táto funkcia zapnutá, budete sa ostatným používateľom vždy javiť ako offline, a to aj pri používaní aplikácie. + Režim offline + Váš domovský server v súčasnosti nepodporuje vlákna, takže táto funkcia môže byť nespoľahlivá. Niektoré správy vo vláknach nemusia byť spoľahlivo dostupné. %sChcete aj tak povoliť vlákna\? + Vlákna Beta + Vlákna pomáhajú udržiavať vaše konverzácie v téme a ľahké sledovanie. %sZapnutím vlákien sa aplikácia obnoví. V prípade niektorých účtov to môže trvať dlhšie. + Vlákna Beta + Zistiť viac + Vyskúšajte si to \ No newline at end of file diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index ddb492016c..18d2b137e1 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1528,7 +1528,9 @@ \n \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme. Hiqe shpërfilljen e përdoruesit - Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. + Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. +\n +\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe do të hajë ca kohë. Anuloje ftesën Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\? Përzëre përdoruesin @@ -2376,7 +2378,7 @@ Me kë do të bisedoni më shumë\? Po e shihni tashmë këtë rrjedhë! Shiheni në Dhomë - Përgjigjuni Te Rrjedha + Përgjigjuni te rrjedha Urdhri “%s” njihet, por nuk mbulohet në rrjedha. Nga një Rrjedhë Ndihmëz: Prekni pakëz gjatë një mesazh dhe përdorni “%s”. @@ -2445,4 +2447,30 @@ Shpjemëni në shtëpi Personalizoni profil Çaktivizoje + Përshtypje për Beta-n e Rrjedhave + Në u aktivizoftë, përdoruesve të tjerë do t’u dukeni në linjë, madje edhe kur përdoret aplikacioni. + Shërbyesi juaj Home aktualisht s’mbulon rrjedha, ndaj kjo veçori mund të jetë e paqëndrueshme. Disa mesazhe rrjedhash mund të mos jenë të përdorshëm. %sDoni të aktivizohen rrjedhat, sido qoftë\? + Beta Rrjedhash + Rrjedhat ju ndihmojnë t’i mbani bisedat tuaja brenda temës dhe ndjekjen e tyre të lehtë. %sAktivizimi i rrjedhave do të sjellë rifreskimin e aplikacionit. Për disa llogari, kjo mund të zgjasë më shumë. + Beta Rrjedhash + Rrjedhat janë në përpunim të vazhdueshëm, me veçori të reja, ngazëlluese, të ardhshme, bie fjala, njoftime. Do të donim fort të dëgjonim mendimin tuaj! + 8 orë + 1 orë + 15 minuta + Tregoni vendndodhjen tuaj drejtpërsëdrejti për + (%1$s) + %1$s (%2$s) + S’arrihet të luhet %1$s + Ndale %1$s + Luaje %1$s + %1$d minuta %2$d sekonda + %1$s, %2$s, %3$s + ${app_name} është gjithashtu i goditur për në punë. Është i besuar nga entet më të sigurta të botës. + BETA + Jepni Përshtypje + BETA + Mënyra offline + Prani + Mësoni më tepër + Provojeni \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index c6c8b45441..a7400528fa 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -837,7 +837,9 @@ Degradera Ignorera användare Avignorera användare - Att avignorera den här användaren kommer att visa alla meddelanden från hen igen. + Att avignorera den här användaren kommer att visa alla meddelanden från denne igen. +\n +\nObservera att detta kräver en omstart av appen och kan ta en stund. Avbryt inbjudan Är du säker på att du vill avbryta inbjudan för den här användaren\? Kicka användaren @@ -2421,14 +2423,14 @@ Trådar närmar sig beta 🎉 Stoppa - Kontinuerligt plats aktiverad - Om du vill dela din plats kontinuerligt så behöver ${app_name} åtkomst till din plats hela tiden när appen är i bakgrunden. + Plats i realtid aktiverad + Om du vill dela din plats i realtid så behöver ${app_name} åtkomst till din plats hela tiden när appen är i bakgrunden. \nVi kommer bara använda din plats under tiden du väljer. Tillåt åtkomst Dela den här platsen Dela den här platsen - Dela plats kontinuerligt - Dela plats kontinuerligt + Dela plats i realtid + Dela plats i realtid Dela min nuvarande plats Dela min nuvarande plats Zooma in till nuvarande plats @@ -2439,7 +2441,7 @@ \n \nDet kommer att vara en engångshändelse, eftersom trådar nu är en del av Matrixspecifikationen. Platsdelning pågår - ${app_name} Kontinuerlig plats + ${app_name} Realtidsplats Hemservern accepterar inte användarnamn med bara siffror. Hoppa över det här steget Spara och fortsätt @@ -2457,4 +2459,32 @@ För mig hem Anpassa profil Inaktivera + Laddar realtidsposition… + 8 timmar + 1 timme + 15 minuter + Dela din position i realtid under + (%1$s) + %1$s (%2$s) + Kunde inte spela %1$s + Pausa %1$s + Spela %1$s + %1$d minuter och %2$d sekunder + %1$s, %2$s, %3$s + Delade sin position i realtid + ${app_name} är även perfekt för arbetet. Flera av världens mest säkra organisationer litar på den. + BETA + Trådar är under utveckling med nya, spännande funktioner så som förbättrade aviseringar. Kom gärna med återkoppling! + Återkoppling för tråd-beta + Ge återkoppling + BETA + Med detta aktiverat så kommer du alltid se offline ut för andra, även om du är aktiv i appen. + Offline-läge + Närvaro + Din hemserver stödjer för närvarande inte trådar, så denna funktion kanske funkar dåligt. Vissa meddelanden i trådar kanske inte alltid visas. %sVill du ändå aktivera trådar\? + Tråd-beta + Trådar hjälper till att hålla dina konversationer till ämnet och gör dem lättare att följa. %sFör att aktivera trådar måste appen laddas om. Detta kan ta lång tid för vissa konton. + Trådar Beta + Läs mer + Prova \ No newline at end of file diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index ce1781cb9c..85900a392e 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -608,8 +608,8 @@ Почати відеозустріч У вас немає повноважень розпочати дзвінок У вас немає повноважень на дзвінок у цій кімнаті - У вас немає повноважень розпочати конференц-дзвінок - У вас немає повноважень розпочати конференц-дзвінок у цій кімнаті + У вас немає повноважень розпочати груповий виклик + У вас немає повноважень розпочати груповий виклик у цій кімнаті Скинути Відхилити Грати @@ -816,7 +816,7 @@ Заблокувати все Дозволити Цей віджет хоче використовувати такі ресурси: - На жаль, конференц-дзвінки з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0) + На жаль, групові виклики з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0) ID кімнати ID віджету Ваша тема @@ -1002,7 +1002,9 @@ Вилучити користувача Ви впевнені, що бажаєте скасувати запрошення для цього користувача\? Скасувати запрошення - Якщо перестати нехтувати цього користувача, усі його повідомлення стануть знову видимими. + Якщо перестати нехтувати цього користувача, усі його повідомлення стануть знову видимими. +\n +\nЗауважте, що ця дія перезапустить застосунок, а це може тривати деякий час. Рознехтувати користувача Нехтування цього користувача призведе до вилучення його повідомлень з усіх спільних кімнат. \n @@ -1142,7 +1144,7 @@ Вийти з кімнати Вийти з кімнати Вийти - Вийти з поточної конференції та перейти до іншої\? + Вийти з поточного групового виклику та перейти до іншого\? Це не загальнодоступна кімната. Ви не зможете знову приєднатися без запрошення. У вас немає дозволу на ввімкнення шифрування в цій кімнаті. Дозволи кімнати @@ -1461,7 +1463,7 @@ Використайте ключ відновлення, щоб розблокувати історію зашифрованих повідомлень використайте свій ключ відновлення Отримання резервної версії… - Перепрошуємо, під час спроби приєднатися до конференції сталася помилка + Перепрошуємо, під час спроби приєднатися до групового виклику сталася помилка Цей сервер уже є у списку Не вдалося знайти цей сервер або його список кімнат Введіть назву нового сервера, який потрібно дослідити. @@ -2549,4 +2551,32 @@ На головну Персоналізувати профіль Вимкнути + Завантаження місцеперебування наживо… + 8 годин + 1 годину + 15 хвилин + Ділитися своїм місцеперебуванням наживо + (%1$s) + %1$s (%2$s) + Неможливо відтворити %1$s + Зупинити %1$s + Відтворити %1$s + %1$d хвилин %2$d секунд + %1$s, %2$s, %3$s + Ділиться своїм місцеперебуванням наживо + ${app_name} також чудово підходить для роботи. Йому довіряють найтаємніші організації світу. + БЕТА + Треди — це поточна розробка з новими, захопливими майбутніми функціями, такими як вдосконалені сповіщення. Ми хотіли б почути ваш відгук! + Відгук про Треди бета + Надіслати відгук + БЕТА + Якщо ввімкнено, ви завжди будете видимі в режимі офлайн для інших користувачів, навіть коли користуєтеся застосунком. + Офлайн режим + Присутність + Ваш домашній сервер не підтримує тредів, тому ця функція може бути ненадійною. Деякі треди повідомлень можуть бути недоступними. %sУсе одно хочете ввімкнути треди\? + Треди бета + Треди допомагають упорядкувати свої розмови за темами та легко стежити за ними. %sУвімкнення тредів перезавантажить застосунок. Це може тривати більше часу для деяких облікових записів. + Треди бета + Докладніше + Спробувати \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index 67bf3f5e06..ec3045ec22 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -867,7 +867,9 @@ \n \n你随时可以在通用设置中反转此操作。 取消忽略用户 - 取消忽略此用户将重新显示来自他们的全部消息。 + 取消忽略此用户将重新显示来自他们的全部消息。 +\n +\n请注意:此操作将重新启动应用程序,并且可能需要一些时间。 取消邀请 你确定想要取消邀请此用户吗? 踢掉用户 @@ -2284,4 +2286,12 @@ 修改服务器 %d 的 ACLs + 子区有助于保持您的对话主题并易于跟踪 + 显示当前房间的所有子区 + 所有子区 + 筛选器 + 子区 + 在房间中筛选子区 + 复制子区的链接 + 在房间中查看 \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index ea0c677168..f93374acdb 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1501,7 +1501,9 @@ \n \n您隨時都可以在一般設定中撤銷此動作。 取消忽略使用者 - 取消忽略此使用者將再次顯示從他們而來的所有訊息。 + 取消忽略此使用者將再次顯示從他們而來的所有訊息。 +\n +\n注意,此動作將會重新啟動應用程式,且可能需要一些時間。 取消邀請 您確定您想要取消對此使用者的邀請嗎? 踢除使用者 @@ -2410,4 +2412,32 @@ 帶我回家 個人化檔案 停用 + 正在載入即時位置…… + 8小時 + 1小時 + 15分鐘 + 分享您的即時位置 + (%1$s) + %1$s (%2$s) + 無法播放 %1$s + 暫停 %1$s + 播放 %1$s + %1$d分鐘%2$d秒 + %1$s, %2$s, %3$s + 分享了他們的即時位置 + ${app_name} 也非常適合工作場所。其受到世界上最安全的組織信任。 + 測試版 + 討論串是一項正在進行中的工作,包含了新的、令人興奮的即將推出的功能,例如改進的通知。我們想要聽到您的回饋! + 討論串測試版回饋 + 給予回饋 + 測試版 + 若啟用,即使在使用應用程式時,您也會對其他使用者顯示為離線狀態。 + 離線模式 + 在場 + 您的家伺服器目前不支援討論串,所以此功能可能不可靠。部份已進入討論串的訊息可能無法可靠地使用。%s您仍想啟用討論串嗎? + 討論串測試版 + 討論串有助於讓您的對話不離題且易於追蹤。%s啟用討論串將會重新整理應用程式。對於特定帳號,可能需要更長的時間。 + 討論串測試版 + 取得更多資訊 + 試試看 \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8607e8e0be..d5bc322546 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - %s\'s invitation Your invitation diff --git a/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt index bb05357cb2..d45e6e7ce1 100644 --- a/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.utils.saveMedia import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.test.fakes.FakeClock import im.vector.app.test.fakes.FakeFile import im.vector.app.test.fakes.FakeSession import io.mockk.MockKAnnotations @@ -57,6 +58,8 @@ class DownloadMediaUseCaseTest { @MockK lateinit var notificationUtils: NotificationUtils + private val clock = FakeClock() + private val file = FakeFile() @OverrideMockKs @@ -85,7 +88,8 @@ class DownloadMediaUseCaseTest { every { getMimeTypeFromUri(appContext, uri) } returns mimeType file.givenName(name) file.givenUri(uri) - coEvery { saveMedia(any(), any(), any(), any(), any()) } just runs + clock.givenEpoch(123) + coEvery { saveMedia(any(), any(), any(), any(), any(), any()) } just runs // When val result = downloadMediaUseCase.execute(file.instance) @@ -100,7 +104,7 @@ class DownloadMediaUseCaseTest { getMimeTypeFromUri(appContext, uri) } coVerify { - saveMedia(appContext, file.instance, name, mimeType, notificationUtils) + saveMedia(appContext, file.instance, name, mimeType, notificationUtils, 123) } } @@ -113,8 +117,9 @@ class DownloadMediaUseCaseTest { val error = Throwable() file.givenName(name) file.givenUri(uri) + clock.givenEpoch(345) every { getMimeTypeFromUri(appContext, uri) } returns mimeType - coEvery { saveMedia(any(), any(), any(), any(), any()) } throws error + coEvery { saveMedia(any(), any(), any(), any(), any(), any()) } throws error // When val result = downloadMediaUseCase.execute(file.instance) @@ -129,7 +134,7 @@ class DownloadMediaUseCaseTest { getMimeTypeFromUri(appContext, uri) } coVerify { - saveMedia(appContext, file.instance, name, mimeType, notificationUtils) + saveMedia(appContext, file.instance, name, mimeType, notificationUtils, 345) } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt new file mode 100644 index 0000000000..1d531f147f --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 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. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.time.Clock +import io.mockk.every +import io.mockk.mockk + +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +}