Merge branch 'develop' into feature/db_perf_research
|
@ -1,4 +1,4 @@
|
||||||
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
|
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
|
||||||
|
|
||||||
# Core team:
|
# Core team:
|
||||||
|
|
||||||
|
@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
|
||||||
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
|
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
|
||||||
|
|
||||||
Feel free to add your name below, when you contribute to the project!
|
Feel free to add your name below, when you contribute to the project!
|
||||||
|
|
||||||
|
Name | Matrix ID | GitHub
|
||||||
|
--------|---------------------|--------------------------------------
|
||||||
|
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
|
||||||
|
|
||||||
|
|
17
CHANGES.md
|
@ -6,9 +6,24 @@ Features ✨:
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Rework sending Event management (#154)
|
- Rework sending Event management (#154)
|
||||||
|
- New room creation screen: set topic and avatar in the room creation form (#2078)
|
||||||
|
- Toggle Low priority tag (#1490)
|
||||||
|
- Add option to send with enter (#1195)
|
||||||
|
- Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440)
|
||||||
|
- Edit and remove icons are now visible on image attachment preview screen (#2294)
|
||||||
|
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
|
||||||
|
- Better visibility of text reactions in dark theme (#1118)
|
||||||
|
- Room member profile: Add action to create (or open) a DM (#2310)
|
||||||
|
- Prepare changelog for F-Droid (#2296)
|
||||||
|
- Add graphic resources for F-Droid (#812, #2220)
|
||||||
|
- Highlight text in the body of the displayed result (#2200)
|
||||||
|
- Considerably faster QR-code bitmap generation (#2331)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
-
|
- Fixed ringtone handling (#2100 & #2246)
|
||||||
|
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
|
||||||
|
- Incoming call continues to ring if call is answered on another device (#1921)
|
||||||
|
- Search Result | scroll jumps after pagination (#2238)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
|
@ -65,9 +65,8 @@ allprojects {
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
// Warnings are potential errors, so stop ignoring them
|
// Warnings are potential errors, so stop ignoring them
|
||||||
// You can override by passing `-PallWarningsAsErrors=false` in the command line
|
// You can override by passing `-PallWarningsAsErrors=false` in the command line
|
||||||
kotlinOptions.allWarningsAsErrors = project.properties['allWarningsAsErrors']?.toBoolean() ?: true
|
kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
|
1
fastlane/metadata/android/en-US/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// TODO
|
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 543 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 334 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 551 KiB |
30
fastlane/metadata/android/eo/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
Element estas nova speco de mesaĝilo kaj kunlaborilo, kiu:
|
||||||
|
|
||||||
|
1. Lasas vin regi vian komunikadon por protekti vian privatecon
|
||||||
|
2. Lasas vin komuniki kun ĉiu ajn en la reto de Matrix, kaj eĉ pliaj, per interkompreno kun aplikaĵoj kiel ekzemple Slack
|
||||||
|
3. Protektas vin de reklamoj, kolektado de datumoj, kaj muritaj ĝardenoj
|
||||||
|
4. Sekurigas vian komunikadon per tutvoja ĉifrado, kun la eblo kontroli aliajn per delegaj subskriboj
|
||||||
|
|
||||||
|
Element malsamas al aliaj mesaĝiloj kaj kunlaboriloj, ĉar ĝi estas sencentra kaj malfermitkoda.
|
||||||
|
|
||||||
|
Element lasas vin gastigi vian propran servilon, aŭ elekti servilon, kiu plaĉas al vi, por ke vi ne perdu privatecon, kaj por ke vi daŭre regu kaj posedu viajn datumojn kaj interparolojn. Ĝi donas al vi aliron al malfermita reto; por ke via interparolado ne estu limigita nur al aliaj uzantoj de Element. Kaj ĝi estas tre sekura.
|
||||||
|
|
||||||
|
Element povas fari ĉi ĉion, ĉar ĝi funkcias per Matrix – publika normo por malfermita, sencentra komunikado.
|
||||||
|
|
||||||
|
Element lasas vi elekti, kiu gastigos viajn interparolojn. Per la aplikaĵo Element, vi povas elekti diversajn specojn de gastigado:
|
||||||
|
|
||||||
|
1. Akiri senpagan konton ĉe la publika servilo matrix.org, gastigata de la programistoj de Matrix, aŭ elekti unu el miloj da publikaj serviloj, gastigataj de volontuloj
|
||||||
|
2. Memgastiĝi per via propra servilo ĉe via propra aparato
|
||||||
|
3. Registriĝi ĉe propra servilo per simpla pagaliĝo al la gastiga platformo «Element Matrix Services»
|
||||||
|
|
||||||
|
<b>Kial Element?</b>
|
||||||
|
|
||||||
|
<b>POSEDU VIAJN DATUMOJN</b>: Vi decidu, kie vi tenu viajn datumojn kaj mesaĝojn. Vi posedas kaj regas ilin, ne iu granda komerca firmao, kiu kolektas viajn datumojn aŭ donas aliron al aliuloj.
|
||||||
|
|
||||||
|
<b>MALFERMAJ MESAĜADO KAJ KUNLABORADO</b>: Vi povas babili kun ĉiu alia en la reto de Matrix, ĉu ĝi uzas Elementon aŭ alian aplikaĵon de Matrix, kaj eĉ se ĝi uzas tute alian mesaĝilon, kiel ekzemple Slack, IRC, aŭ XMPP.
|
||||||
|
|
||||||
|
<b>TRE SEKURA</b>: Vera tutvoja ĉifrado (nur la interparolantoj povas malĉifri siajn mesaĝojn), kaj delegaj subskriboj por kontroli la aparatojn de partoprenantoj.
|
||||||
|
|
||||||
|
<b>SENMANKA KOMUNIKADO</b>: Mesaĝoj, voĉvokoj kaj vidvokoj, havigado de dosieroj, ekrano, kaj multaj diversaj kunigoj, robotoj kaj fenestraĵoj. Kreu ĉambrojn, komunumojn, komuniku kaj kunlaboru.
|
||||||
|
|
||||||
|
<b>ĈIE KUN VI</b>: Tenu vin ĝisdata per historio de mesaĝoj plene spegulita trans ĉiuj viaj aparatoj, kaj sur la reto per https://app.element.io.
|
1
fastlane/metadata/android/eo/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.
|
1
fastlane/metadata/android/eo/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Element (antaŭe Riot.im)
|
30
fastlane/metadata/android/et/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
Element on uut tüüpi suhtlus- ja koostöörakendus, mis:
|
||||||
|
|
||||||
|
1. Võimaldab täielikku kontrolli privaatsuse üle
|
||||||
|
2. Võimaldab suhelda kõigiga Matrixi võrgus ja isegi väljaspool seda, olles integreeritud selliste rakendustega nagu Slack
|
||||||
|
3. Kaitseb sind reklaamide ja andmekogumise eest
|
||||||
|
4. Tagab turvalisuse läbiva krüptimise abil, kasutades risttunnustamist vestlejate tuvastamiseks
|
||||||
|
|
||||||
|
Element erineb täielikult teistest sõnumside- ja koostöörakendustest, kuna see on detsentraliseeritud ja avatud lähtekoodiga.
|
||||||
|
|
||||||
|
Element võimaldab ise hostida - või valida hosti -, et oleks tagatud privaatsus ja kontroll oma andmete ja vestluste üle. Element annab ka juurdepääsu avatud võrgule, nii et te ei pea vaid Elemendi kasutajatega rääkima. Ning kogu süsteem on väga turvaline.
|
||||||
|
|
||||||
|
Element töötab Matrixil - avatud, detsentraliseeritud suhtlus-standardil.
|
||||||
|
|
||||||
|
Võimaldades valida, kes vestlusi korraldab, annab Element annab kontrolli sinule. Rakendust Element saad kasutada mitmel viisil.
|
||||||
|
|
||||||
|
1. Tasuta konto Matrixi arendajate hostitud avalikus serveris matrix.org või vali tuhandete avalike serverite hulgast, mida haldavad vabatahtlikud
|
||||||
|
2. Hosti oma kontot ise, paigaldades serveri oma riistvarale
|
||||||
|
3. Registreeruge serveris olevale kontole, tellides Element Matrix Services teenuseplatvormi
|
||||||
|
|
||||||
|
<b> Miks valida element? </b>
|
||||||
|
|
||||||
|
<b> KONTROLL ANDMETE ÜLE</b>: otsustad ise, kus oma andmeid ja sõnumeid hoida. Need kuuluvad sulle ja sinu käes on kontroll, mitte mõne MEGAFIRMA käes, mis andmeid oma kasuks kaevandab või kolmandatele isikutele juurdepääsu annab.
|
||||||
|
|
||||||
|
<b> AVATUD SUHTLUS JA KOOSTÖÖ </b>: saad vestelda kõigi teistega Matrixi võrgus, olenemata sellest, kas nad kasutavad Elementi või mõnda muud Matrixi rakendust, ja isegi kui nad kasutavad teistsugust suhtlussüsteemi nagu Slack, IRC või XMPP.
|
||||||
|
|
||||||
|
<b> ÜLITURVALINE </b>: tõeline läbiv krüptimine (ainult vestluses osalejad saavad sõnumeid lugeda) ja risttunnustamine vestluses osalejate tuvastamiseks.
|
||||||
|
|
||||||
|
<b> KÕIK SUHTLUSVÕIMALUSED</b>: sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hulk lõiminguid, roboteid ja vidinaid. Loo tubasid, kogukondi, hoia ühendust ja saa asjad aetud.
|
||||||
|
|
||||||
|
<b> KÕIKJAL, KUS VIIBITE</b>: saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga.
|
30
fastlane/metadata/android/fa/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
المنت گونهای جدید از کارههای پیامرسانی و همکاری است که:
|
||||||
|
|
||||||
|
۱. کنترل محرمانگیتان را در دست خودتان میگذارد
|
||||||
|
۲. میگذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید
|
||||||
|
۳. از شما در برابر تبلیغات، دادهکاوری و دیوارهای پرداختی، محافظت میکند
|
||||||
|
۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان میکند
|
||||||
|
|
||||||
|
المنت به خاطر نامتمرکز و نرمافزار آزاد بودن، کاملاً با دیگر کارههای پیامرسانی و همکاری، فرق دارد.
|
||||||
|
|
||||||
|
المنت میگذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش دادهها و گفتوگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکهای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید.
|
||||||
|
|
||||||
|
المنت میتواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفتوگوی باز و نامتمرکز عمل میکند.
|
||||||
|
|
||||||
|
المنت با اجازه برای گزینش کسی که گفتوگوهایتان را میزبانی میکند، کنترل را به شما میدهد. با کارهٔ المنت، میتوانید برگزینید که به روشهای مختلفی میزبانی شوید:
|
||||||
|
|
||||||
|
۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعهدهندگان ماتریکس میزبانی میشود، یا گرینش از میان هزاران کارساز عمومی میزبانیشده به دست داوطلبان
|
||||||
|
۲. خودمیزبانی حسابتان با اجرای کراسازی روی سختافزار خودتان
|
||||||
|
۳. ثبتنام برای حسابی روی یک کارساز سفارشی با اشتراک در بنیازهٔ میزبانی خدمات ماتریکس المنت
|
||||||
|
|
||||||
|
<b>چرا المنت را برگزینیم؟</b>
|
||||||
|
|
||||||
|
<b>مالک دادههایتان باشید</b>: خوتان تصمیم میگیرید که دادهها و پیامهایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان میکنید، نه شرکتهای بزرگی که دادههایتان را کاویده و به شرکتهای دیگر دسترسی میدهند.
|
||||||
|
|
||||||
|
<b>پیامرسانی و همکاری باز</b>: میتوانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیامرسانی متفاوتی مثل اسلک، آیآرسی یا جبر استفاده کنند.
|
||||||
|
|
||||||
|
<b>فوق امن</b>: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفتوگو هستند،میتوانند پیامها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزارههای شرکتکنندگان در گفتوگو.
|
||||||
|
|
||||||
|
<b>ارتباط کامل</b>: پیامرسانی، تماسهای صوتی و تصویری،همرسانی پرونده، همرسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید.
|
||||||
|
|
||||||
|
<b>هرجا که هستید</b>: هر کجا که هستید، با همگام سازی کامل تاریخچهٔ پیامها بین همهٔ افزارههایتان و وب روی https://app.element.io در دسترس باشید.
|
1
fastlane/metadata/android/fa/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
گپ و تماس نامتمرکز امن. دادههایتان را از شرکتها امن نگه دارید.
|
1
fastlane/metadata/android/fa/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
المنت (ریوت سابق)
|
1
fastlane/metadata/android/fr/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité.
|
|
@ -21,7 +21,7 @@ Element sätter dig i kontroll genom att låta dig välja att vara värd för di
|
||||||
|
|
||||||
<b>ÄG DIN DATA</b>: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter.
|
<b>ÄG DIN DATA</b>: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter.
|
||||||
|
|
||||||
<b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
|
<b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
|
||||||
|
|
||||||
<b>SUPERSÄKER</b>: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter.
|
<b>SUPERSÄKER</b>: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter.
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
|
||||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||||
room.updateAvatar(avatarUri, fileName, it)
|
room.updateAvatar(avatarUri, fileName, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteAvatar(): Completable = completableBuilder<Unit> {
|
||||||
|
room.deleteAvatar(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.rx(): RxRoom {
|
fun Room.rx(): RxRoom {
|
||||||
|
|
|
@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
|
||||||
fun syncSession(session: Session) {
|
fun syncSession(session: Session) {
|
||||||
val lock = CountDownLatch(1)
|
val lock = CountDownLatch(1)
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.Main) { session.open() }
|
val job = GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
session.open()
|
||||||
|
}
|
||||||
|
runBlocking { job.join() }
|
||||||
|
|
||||||
session.startSync(true)
|
session.startSync(true)
|
||||||
|
|
||||||
|
@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform a method with a MatrixCallback to a synchronous method
|
// Transform a method with a MatrixCallback to a synchronous method
|
||||||
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
|
inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
|
||||||
val lock = CountDownLatch(1)
|
val lock = CountDownLatch(1)
|
||||||
var result: T? = null
|
var result: T? = null
|
||||||
|
|
||||||
|
@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
|
||||||
|
|
||||||
block.invoke(callback)
|
block.invoke(callback)
|
||||||
|
|
||||||
await(lock)
|
await(lock, timeout)
|
||||||
|
|
||||||
assertNotNull(result)
|
assertNotNull(result)
|
||||||
return result!!
|
return result!!
|
||||||
|
@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
|
||||||
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
|
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
|
||||||
|
|
||||||
fun signOutAndClose(session: Session) {
|
fun signOutAndClose(session: Session) {
|
||||||
doSync<Unit> { session.signOut(true, it) }
|
doSync<Unit>(60_000) { session.signOut(true, it) }
|
||||||
session.close()
|
// no need signout will close
|
||||||
|
// session.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||||
|
@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
val lock = CountDownLatch(1)
|
|
||||||
|
|
||||||
val bobEventsListener = object : Timeline.Listener {
|
|
||||||
override fun onTimelineFailure(throwable: Throwable) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
|
||||||
val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE }
|
|
||||||
.groupBy { it.root.senderId!! }
|
|
||||||
|
|
||||||
// Alice has sent 2 messages and Bob has sent 3 messages
|
|
||||||
if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) {
|
|
||||||
lock.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
|
|
||||||
bobTimeline.start()
|
|
||||||
bobTimeline.addListener(bobEventsListener)
|
|
||||||
|
|
||||||
// Alice sends a message
|
// Alice sends a message
|
||||||
roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
|
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
|
||||||
|
// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
|
||||||
|
|
||||||
// Bob send 3 messages
|
// Bob send 3 messages
|
||||||
roomFromBobPOV.sendTextMessage(messagesFromBob[0])
|
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
|
||||||
roomFromBobPOV.sendTextMessage(messagesFromBob[1])
|
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
|
||||||
roomFromBobPOV.sendTextMessage(messagesFromBob[2])
|
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
|
||||||
|
|
||||||
// Alice sends a message
|
// Alice sends a message
|
||||||
roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
|
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
|
||||||
|
|
||||||
mTestHelper.await(lock)
|
|
||||||
|
|
||||||
bobTimeline.removeListener(bobEventsListener)
|
|
||||||
bobTimeline.dispose()
|
|
||||||
|
|
||||||
return cryptoTestData
|
return cryptoTestData
|
||||||
}
|
}
|
||||||
|
@ -285,14 +251,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
|
|
||||||
fun createDM(alice: Session, bob: Session): String {
|
fun createDM(alice: Session, bob: Session): String {
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
alice.createRoom(
|
alice.createDirectRoom(bob.myUserId, it)
|
||||||
CreateRoomParams().apply {
|
|
||||||
invitedUserIds.add(bob.myUserId)
|
|
||||||
setDirectMessage()
|
|
||||||
enableEncryptionIfInvitedUsersSupportIt = true
|
|
||||||
},
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mTestHelper.waitWithLatch { latch ->
|
mTestHelper.waitWithLatch { latch ->
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
interface CryptoService {
|
interface CryptoService {
|
||||||
|
|
||||||
|
@ -142,10 +144,13 @@ interface CryptoService {
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||||
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||||
|
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
|
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||||
|
|
||||||
fun getGossipingEventsTrail(): List<Event>
|
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||||
|
fun getGossipingEvents(): List<Event>
|
||||||
|
|
||||||
// For testing shared session
|
// For testing shared session
|
||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||||
|
|
|
@ -35,6 +35,22 @@ interface RoomService {
|
||||||
fun createRoom(createRoomParams: CreateRoomParams,
|
fun createRoom(createRoomParams: CreateRoomParams,
|
||||||
callback: MatrixCallback<String>): Cancelable
|
callback: MatrixCallback<String>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
|
||||||
|
*/
|
||||||
|
fun createDirectRoom(otherUserId: String,
|
||||||
|
callback: MatrixCallback<String>): Cancelable {
|
||||||
|
return createRoom(
|
||||||
|
CreateRoomParams()
|
||||||
|
.apply {
|
||||||
|
invitedUserIds.add(otherUserId)
|
||||||
|
setDirectMessage()
|
||||||
|
enableEncryptionIfInvitedUsersSupportIt = true
|
||||||
|
},
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a room by id
|
* Join a room by id
|
||||||
* @param roomIdOrAlias the roomId or the room alias of the room to join
|
* @param roomIdOrAlias the roomId or the room alias of the room to join
|
||||||
|
@ -113,5 +129,16 @@ interface RoomService {
|
||||||
*/
|
*/
|
||||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||||
|
|
||||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
/**
|
||||||
|
* Return the roomId of an existing DM with the other user, or null if such room does not exist
|
||||||
|
* A room is a DM if:
|
||||||
|
* - it is listed in the `m.direct` account data
|
||||||
|
* - the current user has joined the room
|
||||||
|
* - the other user is invited or has joined the room
|
||||||
|
* - it has exactly 2 members
|
||||||
|
* Note:
|
||||||
|
* - the returning room can be encrypted or not
|
||||||
|
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
|
||||||
|
*/
|
||||||
|
fun getExistingDirectRoomWithUser(otherUserId: String): String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,13 @@ data class RoomSummary constructor(
|
||||||
val hasNewMessages: Boolean
|
val hasNewMessages: Boolean
|
||||||
get() = notificationCount != 0
|
get() = notificationCount != 0
|
||||||
|
|
||||||
|
val isLowPriority: Boolean
|
||||||
|
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
|
||||||
|
|
||||||
val isFavorite: Boolean
|
val isFavorite: Boolean
|
||||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
|
||||||
|
|
||||||
|
fun hasTag(tag: String) = tags.any { it.name == tag }
|
||||||
|
|
||||||
val canStartCall: Boolean
|
val canStartCall: Boolean
|
||||||
get() = joinedMembersCount == 2
|
get() = joinedMembersCount == 2
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.create
|
package org.matrix.android.sdk.api.session.room.model.create
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
|
@ -51,6 +52,11 @@ class CreateRoomParams {
|
||||||
*/
|
*/
|
||||||
var topic: String? = null
|
var topic: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is not null, the image uri will be sent to the media server and will be set as a room avatar.
|
||||||
|
*/
|
||||||
|
var avatarUri: Uri? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of user IDs to invite to the room.
|
* A list of user IDs to invite to the room.
|
||||||
* This will tell the server to invite everyone in the list to the newly created room.
|
* This will tell the server to invite everyone in the list to the newly created room.
|
||||||
|
|
|
@ -58,6 +58,11 @@ interface StateService {
|
||||||
*/
|
*/
|
||||||
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the avatar of the room
|
||||||
|
*/
|
||||||
|
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.crypto
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.query.whereType
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||||
|
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The crypto module needs some information regarding rooms that are stored
|
||||||
|
* in the session DB, this class encapsulate this functionality
|
||||||
|
*/
|
||||||
|
internal class CryptoSessionInfoProvider @Inject constructor(
|
||||||
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isRoomEncrypted(roomId: String): Boolean {
|
||||||
|
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||||
|
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||||
|
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||||
|
.isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
return encryptionEvent != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param allActive if true return joined as well as invited, if false, only joined
|
||||||
|
*/
|
||||||
|
fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> {
|
||||||
|
var userIds: List<String> = emptyList()
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
userIds = if (allActive) {
|
||||||
|
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||||
|
} else {
|
||||||
|
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userIds
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,10 @@
|
||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||||
|
@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
|
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||||
|
@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.query.whereType
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
|
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.TaskThread
|
import org.matrix.android.sdk.internal.task.TaskThread
|
||||||
|
@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith
|
||||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.jvm.Throws
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
private val setDeviceNameTask: SetDeviceNameTask,
|
private val setDeviceNameTask: SetDeviceNameTask,
|
||||||
private val uploadKeysTask: UploadKeysTask,
|
private val uploadKeysTask: UploadKeysTask,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val eventDecryptor: EventDecryptor
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val messageEncrypter: MessageEncrypter
|
|
||||||
) : CryptoService {
|
) : CryptoService {
|
||||||
|
|
||||||
init {
|
|
||||||
verificationService.cryptoService = this
|
|
||||||
}
|
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
private val isStarting = AtomicBoolean(false)
|
private val isStarting = AtomicBoolean(false)
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
// The date of the last time we forced establishment
|
|
||||||
// of a new session for each user:device.
|
|
||||||
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
|
||||||
|
|
||||||
fun onStateEvent(roomId: String, event: Event) {
|
fun onStateEvent(roomId: String, event: Event) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||||
|
@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gossipingBuffer = mutableListOf<Event>()
|
||||||
|
|
||||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||||
setDeviceNameTask
|
setDeviceNameTask
|
||||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||||
|
@ -410,7 +389,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||||
|
incomingGossipingRequestManager.close()
|
||||||
olmDevice.release()
|
olmDevice.release()
|
||||||
cryptoStore.close()
|
cryptoStore.close()
|
||||||
}
|
}
|
||||||
|
@ -452,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tryOrNull {
|
||||||
|
gossipingBuffer.toList().let {
|
||||||
|
cryptoStore.saveGossipingEvents(it)
|
||||||
|
}
|
||||||
|
gossipingBuffer.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,13 +598,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
|
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
*/
|
*/
|
||||||
override fun isRoomEncrypted(roomId: String): Boolean {
|
override fun isRoomEncrypted(roomId: String): Boolean {
|
||||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
|
||||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
|
||||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
|
||||||
.isNotNull(EventEntityFields.STATE_KEY)
|
|
||||||
.findFirst()
|
|
||||||
}
|
|
||||||
return encryptionEvent != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -660,11 +640,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
eventType: String,
|
eventType: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||||
|
// moved to crypto scope to have uptodate values
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
// if (!isStarted()) {
|
|
||||||
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
|
|
||||||
// internalStart(false)
|
|
||||||
// }
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
val userIds = getRoomUserIds(roomId)
|
||||||
var alg = roomEncryptorsStore.get(roomId)
|
var alg = roomEncryptorsStore.get(roomId)
|
||||||
if (alg == null) {
|
if (alg == null) {
|
||||||
|
@ -720,14 +697,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param callback the callback to return data or null
|
* @param callback the callback to return data or null
|
||||||
*/
|
*/
|
||||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||||
cryptoCoroutineScope.launch {
|
eventDecryptor.decryptEventAsync(event, timeline, callback)
|
||||||
val result = runCatching {
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
internalDecryptEvent(event, timeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.foldToCallback(callback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -739,42 +709,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
val eventContent = event.content
|
return eventDecryptor.decryptEvent(event, timeline)
|
||||||
if (eventContent == null) {
|
|
||||||
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
|
||||||
} else {
|
|
||||||
val algorithm = eventContent["algorithm"]?.toString()
|
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
|
||||||
if (alg == null) {
|
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
|
||||||
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return alg.decryptEvent(event, timeline)
|
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
|
||||||
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
|
||||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
|
||||||
if (mxCryptoError is MXCryptoError.Base
|
|
||||||
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
|
||||||
// need to find sending device
|
|
||||||
val olmContent = event.content.toModel<OlmEventContent>()
|
|
||||||
cryptoStore.getUserDevices(event.senderId ?: "")
|
|
||||||
?.values
|
|
||||||
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
|
||||||
?.let {
|
|
||||||
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw mxCryptoError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -796,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||||
cryptoStore.saveGossipingEvent(event)
|
gossipingBuffer.add(event)
|
||||||
// Keys are imported directly, not waiting for end of sync
|
// Keys are imported directly, not waiting for end of sync
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
}
|
}
|
||||||
EventType.REQUEST_SECRET,
|
EventType.REQUEST_SECRET,
|
||||||
EventType.ROOM_KEY_REQUEST -> {
|
EventType.ROOM_KEY_REQUEST -> {
|
||||||
// save audit trail
|
// save audit trail
|
||||||
cryptoStore.saveGossipingEvent(event)
|
gossipingBuffer.add(event)
|
||||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||||
}
|
}
|
||||||
EventType.SEND_SECRET -> {
|
EventType.SEND_SECRET -> {
|
||||||
cryptoStore.saveGossipingEvent(event)
|
gossipingBuffer.add(event)
|
||||||
onSecretSendReceived(event)
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
EventType.ROOM_KEY_WITHHELD -> {
|
EventType.ROOM_KEY_WITHHELD -> {
|
||||||
|
@ -828,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun onRoomKeyEvent(event: Event) {
|
private fun onRoomKeyEvent(event: Event) {
|
||||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||||
Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||||
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
|
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
|
||||||
return
|
return
|
||||||
|
@ -935,19 +870,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRoomUserIds(roomId: String): List<String> {
|
private fun getRoomUserIds(roomId: String): List<String> {
|
||||||
var userIds: List<String> = emptyList()
|
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||||
monarchy.doWithRealm { realm ->
|
&& shouldEncryptForInvitedMembers(roomId)
|
||||||
// Check whether the event content must be encrypted for the invited members.
|
return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
|
||||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
|
||||||
&& shouldEncryptForInvitedMembers(roomId)
|
|
||||||
|
|
||||||
userIds = if (encryptForInvitedMembers) {
|
|
||||||
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
|
||||||
} else {
|
|
||||||
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userIds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1257,38 +1182,38 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||||
val deviceKey = deviceInfo.identityKey()
|
// val deviceKey = deviceInfo.identityKey()
|
||||||
|
//
|
||||||
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||||
val now = System.currentTimeMillis()
|
// val now = System.currentTimeMillis()
|
||||||
if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||||
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||||
|
//
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||||
|
//
|
||||||
// Now send a blank message on that session so the other side knows about it.
|
// // Now send a blank message on that session so the other side knows about it.
|
||||||
// (The keyshare request is sent in the clear so that won't do)
|
// // (The keyshare request is sent in the clear so that won't do)
|
||||||
// We send this first such that, as long as the toDevice messages arrive in the
|
// // We send this first such that, as long as the toDevice messages arrive in the
|
||||||
// same order we sent them, the other end will get this first, set up the new session,
|
// // same order we sent them, the other end will get this first, set up the new session,
|
||||||
// then get the keyshare request and send the key over this new session (because it
|
// // then get the keyshare request and send the key over this new session (because it
|
||||||
// is the session it has most recently received a message on).
|
// // is the session it has most recently received a message on).
|
||||||
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
// val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||||
|
//
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
// sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the list of unknown devices
|
* Provides the list of unknown devices
|
||||||
|
@ -1339,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||||
|
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||||
|
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||||
return cryptoStore.getIncomingRoomKeyRequests()
|
return cryptoStore.getIncomingRoomKeyRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEventsTrail(): List<Event> {
|
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||||
return cryptoStore.getGossipingEventsTrail()
|
return cryptoStore.getGossipingEventsTrail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEvents(): List<Event> {
|
||||||
|
return cryptoStore.getGossipingEvents()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update devices trust for these users
|
// Update devices trust for these users
|
||||||
dispatchDeviceChange(downloadUsers)
|
// dispatchDeviceChange(downloadUsers)
|
||||||
|
|
||||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.crypto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
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.events.model.toModel
|
||||||
|
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||||
|
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||||
|
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.MatrixCoroutineDispatchers
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class EventDecryptor @Inject constructor(
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
|
private val messageEncrypter: MessageEncrypter,
|
||||||
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
|
private val cryptoStore: IMXCryptoStore
|
||||||
|
) {
|
||||||
|
|
||||||
|
// The date of the last time we forced establishment
|
||||||
|
// of a new session for each user:device.
|
||||||
|
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an event
|
||||||
|
*
|
||||||
|
* @param event the raw event.
|
||||||
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||||
|
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||||
|
*/
|
||||||
|
@Throws(MXCryptoError::class)
|
||||||
|
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
|
return internalDecryptEvent(event, timeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an event asynchronously
|
||||||
|
*
|
||||||
|
* @param event the raw event.
|
||||||
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||||
|
* @param callback the callback to return data or null
|
||||||
|
*/
|
||||||
|
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||||
|
// is it needed to do that on the crypto scope??
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
runCatching {
|
||||||
|
internalDecryptEvent(event, timeline)
|
||||||
|
}.foldToCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an event
|
||||||
|
*
|
||||||
|
* @param event the raw event.
|
||||||
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||||
|
* @return the MXEventDecryptionResult data, or null in case of error
|
||||||
|
*/
|
||||||
|
@Throws(MXCryptoError::class)
|
||||||
|
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
|
val eventContent = event.content
|
||||||
|
if (eventContent == null) {
|
||||||
|
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
||||||
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||||
|
} else {
|
||||||
|
val algorithm = eventContent["algorithm"]?.toString()
|
||||||
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||||
|
if (alg == null) {
|
||||||
|
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||||
|
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
||||||
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return alg.decryptEvent(event, timeline)
|
||||||
|
} catch (mxCryptoError: MXCryptoError) {
|
||||||
|
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||||
|
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||||
|
if (mxCryptoError is MXCryptoError.Base
|
||||||
|
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||||
|
// need to find sending device
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
val olmContent = event.content.toModel<OlmEventContent>()
|
||||||
|
cryptoStore.getUserDevices(event.senderId ?: "")
|
||||||
|
?.values
|
||||||
|
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
||||||
|
?.let {
|
||||||
|
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
||||||
|
}
|
||||||
|
?: run {
|
||||||
|
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw mxCryptoError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coroutineDispatchers.crypto scope
|
||||||
|
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||||
|
val deviceKey = deviceInfo.identityKey()
|
||||||
|
|
||||||
|
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||||
|
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||||
|
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||||
|
|
||||||
|
// offload this from crypto thread (?)
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||||
|
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||||
|
|
||||||
|
// Now send a blank message on that session so the other side knows about it.
|
||||||
|
// (The keyshare request is sent in the clear so that won't do)
|
||||||
|
// We send this first such that, as long as the toDevice messages arrive in the
|
||||||
|
// same order we sent them, the other end will get this first, set up the new session,
|
||||||
|
// then get the keyshare request and send the key over this new session (because it
|
||||||
|
// is the session it has most recently received a message on).
|
||||||
|
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||||
|
|
||||||
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
|
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||||
|
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.crypto
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to cache and batch store operations on inbound group session store.
|
||||||
|
* Because it is used in the decrypt flow, that can be called quite rapidly
|
||||||
|
*/
|
||||||
|
internal class InboundGroupSessionStore @Inject constructor(
|
||||||
|
private val store: IMXCryptoStore,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||||
|
|
||||||
|
private data class CacheKey(
|
||||||
|
val sessionId: String,
|
||||||
|
val senderKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
|
||||||
|
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
|
||||||
|
if (evicted && oldValue != null) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
|
||||||
|
store.storeInboundGroupSessions(listOf(oldValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val timer = Timer()
|
||||||
|
private var timerTask: TimerTask? = null
|
||||||
|
|
||||||
|
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
||||||
|
synchronized(sessionCache) {
|
||||||
|
val known = sessionCache[CacheKey(sessionId, senderKey)]
|
||||||
|
Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
|
||||||
|
return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
|
||||||
|
Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
|
||||||
|
sessionCache.put(CacheKey(sessionId, senderKey), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) {
|
||||||
|
Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
|
||||||
|
// We want to batch this a bit for performances
|
||||||
|
dirtySession.add(wrapper)
|
||||||
|
|
||||||
|
timerTask?.cancel()
|
||||||
|
timerTask = object : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
batchSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.schedule(timerTask!!, 2_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun batchSave() {
|
||||||
|
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
||||||
|
dirtySession.clear()
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
|
||||||
|
tryOrNull {
|
||||||
|
store.storeInboundGroupSessions(toSave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope) {
|
private val cryptoCoroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||||
// we received in the current sync.
|
// we received in the current sync.
|
||||||
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
||||||
|
@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
executor.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
// Recently verified devices (map of deviceId and timestamp)
|
// Recently verified devices (map of deviceId and timestamp)
|
||||||
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
||||||
|
|
||||||
|
@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
fun onGossipingRequestEvent(event: Event) {
|
fun onGossipingRequestEvent(event: Event) {
|
||||||
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
||||||
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
||||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
// val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
when (roomKeyShare?.action) {
|
when (roomKeyShare?.action) {
|
||||||
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
||||||
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||||
|
@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
// ignore, it was sent by me as *
|
// ignore, it was sent by me as *
|
||||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
} else {
|
} else {
|
||||||
// save in DB
|
// // save in DB
|
||||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
receivedGossipingRequests.add(it)
|
receivedGossipingRequests.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
// ignore, it was sent by me as *
|
// ignore, it was sent by me as *
|
||||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||||
} else {
|
} else {
|
||||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||||
receivedGossipingRequests.add(it)
|
receivedGossipingRequests.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
fun processReceivedGossipingRequests() {
|
fun processReceivedGossipingRequests() {
|
||||||
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
||||||
receivedGossipingRequests.clear()
|
receivedGossipingRequests.clear()
|
||||||
for (request in roomKeyRequestsToProcess) {
|
|
||||||
if (request is IncomingRoomKeyRequest) {
|
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
|
||||||
processIncomingRoomKeyRequest(request)
|
|
||||||
} else if (request is IncomingSecretShareRequest) {
|
|
||||||
processIncomingSecretShareRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
|
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
|
||||||
|
|
||||||
|
@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedRequestCancellations?.forEach { request ->
|
executor.execute {
|
||||||
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
|
||||||
// we should probably only notify the app of cancellations we told it
|
for (request in roomKeyRequestsToProcess) {
|
||||||
// about, but we don't currently have a record of that, so we just pass
|
if (request is IncomingRoomKeyRequest) {
|
||||||
// everything through.
|
processIncomingRoomKeyRequest(request)
|
||||||
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
} else if (request is IncomingSecretShareRequest) {
|
||||||
// ignore remote echo
|
processIncomingSecretShareRequest(request)
|
||||||
return@forEach
|
}
|
||||||
}
|
}
|
||||||
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
|
||||||
if (matchingIncoming == null) {
|
receivedRequestCancellations?.forEach { request ->
|
||||||
// ignore that?
|
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||||
return@forEach
|
// we should probably only notify the app of cancellations we told it
|
||||||
} else {
|
// about, but we don't currently have a record of that, so we just pass
|
||||||
// If it was accepted from this device, keep the information, do not mark as cancelled
|
// everything through.
|
||||||
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
||||||
onRoomKeyRequestCancellation(request)
|
// ignore remote echo
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
return@forEach
|
||||||
|
}
|
||||||
|
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
||||||
|
if (matchingIncoming == null) {
|
||||||
|
// ignore that?
|
||||||
|
return@forEach
|
||||||
|
} else {
|
||||||
|
// If it was accepted from this device, keep the information, do not mark as cancelled
|
||||||
|
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
||||||
|
onRoomKeyRequestCancellation(request)
|
||||||
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* The store where crypto data is saved.
|
* The store where crypto data is saved.
|
||||||
*/
|
*/
|
||||||
private val store: IMXCryptoStore) {
|
private val store: IMXCryptoStore,
|
||||||
|
private val inboundGroupSessionStore: InboundGroupSessionStore
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the Curve25519 key for the account.
|
* @return the Curve25519 key for the account.
|
||||||
|
@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
timelineSet.add(messageIndexKey)
|
timelineSet.add(messageIndexKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
store.storeInboundGroupSessions(listOf(session))
|
inboundGroupSessionStore.storeInBoundGroupSession(session)
|
||||||
val payload = try {
|
val payload = try {
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||||
|
@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = store.getInboundGroupSession(sessionId, senderKey)
|
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
||||||
|
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Check that the room id matches the original one for the session. This stops
|
// Check that the room id matches the original one for the session. This stops
|
||||||
|
|
|
@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
* @param requestBody requestBody
|
* @param requestBody requestBody
|
||||||
*/
|
*/
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||||
cancelRoomKeyRequest(requestBody, false)
|
cancelRoomKeyRequest(requestBody, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
* @param requestBody requestBody
|
* @param requestBody requestBody
|
||||||
*/
|
*/
|
||||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||||
cancelRoomKeyRequest(requestBody, true)
|
cancelRoomKeyRequest(requestBody, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,13 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
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.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
import org.matrix.android.sdk.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : IMXEncrypting {
|
) : IMXEncrypting {
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||||
|
@ -84,15 +90,18 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
||||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
// offload to computation thread
|
||||||
devices.forEach { userId, deviceId, withheldCode ->
|
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
||||||
|
devices.forEach { userId, deviceId, withheldCode ->
|
||||||
|
this.add(UserDevice(userId, deviceId) to withheldCode)
|
||||||
|
}
|
||||||
|
}.groupBy(
|
||||||
|
{ it.second },
|
||||||
|
{ it.first }
|
||||||
|
).forEach { (code, targets) ->
|
||||||
|
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
||||||
}
|
}
|
||||||
}.groupBy(
|
|
||||||
{ it.second },
|
|
||||||
{ it.first }
|
|
||||||
).forEach { (code, targets) ->
|
|
||||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,6 +256,15 @@ internal class MXMegolmEncryption(
|
||||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||||
for ((deviceId) in devicesToShareWith) {
|
for ((deviceId) in devicesToShareWith) {
|
||||||
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||||
|
cryptoStore.saveGossipingEvent(Event(
|
||||||
|
type = EventType.ROOM_KEY,
|
||||||
|
senderId = credentials.userId,
|
||||||
|
content = submap.apply {
|
||||||
|
this["session_key"] = ""
|
||||||
|
// we add a fake key for trail
|
||||||
|
this["_dest"] = "$userId|$deviceId"
|
||||||
|
}
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
|
@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
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.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
|
@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
private val taskExecutor: TaskExecutor) {
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
fun create(roomId: String): MXMegolmEncryption {
|
||||||
return MXMegolmEncryption(
|
return MXMegolmEncryption(
|
||||||
|
@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
sendToDeviceTask,
|
sendToDeviceTask,
|
||||||
messageEncrypter,
|
messageEncrypter,
|
||||||
warnOnUnknownDevicesRepository,
|
warnOnUnknownDevicesRepository,
|
||||||
taskExecutor
|
taskExecutor,
|
||||||
|
coroutineDispatchers,
|
||||||
|
cryptoCoroutineScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.withoutPrefix
|
import org.matrix.android.sdk.internal.util.withoutPrefix
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionId
|
||||||
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
|
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||||
import org.matrix.olm.OlmPkSigning
|
import org.matrix.olm.OlmPkSigning
|
||||||
import org.matrix.olm.OlmUtility
|
import org.matrix.olm.OlmUtility
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class DefaultCrossSigningService @Inject constructor(
|
internal class DefaultCrossSigningService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
|
@SessionId private val sessionId: String,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
||||||
|
@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||||
|
|
||||||
private var olmUtility: OlmUtility? = null
|
private var olmUtility: OlmUtility? = null
|
||||||
|
|
||||||
|
@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// First let's get my user key
|
// First let's get my user key
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||||
|
|
||||||
|
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
||||||
|
|
||||||
|
return UserTrustResult.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
||||||
val myUserKey = myCrossSigningInfo?.userKey()
|
val myUserKey = myCrossSigningInfo?.userKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||||
|
|
||||||
|
@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's get the other user master key
|
// Let's get the other user master key
|
||||||
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
|
val otherMasterKey = otherInfo?.masterKey()
|
||||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
|
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
||||||
|
|
||||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
||||||
?.get(userId) // Signatures made by me
|
?.get(userId) // Signatures made by me
|
||||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
||||||
|
|
||||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
||||||
Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
|
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
|
||||||
return UserTrustResult.KeyNotSigned(otherMasterKey)
|
return UserTrustResult.KeyNotSigned(otherMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||||
|
|
||||||
|
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
|
||||||
|
// Special case when it's me,
|
||||||
|
// I have to check that MSK -> USK -> SSK
|
||||||
|
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
||||||
|
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||||
|
|
||||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
val myMasterKey = myCrossSigningInfo?.masterKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||||
|
|
||||||
|
@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// Maybe it's signed by a locally trusted device?
|
// Maybe it's signed by a locally trusted device?
|
||||||
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
|
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
|
||||||
val potentialDeviceId = key.withoutPrefix("ed25519:")
|
val potentialDeviceId = key.withoutPrefix("ed25519:")
|
||||||
val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
|
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
||||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
if (potentialDevice != null && potentialDevice.isVerified) {
|
||||||
// Check signature validity?
|
// Check signature validity?
|
||||||
try {
|
try {
|
||||||
|
@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||||
checkSelfTrust()
|
checkSelfTrust()
|
||||||
|
// re-verify all trusts
|
||||||
|
onUsersDeviceUpdate(listOf(userId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult {
|
||||||
|
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
|
||||||
|
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
||||||
|
|
||||||
|
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
||||||
|
|
||||||
|
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
|
||||||
|
|
||||||
|
// TODO should we force verification ?
|
||||||
|
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
||||||
|
|
||||||
|
// Check if the trust chain is valid
|
||||||
|
/*
|
||||||
|
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
||||||
|
* ┃ ALICE ┃ ┃ BOB ┃
|
||||||
|
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
||||||
|
* MSK ┌────────────▶MSK
|
||||||
|
* │
|
||||||
|
* │ │ │
|
||||||
|
* │ SSK │ └──▶ SSK ──────────────────┐
|
||||||
|
* │ │ │
|
||||||
|
* │ │ USK │
|
||||||
|
* └──▶ USK ────────────┘ (not visible by │
|
||||||
|
* Alice) │
|
||||||
|
* ▼
|
||||||
|
* ┌──────────────┐
|
||||||
|
* │ BOB's Device │
|
||||||
|
* └──────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
||||||
|
?: return legacyFallbackTrust(
|
||||||
|
locallyTrusted,
|
||||||
|
DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey()
|
||||||
|
?.unpaddedBase64PublicKey
|
||||||
|
?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check bob's device is signed by bob's SSK
|
||||||
|
try {
|
||||||
|
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
||||||
|
}
|
||||||
|
|
||||||
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
||||||
return if (locallyTrusted == true) {
|
return if (locallyTrusted == true) {
|
||||||
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
||||||
|
@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
|
||||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
|
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
|
||||||
userIds.forEach { otherUserId ->
|
val workerData = WorkerParamsFactory.toData(workerParams)
|
||||||
checkUserTrust(otherUserId).let {
|
|
||||||
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
|
||||||
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now check device trust
|
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
.setInputData(workerData)
|
||||||
userIds.forEach { otherUserId ->
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||||
// TODO if my keys have changes, i should recheck all devices of all users?
|
.build()
|
||||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
|
||||||
devices?.forEach { device ->
|
|
||||||
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
|
||||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
|
||||||
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (otherUserId == userId) {
|
workManagerProvider.workManager
|
||||||
// It's me, i should check if a newly trusted device is signing my master key
|
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||||
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
|
.enqueue()
|
||||||
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.post(CryptoToSessionUserTrustChange(userIds))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 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.crypto.crosssigning
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
|
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.util.createBackgroundHandler
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.greenrobot.eventbus.Subscribe
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class ShieldTrustUpdater @Inject constructor(
|
|
||||||
private val eventBus: EventBus,
|
|
||||||
private val computeTrustTask: ComputeTrustTask,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
|
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater
|
|
||||||
) : SessionLifecycleObserver {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
|
|
||||||
private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val backgroundSessionRealm = AtomicReference<Realm>()
|
|
||||||
|
|
||||||
private val isStarted = AtomicBoolean()
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
|
||||||
eventBus.register(this)
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
if (isStarted.compareAndSet(true, false)) {
|
|
||||||
eventBus.unregister(this)
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
backgroundSessionRealm.getAndSet(null).also {
|
|
||||||
it?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
|
|
||||||
if (!isStarted.get()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
|
||||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
|
|
||||||
// We need to send that back to session base
|
|
||||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
|
||||||
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
|
|
||||||
if (!isStarted.get()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onCryptoDevicesChange(update.userIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCryptoDevicesChange(users: List<String>) {
|
|
||||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
|
||||||
val realm = backgroundSessionRealm.get() ?: return@launch
|
|
||||||
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
|
|
||||||
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
|
|
||||||
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
|
||||||
.findAll()
|
|
||||||
.map { it.roomId }
|
|
||||||
|
|
||||||
distinctRoomIds.forEach { roomId ->
|
|
||||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
|
||||||
if (roomSummary?.isEncrypted.orFalse()) {
|
|
||||||
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
|
||||||
try {
|
|
||||||
val updatedTrust = computeTrustTask.execute(
|
|
||||||
ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
|
|
||||||
)
|
|
||||||
realm.executeTransaction {
|
|
||||||
roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.crypto.crosssigning
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||||
|
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||||
|
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class UpdateTrustWorker(context: Context,
|
||||||
|
params: WorkerParameters)
|
||||||
|
: SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, Params::class.java) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
override val sessionId: String,
|
||||||
|
override val lastFailureMessage: String? = null,
|
||||||
|
val updatedUserIds: List<String>
|
||||||
|
) : SessionWorkerParams
|
||||||
|
|
||||||
|
@Inject lateinit var crossSigningService: DefaultCrossSigningService
|
||||||
|
|
||||||
|
// It breaks the crypto store contract, but we need to batch things :/
|
||||||
|
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
|
||||||
|
@UserId @Inject lateinit var myUserId: String
|
||||||
|
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
|
||||||
|
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
|
||||||
|
|
||||||
|
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
|
||||||
|
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||||
|
|
||||||
|
override fun injectWith(injector: SessionComponent) {
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doSafeWork(params: Params): Result {
|
||||||
|
var userList = params.updatedUserIds
|
||||||
|
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
||||||
|
// or a new device?) So we check all again :/
|
||||||
|
|
||||||
|
Timber.d("## CrossSigning - Updating trust for $userList")
|
||||||
|
|
||||||
|
// First we check that the users MSK are trusted by mine
|
||||||
|
// After that we check the trust chain for each devices of each users
|
||||||
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
|
realm.executeTransaction {
|
||||||
|
// By mapping here to model, this object is not live
|
||||||
|
// I should update it if needed
|
||||||
|
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||||
|
|
||||||
|
var myTrustResult: UserTrustResult? = null
|
||||||
|
|
||||||
|
if (userList.contains(myUserId)) {
|
||||||
|
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
|
||||||
|
// i am in the list.. but i don't know exactly the delta of change :/
|
||||||
|
// If it's my cross signing keys we should refresh all trust
|
||||||
|
// do it anyway ?
|
||||||
|
userList = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.findAll().mapNotNull { it.userId }
|
||||||
|
Timber.d("## CrossSigning - Updating trust for all $userList")
|
||||||
|
|
||||||
|
// check right now my keys and mark it as trusted as other trust depends on it
|
||||||
|
val myDevices = realm.where<UserEntity>()
|
||||||
|
.equalTo(UserEntityFields.USER_ID, myUserId)
|
||||||
|
.findFirst()
|
||||||
|
?.devices
|
||||||
|
?.map { deviceInfo ->
|
||||||
|
CryptoMapper.mapToModel(deviceInfo)
|
||||||
|
}
|
||||||
|
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
|
||||||
|
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
|
||||||
|
// update model reference
|
||||||
|
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val otherInfos = userList.map {
|
||||||
|
it to realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
val trusts = otherInfos.map { infoEntry ->
|
||||||
|
infoEntry.key to when (infoEntry.key) {
|
||||||
|
myUserId -> myTrustResult
|
||||||
|
else -> {
|
||||||
|
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
|
||||||
|
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
|
||||||
|
// i have all the new trusts, update DB
|
||||||
|
trusts.forEach {
|
||||||
|
val verified = it.value?.isVerified() == true
|
||||||
|
updateCrossSigningKeysTrust(realm, it.key, verified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok so now we have to check device trust for all these users..
|
||||||
|
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
|
||||||
|
trusts.keys.forEach {
|
||||||
|
val devicesEntities = realm.where<UserEntity>()
|
||||||
|
.equalTo(UserEntityFields.USER_ID, it)
|
||||||
|
.findFirst()
|
||||||
|
?.devices
|
||||||
|
|
||||||
|
val trustMap = devicesEntities?.map { device ->
|
||||||
|
// get up to date from DB has could have been updated
|
||||||
|
val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||||
|
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
|
||||||
|
}?.toMap()
|
||||||
|
|
||||||
|
// Update trust if needed
|
||||||
|
devicesEntities?.forEach { device ->
|
||||||
|
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
||||||
|
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
||||||
|
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
||||||
|
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
||||||
|
// need to save
|
||||||
|
val trustEntity = device.trustLevelEntity
|
||||||
|
if (trustEntity == null) {
|
||||||
|
realm.createObject(TrustLevelEntity::class.java).let {
|
||||||
|
it.locallyVerified = false
|
||||||
|
it.crossSignedVerified = crossSignedVerified
|
||||||
|
device.trustLevelEntity = it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trustEntity.crossSignedVerified = crossSignedVerified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// So Cross Signing keys trust is updated, device trust is updated
|
||||||
|
// We can now update room shields? in the session DB?
|
||||||
|
|
||||||
|
Timber.d("## CrossSigning - Updating shields for impacted rooms...")
|
||||||
|
Realm.getInstance(sessionRealmConfiguration).use { it ->
|
||||||
|
it.executeTransaction { realm ->
|
||||||
|
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
|
||||||
|
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
|
||||||
|
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
||||||
|
.findAll()
|
||||||
|
.map { it.roomId }
|
||||||
|
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
|
||||||
|
distinctRoomIds.forEach { roomId ->
|
||||||
|
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
|
if (roomSummary?.isEncrypted == true) {
|
||||||
|
Timber.d("## CrossSigning - Check shield state for room $roomId")
|
||||||
|
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||||
|
try {
|
||||||
|
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
|
||||||
|
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
||||||
|
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
|
||||||
|
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
|
||||||
|
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||||
|
.findFirst()
|
||||||
|
xInfoEntity?.crossSigningKeys?.forEach { info ->
|
||||||
|
// optimization to avoid trigger updates when there is no change..
|
||||||
|
if (info.trustLevelEntity?.isVerified() != verified) {
|
||||||
|
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
||||||
|
val level = info.trustLevelEntity
|
||||||
|
if (level == null) {
|
||||||
|
val newLevel = realm.createObject(TrustLevelEntity::class.java)
|
||||||
|
newLevel.locallyVerified = verified
|
||||||
|
newLevel.crossSignedVerified = verified
|
||||||
|
info.trustLevelEntity = newLevel
|
||||||
|
} else {
|
||||||
|
level.locallyVerified = verified
|
||||||
|
level.crossSignedVerified = verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
||||||
|
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
|
||||||
|
// The set of “all users” depends on the type of room:
|
||||||
|
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||||
|
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||||
|
val listToCheck = if (roomSummaryEntity.isDirect) {
|
||||||
|
activeMemberUserIds.filter { it != myUserId }
|
||||||
|
} else {
|
||||||
|
activeMemberUserIds
|
||||||
|
}
|
||||||
|
|
||||||
|
val allTrustedUserIds = listToCheck
|
||||||
|
.filter { userId ->
|
||||||
|
Realm.getInstance(realmConfiguration).use {
|
||||||
|
it.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val myCrossKeys = Realm.getInstance(realmConfiguration).use {
|
||||||
|
it.where(CrossSigningInfoEntity::class.java)
|
||||||
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||||
|
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (allTrustedUserIds.isEmpty()) {
|
||||||
|
RoomEncryptionTrustLevel.Default
|
||||||
|
} else {
|
||||||
|
// If one of the verified user as an untrusted device -> warning
|
||||||
|
// If all devices of all verified users are trusted -> green
|
||||||
|
// else -> black
|
||||||
|
allTrustedUserIds
|
||||||
|
.mapNotNull { uid ->
|
||||||
|
Realm.getInstance(realmConfiguration).use {
|
||||||
|
it.where<UserEntity>()
|
||||||
|
.equalTo(UserEntityFields.USER_ID, uid)
|
||||||
|
.findFirst()
|
||||||
|
?.devices
|
||||||
|
?.map {
|
||||||
|
CryptoMapper.mapToModel(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatten()
|
||||||
|
.let { allDevices ->
|
||||||
|
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
|
||||||
|
if (myCrossKeys != null) {
|
||||||
|
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
||||||
|
} else {
|
||||||
|
// Legacy method
|
||||||
|
allDevices.any { !it.isVerified }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { hasWarning ->
|
||||||
|
if (hasWarning) {
|
||||||
|
RoomEncryptionTrustLevel.Warning
|
||||||
|
} else {
|
||||||
|
if (listToCheck.size == allTrustedUserIds.size) {
|
||||||
|
// all users are trusted and all devices are verified
|
||||||
|
RoomEncryptionTrustLevel.Trusted
|
||||||
|
} else {
|
||||||
|
RoomEncryptionTrustLevel.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
|
||||||
|
val userId = xsignInfo.userId ?: ""
|
||||||
|
return MXCrossSigningInfo(
|
||||||
|
userId = userId,
|
||||||
|
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||||
|
crossSigningKeysMapper.map(userId, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildErrorParams(params: Params, message: String): Params {
|
||||||
|
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||||
|
}
|
||||||
|
}
|
|
@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
this.callback = object : MatrixCallback<KeysVersion> {
|
this.callback = object : MatrixCallback<KeysVersion> {
|
||||||
override fun onSuccess(data: KeysVersion) {
|
override fun onSuccess(data: KeysVersion) {
|
||||||
// Reset backup markers.
|
// Reset backup markers.
|
||||||
cryptoStore.resetBackupMarkers()
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
// move tx out of UI thread
|
||||||
|
cryptoStore.resetBackupMarkers()
|
||||||
|
}
|
||||||
|
|
||||||
val keyBackupVersion = KeysVersionResult(
|
val keyBackupVersion = KeysVersionResult(
|
||||||
algorithm = createKeysBackupVersionBody.algorithm,
|
algorithm = createKeysBackupVersionBody.algorithm,
|
||||||
|
@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
val importResult = awaitCallback<ImportRoomKeysResult> {
|
val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
||||||
}
|
}
|
||||||
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
withContext(coroutineDispatchers.crypto) {
|
||||||
|
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||||
|
}
|
||||||
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
|
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
|
||||||
} else {
|
} else {
|
||||||
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
||||||
|
@ -685,7 +690,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
// Get backed up keys from the homeserver
|
// Get backed up keys from the homeserver
|
||||||
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
|
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
|
||||||
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.computation) {
|
||||||
val sessionsData = ArrayList<MegolmSessionData>()
|
val sessionsData = ArrayList<MegolmSessionData>()
|
||||||
// Restore that data
|
// Restore that data
|
||||||
var sessionsFromHsCount = 0
|
var sessionsFromHsCount = 0
|
||||||
|
@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
|
|
||||||
if (retrievedMegolmBackupAuthData != null) {
|
if (retrievedMegolmBackupAuthData != null) {
|
||||||
keysBackupVersion = keysVersionResult
|
keysBackupVersion = keysVersionResult
|
||||||
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
|
||||||
|
}
|
||||||
|
|
||||||
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.store
|
package org.matrix.android.sdk.internal.crypto.store
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
@ -126,7 +127,10 @@ internal interface IMXCryptoStore {
|
||||||
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
|
|
||||||
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
||||||
|
|
||||||
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
||||||
|
|
||||||
|
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
|
||||||
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -364,6 +368,7 @@ internal interface IMXCryptoStore {
|
||||||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||||
|
|
||||||
fun saveGossipingEvent(event: Event)
|
fun saveGossipingEvent(event: Event)
|
||||||
|
fun saveGossipingEvents(events: List<Event>)
|
||||||
|
|
||||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
||||||
updateGossipingRequestState(
|
updateGossipingRequestState(
|
||||||
|
@ -441,10 +446,13 @@ internal interface IMXCryptoStore {
|
||||||
// Dev tools
|
// Dev tools
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||||
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
fun getGossipingEventsTrail(): List<Event>
|
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||||
|
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||||
|
fun getGossipingEvents(): List<Event>
|
||||||
|
|
||||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||||
fun getDeviceKeysUploaded(): Boolean
|
fun getDeviceKeysUploaded(): Boolean
|
||||||
|
|
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.paging.LivePagedListBuilder
|
||||||
|
import androidx.paging.PagedList
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
|
@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEventsTrail(): List<Event> {
|
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||||
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
|
realm.where<IncomingGossipingRequestEntity>()
|
||||||
|
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
|
||||||
|
}
|
||||||
|
val dataSourceFactory = realmDataSourceFactory.map {
|
||||||
|
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||||
|
?: IncomingRoomKeyRequest(
|
||||||
|
requestBody = null,
|
||||||
|
deviceId = "",
|
||||||
|
userId = "",
|
||||||
|
requestId = "",
|
||||||
|
state = GossipingRequestState.NONE,
|
||||||
|
localCreationTimestamp = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||||
|
LivePagedListBuilder(dataSourceFactory,
|
||||||
|
PagedList.Config.Builder()
|
||||||
|
.setPageSize(20)
|
||||||
|
.setEnablePlaceholders(false)
|
||||||
|
.setPrefetchDistance(1)
|
||||||
|
.build())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||||
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
|
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||||
|
}
|
||||||
|
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
|
||||||
|
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||||
|
LivePagedListBuilder(dataSourceFactory,
|
||||||
|
PagedList.Config.Builder()
|
||||||
|
.setPageSize(20)
|
||||||
|
.setEnablePlaceholders(false)
|
||||||
|
.setPrefetchDistance(1)
|
||||||
|
.build())
|
||||||
|
)
|
||||||
|
return trail
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEvents(): List<Event> {
|
||||||
return monarchy.fetchAllCopiedSync { realm ->
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
realm.where<GossipingEventEntity>()
|
realm.where<GossipingEventEntity>()
|
||||||
}.map {
|
}.map {
|
||||||
|
@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveGossipingEvent(event: Event) {
|
override fun saveGossipingEvents(events: List<Event>) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
monarchy.writeAsync { realm ->
|
||||||
val entity = GossipingEventEntity(
|
events.forEach { event ->
|
||||||
type = event.type,
|
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||||
sender = event.senderId,
|
val entity = GossipingEventEntity(
|
||||||
ageLocalTs = ageLocalTs,
|
type = event.type,
|
||||||
content = ContentMapper.map(event.content)
|
sender = event.senderId,
|
||||||
).apply {
|
ageLocalTs = ageLocalTs,
|
||||||
sendState = SendState.SYNCED
|
content = ContentMapper.map(event.content)
|
||||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
).apply {
|
||||||
decryptionErrorCode = event.mCryptoError?.name
|
sendState = SendState.SYNCED
|
||||||
}
|
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
decryptionErrorCode = event.mCryptoError?.name
|
||||||
realm.insertOrUpdate(entity)
|
}
|
||||||
|
realm.insertOrUpdate(entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun saveGossipingEvent(event: Event) {
|
||||||
|
monarchy.writeAsync { realm ->
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||||
|
val entity = GossipingEventEntity(
|
||||||
|
type = event.type,
|
||||||
|
sender = event.senderId,
|
||||||
|
ageLocalTs = ageLocalTs,
|
||||||
|
content = ContentMapper.map(event.content)
|
||||||
|
).apply {
|
||||||
|
sendState = SendState.SYNCED
|
||||||
|
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||||
|
decryptionErrorCode = event.mCryptoError?.name
|
||||||
|
}
|
||||||
|
realm.insertOrUpdate(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||||
|
@ -1284,6 +1349,28 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
|
||||||
|
doRealmTransactionAsync(realmConfiguration) { realm ->
|
||||||
|
requests.forEach { request ->
|
||||||
|
// After a clear cache, we might have a
|
||||||
|
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
|
||||||
|
it.otherDeviceId = request.deviceId
|
||||||
|
it.otherUserId = request.userId
|
||||||
|
it.requestId = request.requestId ?: ""
|
||||||
|
it.requestState = GossipingRequestState.PENDING
|
||||||
|
it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis()
|
||||||
|
if (request is IncomingSecretShareRequest) {
|
||||||
|
it.type = GossipRequestType.SECRET
|
||||||
|
it.requestedInfoStr = request.secretName
|
||||||
|
} else if (request is IncomingRoomKeyRequest) {
|
||||||
|
it.type = GossipRequestType.KEY
|
||||||
|
it.requestedInfoStr = request.requestBody?.toJson()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
|
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
|
||||||
// return doRealmQueryAndCopyList(realmConfiguration) {
|
// return doRealmQueryAndCopyList(realmConfiguration) {
|
||||||
// it.where<GossipingEventEntity>()
|
// it.where<GossipingEventEntity>()
|
||||||
|
@ -1417,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||||
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
|
realm
|
||||||
|
.where(OutgoingGossipingRequestEntity::class.java)
|
||||||
|
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||||
|
}
|
||||||
|
val dataSourceFactory = realmDataSourceFactory.map {
|
||||||
|
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||||
|
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
||||||
|
}
|
||||||
|
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||||
|
LivePagedListBuilder(dataSourceFactory,
|
||||||
|
PagedList.Config.Builder()
|
||||||
|
.setPageSize(20)
|
||||||
|
.setEnablePlaceholders(false)
|
||||||
|
.setPrefetchDistance(1)
|
||||||
|
.build())
|
||||||
|
)
|
||||||
|
return trail
|
||||||
|
}
|
||||||
|
|
||||||
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||||
return doWithRealm(realmConfiguration) { realm ->
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
|
||||||
import org.matrix.android.sdk.internal.di.SerializeNulls
|
import org.matrix.android.sdk.internal.di.SerializeNulls
|
||||||
import io.realm.DynamicRealm
|
import io.realm.DynamicRealm
|
||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
|
import io.realm.RealmObjectSchema
|
||||||
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
|
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
|
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
|
||||||
|
if (!hasField(fieldName)) {
|
||||||
|
addField(fieldName, fieldType)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
|
||||||
|
if (hasField(fieldName)) {
|
||||||
|
removeField(fieldName)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
|
||||||
|
if (isRequired != isRequired(fieldName)) {
|
||||||
|
setRequired(fieldName, isRequired)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||||
|
|
||||||
|
@ -89,13 +111,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
||||||
|
|
||||||
realm.schema.get("IncomingRoomKeyRequestEntity")
|
realm.schema.get("IncomingRoomKeyRequestEntity")
|
||||||
?.addField("requestBodyAlgorithm", String::class.java)
|
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
|
||||||
?.addField("requestBodyRoomId", String::class.java)
|
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
|
||||||
?.addField("requestBodySenderKey", String::class.java)
|
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
|
||||||
?.addField("requestBodySessionId", String::class.java)
|
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
|
||||||
?.transform { dynamicObject ->
|
?.transform { dynamicObject ->
|
||||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
|
||||||
try {
|
try {
|
||||||
|
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||||
// It was a map before
|
// It was a map before
|
||||||
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
||||||
|
|
||||||
|
@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
Timber.e(e, "Error")
|
Timber.e(e, "Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?.removeField("requestBodyString")
|
?.removeFieldIfExists("requestBodyString")
|
||||||
|
|
||||||
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
||||||
|
|
||||||
realm.schema.get("OutgoingRoomKeyRequestEntity")
|
realm.schema.get("OutgoingRoomKeyRequestEntity")
|
||||||
?.addField("requestBodyAlgorithm", String::class.java)
|
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
|
||||||
?.addField("requestBodyRoomId", String::class.java)
|
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
|
||||||
?.addField("requestBodySenderKey", String::class.java)
|
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
|
||||||
?.addField("requestBodySessionId", String::class.java)
|
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
|
||||||
?.transform { dynamicObject ->
|
?.transform { dynamicObject ->
|
||||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
|
||||||
try {
|
try {
|
||||||
|
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||||
// It was a map before
|
// It was a map before
|
||||||
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
||||||
|
|
||||||
|
@ -134,16 +156,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
Timber.e(e, "Error")
|
Timber.e(e, "Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?.removeField("requestBodyString")
|
?.removeFieldIfExists("requestBodyString")
|
||||||
|
|
||||||
Timber.d("Create KeysBackupDataEntity")
|
Timber.d("Create KeysBackupDataEntity")
|
||||||
|
|
||||||
realm.schema.create("KeysBackupDataEntity")
|
if (!realm.schema.contains("KeysBackupDataEntity")) {
|
||||||
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
|
realm.schema.create("KeysBackupDataEntity")
|
||||||
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
|
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
|
||||||
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
|
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
|
||||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
|
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
|
||||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
|
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
|
||||||
|
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo3RiotX(realm: DynamicRealm) {
|
private fun migrateTo3RiotX(realm: DynamicRealm) {
|
||||||
|
@ -151,8 +175,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
Timber.d("Migrate to RiotX model")
|
Timber.d("Migrate to RiotX model")
|
||||||
|
|
||||||
realm.schema.get("CryptoRoomEntity")
|
realm.schema.get("CryptoRoomEntity")
|
||||||
?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
|
?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
|
||||||
?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
|
?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
|
||||||
|
|
||||||
// Convert format of MXDeviceInfo, package has to be the same.
|
// Convert format of MXDeviceInfo, package has to be the same.
|
||||||
realm.schema.get("DeviceInfoEntity")
|
realm.schema.get("DeviceInfoEntity")
|
||||||
|
@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
// Version 4L added Cross Signing info persistence
|
// Version 4L added Cross Signing info persistence
|
||||||
private fun migrateTo4(realm: DynamicRealm) {
|
private fun migrateTo4(realm: DynamicRealm) {
|
||||||
Timber.d("Step 3 -> 4")
|
Timber.d("Step 3 -> 4")
|
||||||
Timber.d("Create KeyInfoEntity")
|
|
||||||
|
|
||||||
|
if (realm.schema.contains("TrustLevelEntity")) {
|
||||||
|
Timber.d("Skipping Step 3 -> 4 because entities already exist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Create KeyInfoEntity")
|
||||||
val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
|
val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||||
|
|
|
@ -33,14 +33,13 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
|
||||||
data class Params(val roomId: String,
|
data class Params(val roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
||||||
val keepKeys: List<String>? = null,
|
val keepKeys: List<String>? = null
|
||||||
val crypto: CryptoService
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultEncryptEventTask @Inject constructor(
|
internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
// private val crypto: CryptoService
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val localEchoRepository: LocalEchoRepository
|
private val cryptoService: CryptoService
|
||||||
) : EncryptEventTask {
|
) : EncryptEventTask {
|
||||||
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
||||||
// don't want to wait for any query
|
// don't want to wait for any query
|
||||||
|
@ -60,7 +59,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
// try {
|
// try {
|
||||||
// let it throws
|
// let it throws
|
||||||
awaitCallback<MXEncryptEventContentResult> {
|
awaitCallback<MXEncryptEventContentResult> {
|
||||||
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||||
}.let { result ->
|
}.let { result ->
|
||||||
val modifiedContent = HashMap(result.eventContent)
|
val modifiedContent = HashMap(result.eventContent)
|
||||||
params.keepKeys?.forEach { toKeep ->
|
params.keepKeys?.forEach { toKeep ->
|
||||||
|
@ -81,7 +80,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
).toContent(),
|
).toContent(),
|
||||||
forwardingCurve25519KeyChain = emptyList(),
|
forwardingCurve25519KeyChain = emptyList(),
|
||||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||||
claimedEd25519Key = params.crypto.getMyDevice().fingerprint()
|
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
@ -29,8 +28,7 @@ import javax.inject.Inject
|
||||||
internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val encrypt: Boolean,
|
val encrypt: Boolean
|
||||||
val cryptoService: CryptoService?
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +66,7 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||||
params.event.roomId ?: "",
|
params.event.roomId ?: "",
|
||||||
params.event,
|
params.event,
|
||||||
listOf("m.relates_to"),
|
listOf("m.relates_to")
|
||||||
params.cryptoService!!
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return params.event
|
return params.event
|
||||||
|
|
|
@ -15,21 +15,20 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||||
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val event: Event,
|
val event: Event
|
||||||
val cryptoService: CryptoService?
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val encryptEventTask: DefaultEncryptEventTask,
|
private val encryptEventTask: DefaultEncryptEventTask,
|
||||||
private val roomAPI: RoomAPI,
|
private val roomAPI: RoomAPI,
|
||||||
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val eventBus: EventBus) : SendVerificationMessageTask {
|
private val eventBus: EventBus) : SendVerificationMessageTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
|
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
|
||||||
|
@ -62,13 +62,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
|
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
|
||||||
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
|
if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
|
||||||
try {
|
try {
|
||||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||||
params.event.roomId ?: "",
|
params.event.roomId ?: "",
|
||||||
params.event,
|
params.event,
|
||||||
listOf("m.relates_to"),
|
listOf("m.relates_to")
|
||||||
params.cryptoService
|
|
||||||
))
|
))
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
// We said it's ok to send verification request in clear
|
// We said it's ok to send verification request in clear
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
|
@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
// Cannot be injected in constructor as it creates a dependency cycle
|
|
||||||
lateinit var cryptoService: CryptoService
|
|
||||||
|
|
||||||
// map [sender : [transaction]]
|
// map [sender : [transaction]]
|
||||||
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||||
|
|
||||||
|
@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
// Event received from the sync
|
// Event received from the sync
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.KEY_VERIFICATION_START -> {
|
EventType.KEY_VERIFICATION_START -> {
|
||||||
onStartRequestReceived(event)
|
onStartRequestReceived(event)
|
||||||
|
@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRoomEvent(event: Event) {
|
fun onRoomEvent(event: Event) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.KEY_VERIFICATION_START -> {
|
EventType.KEY_VERIFICATION_START -> {
|
||||||
onRoomStartRequestReceived(event)
|
onRoomStartRequestReceived(event)
|
||||||
|
@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
|
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
|
||||||
|
Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
|
||||||
uiHandler.post {
|
uiHandler.post {
|
||||||
listeners.forEach {
|
listeners.forEach {
|
||||||
try {
|
try {
|
||||||
|
@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
// We don't want to block here
|
// We don't want to block here
|
||||||
val otherDeviceId = validRequestInfo.fromDevice
|
val otherDeviceId = validRequestInfo.fromDevice
|
||||||
|
|
||||||
|
Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch {
|
cryptoCoroutineScope.launch {
|
||||||
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
|
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
|
||||||
Timber.e("## Verification device $otherDeviceId is not known")
|
Timber.e("## Verification device $otherDeviceId is not known")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
|
||||||
|
|
||||||
// Remember this request
|
// Remember this request
|
||||||
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
|
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
|
||||||
|
@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
// TODO refactor this with the DM one
|
// TODO refactor this with the DM one
|
||||||
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
|
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
|
||||||
|
|
||||||
val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
|
val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
|
||||||
|
?.values?.map { it.deviceId } ?: emptyList()
|
||||||
|
|
||||||
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
|
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
|
||||||
|
|
||||||
val transport = verificationTransportToDeviceFactory.createTransport(null)
|
val transport = verificationTransportToDeviceFactory.createTransport(null)
|
||||||
|
|
|
@ -20,7 +20,6 @@ import androidx.work.Data
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||||
|
@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context,
|
||||||
|
|
||||||
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||||
@Inject lateinit var cryptoService: CryptoService
|
|
||||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||||
|
|
||||||
override fun injectWith(injector: SessionComponent) {
|
override fun injectWith(injector: SessionComponent) {
|
||||||
|
@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
||||||
return try {
|
return try {
|
||||||
val resultEventId = sendVerificationMessageTask.execute(
|
val resultEventId = sendVerificationMessageTask.execute(
|
||||||
SendVerificationMessageTask.Params(
|
SendVerificationMessageTask.Params(
|
||||||
event = localEvent,
|
event = localEvent
|
||||||
cryptoService = cryptoService
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
package org.matrix.android.sdk.internal.crypto.verification
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class VerificationMessageProcessor @Inject constructor(
|
internal class VerificationMessageProcessor @Inject constructor(
|
||||||
private val cryptoService: CryptoService,
|
private val eventDecryptor: EventDecryptor,
|
||||||
private val verificationService: DefaultVerificationService,
|
private val verificationService: DefaultVerificationService,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@DeviceId private val deviceId: String?
|
@DeviceId private val deviceId: String?
|
||||||
|
@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor(
|
||||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||||
// for now decrypt sync
|
// for now decrypt sync
|
||||||
try {
|
try {
|
||||||
val result = cryptoService.decryptEvent(event, "")
|
val result = eventDecryptor.decryptEvent(event, "")
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
senderKey = result.senderCurve25519Key,
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.database
|
package org.matrix.android.sdk.internal.database
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||||
|
@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||||
private val cryptoService: CryptoService)
|
private val eventDecryptor: EventDecryptor)
|
||||||
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query {
|
override val query = Monarchy.Query {
|
||||||
|
@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val domainEvent = event.asDomain()
|
val domainEvent = event.asDomain()
|
||||||
decryptIfNeeded(domainEvent)
|
// decryptIfNeeded(domainEvent)
|
||||||
processors.filter {
|
processors.filter {
|
||||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||||
}.forEach {
|
}.forEach {
|
||||||
|
@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptIfNeeded(event: Event) {
|
// private fun decryptIfNeeded(event: Event) {
|
||||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
// if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||||
try {
|
// try {
|
||||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
// event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
// payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
// senderKey = result.senderCurve25519Key,
|
||||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
)
|
// )
|
||||||
} catch (e: MXCryptoError) {
|
// } catch (e: MXCryptoError) {
|
||||||
Timber.v("Failed to decrypt event")
|
// Timber.v("Failed to decrypt event")
|
||||||
// TODO -> we should keep track of this and retry, or some processing will never be handled
|
// // TODO -> we should keep track of this and retry, or some processing will never be handled
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||||
return processors.any {
|
return processors.any {
|
||||||
|
|
|
@ -65,7 +65,6 @@ import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
|
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncThread
|
import org.matrix.android.sdk.internal.session.sync.job.SyncThread
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
||||||
|
@ -115,7 +114,6 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val accountDataService: Lazy<AccountDataService>,
|
private val accountDataService: Lazy<AccountDataService>,
|
||||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||||
private val accountService: Lazy<AccountService>,
|
private val accountService: Lazy<AccountService>,
|
||||||
private val timelineEventDecryptor: TimelineEventDecryptor,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val defaultIdentityService: DefaultIdentityService,
|
private val defaultIdentityService: DefaultIdentityService,
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
|
@ -162,7 +160,6 @@ internal class DefaultSession @Inject constructor(
|
||||||
lifecycleObservers.forEach { it.onStart() }
|
lifecycleObservers.forEach { it.onStart() }
|
||||||
}
|
}
|
||||||
eventBus.register(this)
|
eventBus.register(this)
|
||||||
timelineEventDecryptor.start()
|
|
||||||
eventSenderProcessor.start()
|
eventSenderProcessor.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +197,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
override fun close() {
|
override fun close() {
|
||||||
assert(isOpen)
|
assert(isOpen)
|
||||||
stopSync()
|
stopSync()
|
||||||
timelineEventDecryptor.destroy()
|
// timelineEventDecryptor.destroy()
|
||||||
uiHandler.post {
|
uiHandler.post {
|
||||||
lifecycleObservers.forEach { it.onStop() }
|
lifecycleObservers.forEach { it.onStop() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||||
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
|
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
|
||||||
|
@ -128,6 +129,8 @@ internal interface SessionComponent {
|
||||||
|
|
||||||
fun inject(worker: SendGossipWorker)
|
fun inject(worker: SendGossipWorker)
|
||||||
|
|
||||||
|
fun inject(worker: UpdateTrustWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater
|
|
||||||
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||||
|
@ -333,10 +332,6 @@ internal abstract class SessionModule {
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
|
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoSet
|
|
||||||
abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
|
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
|
||||||
|
|
|
@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor(
|
||||||
return roomGetter.getRoom(roomId)
|
return roomGetter.getRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
|
override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
|
||||||
return roomGetter.getDirectRoomWith(otherUserId)
|
return roomGetter.getDirectRoomWith(otherUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.session.room
|
package org.matrix.android.sdk.internal.session.room
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import io.realm.Realm
|
||||||
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
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.events.model.EventType
|
||||||
|
@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
import io.realm.Realm
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
|
||||||
return newState
|
return newState
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
|
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
|
||||||
private val cryptoService: CryptoService
|
: EventInsertLiveProcessor {
|
||||||
) : EventInsertLiveProcessor {
|
|
||||||
|
|
||||||
private val allowedTypes = listOf(
|
private val allowedTypes = listOf(
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
|
|
|
@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface RoomGetter {
|
internal interface RoomGetter {
|
||||||
fun getRoom(roomId: String): Room?
|
fun getRoom(roomId: String): Room?
|
||||||
|
|
||||||
fun getDirectRoomWith(otherUserId: String): Room?
|
fun getDirectRoomWith(otherUserId: String): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDirectRoomWith(otherUserId: String): Room? {
|
override fun getDirectRoomWith(otherUserId: String): String? {
|
||||||
return realmSessionProvider.withRealm { realm ->
|
return realmSessionProvider.withRealm { realm ->
|
||||||
RoomSummaryEntity.where(realm)
|
RoomSummaryEntity.where(realm)
|
||||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||||
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
||||||
.findAll()
|
.findAll()
|
||||||
.filter { dm -> dm.otherMemberIds.contains(otherUserId) }
|
.firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId }
|
||||||
.map { it.roomId }
|
?.roomId
|
||||||
.firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
|
|
||||||
?.let { roomId -> createRoom(realm, roomId) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.create
|
package org.matrix.android.sdk.internal.session.room.create
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
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.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
|
@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
||||||
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
|
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
|
||||||
|
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||||
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
|
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
|
||||||
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
|
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
|
||||||
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class CreateRoomBodyBuilder @Inject constructor(
|
internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
|
@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val identityStore: IdentityStore,
|
private val identityStore: IdentityStore,
|
||||||
|
private val fileUploader: FileUploader,
|
||||||
@AuthenticatedIdentity
|
@AuthenticatedIdentity
|
||||||
private val accessTokenProvider: AccessTokenProvider
|
private val accessTokenProvider: AccessTokenProvider
|
||||||
) {
|
) {
|
||||||
|
@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
|
|
||||||
val initialStates = listOfNotNull(
|
val initialStates = listOfNotNull(
|
||||||
buildEncryptionWithAlgorithmEvent(params),
|
buildEncryptionWithAlgorithmEvent(params),
|
||||||
buildHistoryVisibilityEvent(params)
|
buildHistoryVisibilityEvent(params),
|
||||||
|
buildAvatarEvent(params)
|
||||||
)
|
)
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
||||||
|
return params.avatarUri?.let { avatarUri ->
|
||||||
|
// First upload the image, ignoring any error
|
||||||
|
tryOrNull {
|
||||||
|
fileUploader.uploadFromUri(
|
||||||
|
uri = avatarUri,
|
||||||
|
filename = UUID.randomUUID().toString(),
|
||||||
|
mimeType = "image/jpeg")
|
||||||
|
}
|
||||||
|
?.let { response ->
|
||||||
|
Event(
|
||||||
|
type = EventType.STATE_ROOM_AVATAR,
|
||||||
|
stateKey = "",
|
||||||
|
content = mapOf("url" to response.contentUri)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
|
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
|
||||||
return params.historyVisibility
|
return params.historyVisibility
|
||||||
?.let {
|
?.let {
|
||||||
val contentMap = mapOf("history_visibility" to it)
|
|
||||||
|
|
||||||
Event(
|
Event(
|
||||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
stateKey = "",
|
stateKey = "",
|
||||||
content = contentMap.toContent())
|
content = mapOf("history_visibility" to it)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
|
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||||
throw InvalidParameterException("Unsupported algorithm: $it")
|
throw InvalidParameterException("Unsupported algorithm: $it")
|
||||||
}
|
}
|
||||||
val contentMap = mapOf("algorithm" to it)
|
|
||||||
|
|
||||||
Event(
|
Event(
|
||||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||||
stateKey = "",
|
stateKey = "",
|
||||||
content = contentMap.toContent()
|
content = mapOf("algorithm" to it)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
@ -31,13 +30,13 @@ import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
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.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
|
@ -47,11 +46,9 @@ import timber.log.Timber
|
||||||
|
|
||||||
internal class DefaultRelationService @AssistedInject constructor(
|
internal class DefaultRelationService @AssistedInject constructor(
|
||||||
@Assisted private val roomId: String,
|
@Assisted private val roomId: String,
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
// private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
|
@ -122,7 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||||
.also { saveLocalEcho(it) }
|
.also { saveLocalEcho(it) }
|
||||||
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editReply(replyToEdit: TimelineEvent,
|
override fun editReply(replyToEdit: TimelineEvent,
|
||||||
|
@ -139,11 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||||
compatibilityBodyText
|
compatibilityBodyText
|
||||||
)
|
)
|
||||||
.also { saveLocalEcho(it) }
|
.also { saveLocalEcho(it) }
|
||||||
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
|
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
|
||||||
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
|
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
|
||||||
fetchEditHistoryTask
|
fetchEditHistoryTask
|
||||||
.configureWith(params) {
|
.configureWith(params) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
|
@ -156,7 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||||
?.also { saveLocalEcho(it) }
|
?.also { saveLocalEcho(it) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
|
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
|
||||||
|
|
|
@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||||
|
@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.CancelableBag
|
import org.matrix.android.sdk.api.util.CancelableBag
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||||
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
import org.matrix.android.sdk.internal.di.SessionId
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||||
|
@ -64,7 +64,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
|
@ -251,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
||||||
val cancelableBag = CancelableBag()
|
val cancelableBag = CancelableBag()
|
||||||
|
|
||||||
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
|
allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
|
||||||
.apply {
|
.apply {
|
||||||
keys.forEach { isRoomEncrypted ->
|
keys.forEach { isRoomEncrypted ->
|
||||||
// Should never be empty
|
// Should never be empty
|
||||||
|
@ -282,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendEvent(event: Event): Cancelable {
|
private fun sendEvent(event: Event): Cancelable {
|
||||||
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(event.roomId!!))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
|
|
|
@ -87,7 +87,7 @@ internal class SendEventWorker(context: Context,
|
||||||
|
|
||||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
|
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
|
||||||
return try {
|
return try {
|
||||||
sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId), cryptoService))
|
sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId)))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
|
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ internal class SendEventQueuedTask(
|
||||||
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
|
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute() {
|
||||||
sendEventTask.execute(SendEventTask.Params(event, encrypt, cryptoService))
|
sendEventTask.execute(SendEventTask.Params(event, encrypt))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskFailed() {
|
override fun onTaskFailed() {
|
||||||
|
|
|
@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||||
|
sendStateEvent(
|
||||||
|
eventType = EventType.STATE_ROOM_AVATAR,
|
||||||
|
body = emptyMap(),
|
||||||
|
callback = callback,
|
||||||
|
stateKey = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.summary
|
package org.matrix.android.sdk.internal.session.room.summary
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
|
@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
|
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
|
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
|
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
|
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||||
private val roomAvatarResolver: RoomAvatarResolver,
|
private val roomAvatarResolver: RoomAvatarResolver,
|
||||||
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
|
private val eventDecryptor: EventDecryptor,
|
||||||
private val eventBus: EventBus) {
|
private val crossSigningService: DefaultCrossSigningService) {
|
||||||
|
|
||||||
fun update(realm: Realm,
|
fun update(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
|
@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
}
|
}
|
||||||
roomSummaryEntity.updateHasFailedSending()
|
roomSummaryEntity.updateHasFailedSending()
|
||||||
|
|
||||||
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
|
val root = latestPreviewableEvent?.root
|
||||||
|
if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
||||||
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
||||||
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
|
// mmm i want to decrypt now or is it ok to do it async?
|
||||||
|
tryOrNull {
|
||||||
|
eventDecryptor.decryptEvent(root.asDomain(), "")
|
||||||
|
// eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateMembers) {
|
if (updateMembers) {
|
||||||
|
@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
roomSummaryEntity.otherMemberIds.clear()
|
roomSummaryEntity.otherMemberIds.clear()
|
||||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||||
if (roomSummaryEntity.isEncrypted) {
|
if (roomSummaryEntity.isEncrypted) {
|
||||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
|
// mmm maybe we could only refresh shield instead of checking trust also?
|
||||||
|
crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
roomSummaryEntity.updateHasFailedSending()
|
roomSummaryEntity.updateHasFailedSending()
|
||||||
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateShieldTrust(realm: Realm,
|
|
||||||
roomId: String,
|
|
||||||
trust: RoomEncryptionTrustLevel?) {
|
|
||||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
|
||||||
if (roomSummaryEntity.isEncrypted) {
|
|
||||||
roomSummaryEntity.roomEncryptionTrustLevel = trust
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,11 +157,14 @@ internal class DefaultTimeline(
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
?: throw IllegalStateException("Can't open a timeline without a room")
|
?: throw IllegalStateException("Can't open a timeline without a room")
|
||||||
|
|
||||||
sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll()
|
// We don't want to filter here because some sending events that are not displayed
|
||||||
|
// are still used for ui echo (relation like reaction)
|
||||||
|
sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
|
||||||
sendingEvents.addChangeListener { events ->
|
sendingEvents.addChangeListener { events ->
|
||||||
uiEchoManager.sentEventsUpdated(events)
|
uiEchoManager.sentEventsUpdated(events)
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||||
filteredEvents = nonFilteredEvents.where()
|
filteredEvents = nonFilteredEvents.where()
|
||||||
.filterEventsWithSettings()
|
.filterEventsWithSettings()
|
||||||
|
@ -411,13 +414,16 @@ internal class DefaultTimeline(
|
||||||
val builtSendingEvents = ArrayList<TimelineEvent>()
|
val builtSendingEvents = ArrayList<TimelineEvent>()
|
||||||
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
||||||
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
|
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
|
||||||
sendingEvents.forEach { timelineEventEntity ->
|
sendingEvents
|
||||||
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
|
.map { timelineEventMapper.map(it) }
|
||||||
val element = timelineEventMapper.map(timelineEventEntity)
|
// Filter out sending event that are not displayable!
|
||||||
uiEchoManager.updateSentStateWithUiEcho(element)
|
.filterEventsWithSettings()
|
||||||
builtSendingEvents.add(element)
|
.forEach { timelineEvent ->
|
||||||
}
|
if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
|
||||||
}
|
uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
|
||||||
|
builtSendingEvents.add(timelineEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return builtSendingEvents
|
return builtSendingEvents
|
||||||
}
|
}
|
||||||
|
@ -634,7 +640,7 @@ internal class DefaultTimeline(
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) }
|
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||||
|
@ -786,8 +792,8 @@ internal class DefaultTimeline(
|
||||||
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
|
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
|
||||||
if (!filterType) return@filter false
|
if (!filterType) return@filter false
|
||||||
|
|
||||||
val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
|
val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
|
||||||
val messageContent = it.root.content.toModel<MessageContent>()
|
val messageContent = it.root.getClearContent().toModel<MessageContent>()
|
||||||
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
|
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
@ -817,7 +823,7 @@ internal class DefaultTimeline(
|
||||||
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
|
|
||||||
fun getInMemorySendingEvents(): List<TimelineEvent> {
|
fun getInMemorySendingEvents(): List<TimelineEvent> {
|
||||||
return inMemorySendingEvents
|
return inMemorySendingEvents.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -835,8 +841,13 @@ internal class DefaultTimeline(
|
||||||
inMemorySendingStates.keys.removeAll { key ->
|
inMemorySendingStates.keys.removeAll { key ->
|
||||||
events.find { it.eventId == key } == null
|
events.find { it.eventId == key } == null
|
||||||
}
|
}
|
||||||
inMemoryReactions.keys.removeAll { key ->
|
|
||||||
events.find { it.eventId == key } == null
|
inMemoryReactions.forEach { (_, uiEchoData) ->
|
||||||
|
uiEchoData.removeAll { data ->
|
||||||
|
// I remove the uiEcho, when the related event is not anymore in the sending list
|
||||||
|
// (means that it is synced)!
|
||||||
|
events.find { it.eventId == data.localEchoId } == null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -900,6 +911,7 @@ internal class DefaultTimeline(
|
||||||
relatedEventID
|
relatedEventID
|
||||||
)
|
)
|
||||||
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
|
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
|
||||||
|
|
||||||
contents.forEach { uiEchoReaction ->
|
contents.forEach { uiEchoReaction ->
|
||||||
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
|
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
|
|
|
@ -15,24 +15,22 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.session.room.timeline
|
package org.matrix.android.sdk.internal.session.room.timeline
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class TimelineEventDecryptor @Inject constructor(
|
internal class TimelineEventDecryptor @Inject constructor(
|
||||||
@SessionDatabase
|
@SessionDatabase
|
||||||
private val realmConfiguration: RealmConfiguration,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
|
@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
for (requests in unknownSessionsFailure.values) {
|
for (requests in unknownSessionsFailure.values) {
|
||||||
if (request in requests) {
|
if (request in requests) {
|
||||||
Timber.d("Skip Decryption request for event ${request.eventId}, unknown session")
|
Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronized(existingRequests) {
|
synchronized(existingRequests) {
|
||||||
if (!existingRequests.add(request)) {
|
if (!existingRequests.add(request)) {
|
||||||
Timber.d("Skip Decryption request for event ${request.eventId}, already requested")
|
Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
|
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
|
||||||
val eventId = request.eventId
|
val event = request.event
|
||||||
val timelineId = request.timelineId
|
val timelineId = request.timelineId
|
||||||
Timber.v("Decryption request for event $eventId")
|
|
||||||
val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
|
|
||||||
?: return@executeTransaction Unit.also {
|
|
||||||
Timber.d("Decryption request for unknown message")
|
|
||||||
}
|
|
||||||
val event = eventEntity.asDomain()
|
|
||||||
try {
|
try {
|
||||||
val result = cryptoService.decryptEvent(event, timelineId)
|
val result = cryptoService.decryptEvent(request.event, timelineId)
|
||||||
Timber.v("Successfully decrypted event $eventId")
|
Timber.v("Successfully decrypted event ${event.eventId}")
|
||||||
eventEntity.setDecryptionResult(result)
|
realm.executeTransaction {
|
||||||
|
EventEntity.where(it, eventId = event.eventId ?: "")
|
||||||
|
.findFirst()
|
||||||
|
?.setDecryptionResult(result)
|
||||||
|
}
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
Timber.v(e, "Failed to decrypt event $eventId")
|
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
|
||||||
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
|
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
|
||||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||||
eventEntity.decryptionErrorCode = e.errorType.name
|
realm.executeTransaction {
|
||||||
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
EventEntity.where(it, eventId = event.eventId ?: "")
|
||||||
|
.findFirst()
|
||||||
|
?.let {
|
||||||
|
it.decryptionErrorCode = e.errorType.name
|
||||||
|
it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||||
content.sessionId?.let { sessionId ->
|
content.sessionId?.let { sessionId ->
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
|
@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
|
Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}")
|
||||||
} finally {
|
} finally {
|
||||||
synchronized(existingRequests) {
|
synchronized(existingRequests) {
|
||||||
existingRequests.remove(request)
|
existingRequests.remove(request)
|
||||||
|
@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DecryptionRequest(
|
data class DecryptionRequest(
|
||||||
val eventId: String,
|
val event: Event,
|
||||||
val timelineId: String
|
val timelineId: String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.sync
|
package org.matrix.android.sdk.internal.session.sync
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.matrix.android.sdk.R
|
import org.matrix.android.sdk.R
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
|
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
|
||||||
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
|
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
|
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
|
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
|
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
|
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
|
||||||
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
|
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.kotlin.createObject
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
||||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus) {
|
||||||
private val timelineEventDecryptor: TimelineEventDecryptor) {
|
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||||
|
|
|
@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||||
private val activeCallHandler: ActiveCallHandler
|
private val activeCallHandler: ActiveCallHandler
|
||||||
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||||
|
|
||||||
private var state: SyncState = SyncState.Idle
|
private var state: SyncState = SyncState.Idle
|
||||||
private var liveState = MutableLiveData<SyncState>(state)
|
private var liveState = MutableLiveData<SyncState>(state)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="summary_user_sent_image">%1$s изпрати снимка.</string>
|
<string name="summary_user_sent_image">%1$s изпрати снимка.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">Поканата на %s</string>
|
<string name="notice_room_invite_no_invitee">Поканата на %s</string>
|
||||||
<string name="notice_room_invite">%1$s покани %2$s</string>
|
<string name="notice_room_invite">%1$s покани %2$s</string>
|
||||||
<string name="notice_room_invite_you">%1$s Ви покани</string>
|
<string name="notice_room_invite_you">%1$s Ви покани</string>
|
||||||
|
@ -22,7 +20,7 @@
|
||||||
<string name="notice_room_name_changed">%1$s смени името на стаята на: %2$s</string>
|
<string name="notice_room_name_changed">%1$s смени името на стаята на: %2$s</string>
|
||||||
<string name="notice_placed_video_call">%s започна видео разговор.</string>
|
<string name="notice_placed_video_call">%s започна видео разговор.</string>
|
||||||
<string name="notice_placed_voice_call">%s започна гласов разговор.</string>
|
<string name="notice_placed_voice_call">%s започна гласов разговор.</string>
|
||||||
<string name="notice_answered_call">%s отговори на повикването.</string>
|
<string name="notice_answered_call">%s отговори на обаждането.</string>
|
||||||
<string name="notice_ended_call">%s прекрати разговора.</string>
|
<string name="notice_ended_call">%s прекрати разговора.</string>
|
||||||
<string name="notice_made_future_room_visibility">%1$s направи бъдещата история на стаята видима за %2$s</string>
|
<string name="notice_made_future_room_visibility">%1$s направи бъдещата история на стаята видима за %2$s</string>
|
||||||
<string name="notice_room_visibility_invited">всички членове, от момента на поканването им в нея.</string>
|
<string name="notice_room_visibility_invited">всички членове, от момента на поканването им в нея.</string>
|
||||||
|
@ -31,54 +29,39 @@
|
||||||
<string name="notice_room_visibility_world_readable">всеки.</string>
|
<string name="notice_room_visibility_world_readable">всеки.</string>
|
||||||
<string name="notice_room_visibility_unknown">непозната (%s).</string>
|
<string name="notice_room_visibility_unknown">непозната (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s включи шифроване от край до край (%2$s)</string>
|
<string name="notice_end_to_end">%1$s включи шифроване от край до край (%2$s)</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s заяви VoIP групов разговор</string>
|
<string name="notice_requested_voip_conference">%1$s заяви VoIP групов разговор</string>
|
||||||
<string name="notice_voip_started">Започна VoIP групов разговор</string>
|
<string name="notice_voip_started">Започна VoIP групов разговор</string>
|
||||||
<string name="notice_voip_finished">Груповият разговор приключи</string>
|
<string name="notice_voip_finished">Груповият разговор приключи</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(профилната снимка също беше сменена)</string>
|
<string name="notice_avatar_changed_too">(профилната снимка също беше сменена)</string>
|
||||||
<string name="notice_room_name_removed">%1$s премахна името на стаята</string>
|
<string name="notice_room_name_removed">%1$s премахна името на стаята</string>
|
||||||
<string name="notice_room_topic_removed">%1$s премахна темата на стаята</string>
|
<string name="notice_room_topic_removed">%1$s премахна темата на стаята</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s обнови своя профил %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s обнови своя профил %2$s</string>
|
||||||
<string name="notice_room_third_party_invite">%1$s изпрати покана на %2$s да се присъедини към стаята</string>
|
<string name="notice_room_third_party_invite">%1$s изпрати покана на %2$s да се присъедини към стаята</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s прие поканата за %2$s</string>
|
<string name="notice_room_third_party_registered_invite">%1$s прие поканата за %2$s</string>
|
||||||
|
|
||||||
<string name="notice_crypto_unable_to_decrypt">** Неуспешно разшифроване: %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** Неуспешно разшифроване: %s **</string>
|
||||||
<string name="could_not_redact">Неуспешно премахване</string>
|
<string name="could_not_redact">Неуспешно премахване</string>
|
||||||
<string name="unable_to_send_message">Неуспешно изпращане на съобщението</string>
|
<string name="unable_to_send_message">Неуспешно изпращане на съобщението</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">Неуспешно качване на снимката</string>
|
<string name="message_failed_to_upload">Неуспешно качване на снимката</string>
|
||||||
|
|
||||||
<string name="network_error">Грешка в мрежата</string>
|
<string name="network_error">Грешка в мрежата</string>
|
||||||
<string name="matrix_error">Matrix грешка</string>
|
<string name="matrix_error">Matrix грешка</string>
|
||||||
|
|
||||||
<string name="room_error_join_failed_empty_room">В момента не е възможно да се присъедините отново към празна стая.</string>
|
<string name="room_error_join_failed_empty_room">В момента не е възможно да се присъедините отново към празна стая.</string>
|
||||||
|
|
||||||
<string name="encrypted_message">Шифровано съобщение</string>
|
<string name="encrypted_message">Шифровано съобщение</string>
|
||||||
|
|
||||||
<string name="medium_email">Имейл адрес</string>
|
<string name="medium_email">Имейл адрес</string>
|
||||||
<string name="medium_phone_number">Телефонен номер</string>
|
<string name="medium_phone_number">Телефонен номер</string>
|
||||||
|
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Устройството на подателя не изпрати ключовете за това съобщение.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">Устройството на подателя не изпрати ключовете за това съобщение.</string>
|
||||||
|
|
||||||
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
|
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">Покана от %s</string>
|
<string name="room_displayname_invite_from">Покана от %s</string>
|
||||||
<string name="room_displayname_room_invite">Покана за стая</string>
|
<string name="room_displayname_room_invite">Покана за стая</string>
|
||||||
<string name="room_displayname_two_members">%1$s и %2$s</string>
|
<string name="room_displayname_two_members">%1$s и %2$s</string>
|
||||||
|
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s и 1 друг</item>
|
<item quantity="one">%1$s и 1 друг</item>
|
||||||
<item quantity="other">%1$s и %2$d други</item>
|
<item quantity="other">%1$s и %2$d други</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="room_displayname_empty_room">Празна стая</string>
|
<string name="room_displayname_empty_room">Празна стая</string>
|
||||||
|
|
||||||
<string name="notice_event_redacted">Премахнато съобщение</string>
|
<string name="notice_event_redacted">Премахнато съобщение</string>
|
||||||
<string name="notice_event_redacted_by">Съобщение премахнато от %1$s</string>
|
<string name="notice_event_redacted_by">Съобщение премахнато от %1$s</string>
|
||||||
<string name="notice_event_redacted_with_reason">Премахнато съобщение [причина: %1$s]</string>
|
<string name="notice_event_redacted_with_reason">Премахнато съобщение [причина: %1$s]</string>
|
||||||
<string name="notice_event_redacted_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string>
|
<string name="notice_event_redacted_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account">Начална синхронизация:
|
<string name="initial_sync_start_importing_account">Начална синхронизация:
|
||||||
\nИмпортиране на профил…</string>
|
\nИмпортиране на профил…</string>
|
||||||
<string name="initial_sync_start_importing_account_crypto">Начална синхронизация:
|
<string name="initial_sync_start_importing_account_crypto">Начална синхронизация:
|
||||||
|
@ -95,12 +78,9 @@
|
||||||
\nИмпортиране на общности</string>
|
\nИмпортиране на общности</string>
|
||||||
<string name="initial_sync_start_importing_account_data">Начална синхронизация:
|
<string name="initial_sync_start_importing_account_data">Начална синхронизация:
|
||||||
\nИмпортиране на данни за профила</string>
|
\nИмпортиране на данни за профила</string>
|
||||||
|
|
||||||
<string name="notice_room_update">%s обнови тази стая.</string>
|
<string name="notice_room_update">%s обнови тази стая.</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">Изпращане на съобщение…</string>
|
<string name="event_status_sending_message">Изпращане на съобщение…</string>
|
||||||
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>
|
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>
|
||||||
|
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s оттегли поканата за присъединяване на %2$s към стаята</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s оттегли поканата за присъединяване на %2$s към стаята</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">поканата на %1$s. Причина: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">поканата на %1$s. Причина: %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s покани %2$s. Причина: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s покани %2$s. Причина: %3$s</string>
|
||||||
|
@ -115,34 +95,25 @@
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s прие поканата за %2$s. Причина: %3$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason">%1$s прие поканата за %2$s. Причина: %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s оттегли поканата на %2$s. Причина: %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s оттегли поканата на %2$s. Причина: %3$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s добави %2$s като адрес за тази стая.</item>
|
<item quantity="one">%1$s добави %2$s като адрес за тази стая.</item>
|
||||||
<item quantity="other">%1$s добави %2$s като адреси за тази стая.</item>
|
<item quantity="other">%1$s добави %2$s като адреси за тази стая.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s премахна %2$s като адрес за тази стая.</item>
|
<item quantity="one">%1$s премахна %2$s като адрес за тази стая.</item>
|
||||||
<item quantity="other">%1$s премахна %2$s като адреси за тази стая.</item>
|
<item quantity="other">%1$s премахна %2$s като адреси за тази стая.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s добави %2$s и премахна %3$s като адреси за тази стая.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s добави %2$s и премахна %3$s като адреси за тази стая.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set">%1$s настрой %2$s като основен адрес за тази стая.</string>
|
<string name="notice_room_canonical_alias_set">%1$s настрой %2$s като основен адрес за тази стая.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s премахна основния адрес за тази стая.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s премахна основния адрес за тази стая.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join">%1$s разреши на гости да се присъединяват в стаята.</string>
|
<string name="notice_room_guest_access_can_join">%1$s разреши на гости да се присъединяват в стаята.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s предотврати присъединяването на гости в стаята.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s предотврати присъединяването на гости в стаята.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s включи шифроване от-край-до-край.</string>
|
<string name="notice_end_to_end_ok">%1$s включи шифроване от-край-до-край.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s).</string>
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове.</string>
|
<string name="key_verification_request_fallback_message">%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове.</string>
|
||||||
|
|
||||||
<string name="notice_room_created">%1$s създаде стаята</string>
|
<string name="notice_room_created">%1$s създаде стаята</string>
|
||||||
<string name="summary_you_sent_image">Изпратихте снимка.</string>
|
<string name="summary_you_sent_image">Изпратихте снимка.</string>
|
||||||
<string name="summary_you_sent_sticker">Изпратихте стикер.</string>
|
<string name="summary_you_sent_sticker">Изпратихте стикер.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_by_you">Ваша покана</string>
|
<string name="notice_room_invite_no_invitee_by_you">Ваша покана</string>
|
||||||
<string name="notice_room_created_by_you">Създадохте стаята</string>
|
<string name="notice_room_created_by_you">Създадохте стаята</string>
|
||||||
<string name="notice_room_invite_by_you">Поканихте %1$s</string>
|
<string name="notice_room_invite_by_you">Поканихте %1$s</string>
|
||||||
|
@ -152,4 +123,94 @@
|
||||||
<string name="notice_room_kick_by_you">Изгонихте %1$s</string>
|
<string name="notice_room_kick_by_you">Изгонихте %1$s</string>
|
||||||
<string name="notice_room_unban_by_you">Отблокирахте %1$s</string>
|
<string name="notice_room_unban_by_you">Отблокирахте %1$s</string>
|
||||||
<string name="notice_room_ban_by_you">Блокирахте %1$s</string>
|
<string name="notice_room_ban_by_you">Блокирахте %1$s</string>
|
||||||
</resources>
|
<string name="notice_end_to_end_unknown_algorithm_by_you">Включихте шифроване от-край-до-край (непознат алгоритъм: %1$s).</string>
|
||||||
|
<string name="notice_end_to_end_ok_by_you">Включихте шифроване от-край-до-край.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">Спряхте възможността гости да се присъединяват в стаята.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden">%1$s спря възможността гости да се присъединяват в стаята.</string>
|
||||||
|
<string name="notice_room_guest_access_forbidden_by_you">Спряхте възможността гости да се присъединяват в стаята.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join_by_you">Позволихте на гости да се присъединяват тук.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join">%1$s позволи на гости да се присъединяват тук.</string>
|
||||||
|
<string name="notice_room_guest_access_can_join_by_you">Позволихте на гости да се присъединяват към стаята.</string>
|
||||||
|
<string name="notice_room_canonical_alias_unset_by_you">Премахнахте основния адрес на стаята.</string>
|
||||||
|
<string name="notice_room_canonical_alias_set_by_you">Зададохте %1$s като основен адрес на стаята.</string>
|
||||||
|
<string name="notice_room_aliases_added_and_removed_by_you">Добавихте %1$s и премахнахте %2$s от адресите за стаята.</string>
|
||||||
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
|
<item quantity="one">Премахнахте %1$s от адресите на стаята.</item>
|
||||||
|
<item quantity="other">Премахнахте %1$s от адресите на стаята.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
|
<item quantity="one">Добавихте %1$s като адрес за тази стая.</item>
|
||||||
|
<item quantity="other">Добавихте %1$s като адреси за тази стая.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="notice_room_withdraw_with_reason_by_you">Оттеглихте поканата на %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Приехте поканата за %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Оттеглихте поканата за присъединяване в стаята от %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_with_reason_by_you">Изпратихте покана към %1$s за присъединяване в стаята. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_ban_with_reason_by_you">Блокирахте %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_unban_with_reason_by_you">Отблокирахте %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_kick_with_reason_by_you">Изгонихте %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_reject_with_reason_by_you">Отхвърлихте поканата. Причина: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason_by_you">Напуснахте. Причина: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason">%1$s напусна. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_leave_with_reason_by_you">Напуснахте стаята. Причина: %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason_by_you">Присъединихте се. Причина: %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason">%1$s се присъедини. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_join_with_reason_by_you">Присъединихте се в стаята. Причина: %1$s</string>
|
||||||
|
<string name="notice_room_invite_with_reason_by_you">Поканихте %1$s. Причина: %2$s</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">Ваша покана. Причина: %1$s</string>
|
||||||
|
<string name="notice_power_level_diff">%1$s от %2$s на %3$s</string>
|
||||||
|
<string name="notice_power_level_changed">%1$s промени нивото на достъп на %2$s.</string>
|
||||||
|
<string name="notice_power_level_changed_by_you">Променихте нивото на достъп на %1$s.</string>
|
||||||
|
<string name="power_level_custom_no_value">Собствено ниво</string>
|
||||||
|
<string name="power_level_custom">Собствено ниво (%1$d)</string>
|
||||||
|
<string name="power_level_default">По подразбиране</string>
|
||||||
|
<string name="power_level_moderator">Модератор</string>
|
||||||
|
<string name="power_level_admin">Администратор</string>
|
||||||
|
<string name="notice_widget_modified_by_you">Променихте %1$s приспособлението</string>
|
||||||
|
<string name="notice_widget_modified">%1$s промени %2$s приспособлението</string>
|
||||||
|
<string name="notice_widget_removed_by_you">Премахнахте %1$s приспособлението</string>
|
||||||
|
<string name="notice_widget_removed">%1$s премахна %2$s приспособлението</string>
|
||||||
|
<string name="notice_widget_added_by_you">Добавихте %1$s приспособление</string>
|
||||||
|
<string name="notice_widget_added">%1$s добави %2$s приспособление</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_by_you">Приехте поканата за %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite_by_you">Оттеглихте поканата от %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite">%1$s оттегли поканата от %2$s</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_by_you">Оттеглихте поканата за присъединяване в стаята от %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite_by_you">Поканихте %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite">%1$s покани %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_by_you">Изпратихте покана към %1$s за присъединяване в стаята</string>
|
||||||
|
<string name="notice_profile_change_redacted_by_you">Обновихте профила си %1$s</string>
|
||||||
|
<string name="notice_room_avatar_removed_by_you">Премахнахте снимката на стаята</string>
|
||||||
|
<string name="notice_room_avatar_removed">%1$s премахна снимката на стаята</string>
|
||||||
|
<string name="notice_room_topic_removed_by_you">Премахнахте темата на стаята</string>
|
||||||
|
<string name="notice_room_name_removed_by_you">Премахнахте името на стаята</string>
|
||||||
|
<string name="notice_requested_voip_conference_by_you">Заявихте VoIP конференция</string>
|
||||||
|
<string name="notice_direct_room_update_by_you">Обновихте чата.</string>
|
||||||
|
<string name="notice_direct_room_update">%s обнови чата.</string>
|
||||||
|
<string name="notice_room_update_by_you">Обновихте стаята.</string>
|
||||||
|
<string name="notice_end_to_end_by_you">Включихте шифроване от-край-до-край (%1$s)</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility_by_you">Направихте бъдещите съобщения видими за %1$s</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility">%1$s направи бъдещите съобщения видими за %2$s</string>
|
||||||
|
<string name="notice_made_future_room_visibility_by_you">Направихте бъдещата история на стаята видима за %1$s</string>
|
||||||
|
<string name="notice_ended_call_by_you">Прекратихте разговора.</string>
|
||||||
|
<string name="notice_placed_video_call_by_you">Започнахте видео разговор.</string>
|
||||||
|
<string name="notice_answered_call_by_you">Отговорихте на обаждането.</string>
|
||||||
|
<string name="notice_call_candidates_by_you">Изпратихте данни за настройка на разговора.</string>
|
||||||
|
<string name="notice_call_candidates">%s изпрати данни за настройка на разговора.</string>
|
||||||
|
<string name="notice_placed_voice_call_by_you">Започнахте гласов разговор.</string>
|
||||||
|
<string name="notice_room_name_changed_by_you">Променихте името на стаята на: %1$s</string>
|
||||||
|
<string name="notice_room_avatar_changed_by_you">Променихте снимката на стаята</string>
|
||||||
|
<string name="notice_room_avatar_changed">%1$s промени снимката на стаята</string>
|
||||||
|
<string name="notice_room_topic_changed_by_you">Променихте темата на: %1$s</string>
|
||||||
|
<string name="notice_display_name_removed_by_you">Премахнахте името си (%1$s)</string>
|
||||||
|
<string name="notice_display_name_changed_from_by_you">Променихте името си от %1$s на %2$s</string>
|
||||||
|
<string name="notice_display_name_set_by_you">Променихте името си на %1$s</string>
|
||||||
|
<string name="notice_avatar_url_changed_by_you">Променихте снимката си</string>
|
||||||
|
<string name="notice_room_withdraw_by_you">Оттеглихте поканата от %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_by_you">Напуснахте стаята</string>
|
||||||
|
<string name="notice_direct_room_leave">%1$s напусна стаята</string>
|
||||||
|
<string name="notice_direct_room_join_by_you">Присъединихте се</string>
|
||||||
|
<string name="notice_direct_room_join">%1$s се присъедини</string>
|
||||||
|
<string name="notice_direct_room_created_by_you">Създадохте дискусията</string>
|
||||||
|
<string name="notice_direct_room_created">%1$s създаде дискусията</string>
|
||||||
|
</resources>
|
|
@ -174,7 +174,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।</item>
|
<item quantity="one">আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।</item>
|
||||||
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন।</item>
|
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %1$s গুলি সরিয়েছেন।</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে।</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে।</string>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন।</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন।</string>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="summary_user_sent_image">Uživatel %1$s poslal obrázek.</string>
|
<string name="summary_user_sent_image">Uživatel %1$s poslal obrázek.</string>
|
||||||
<string name="summary_user_sent_sticker">Uživatel %1$s poslal nálepku.</string>
|
<string name="summary_user_sent_sticker">Uživatel %1$s poslal nálepku.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">Pozvání od uživatele %s</string>
|
<string name="notice_room_invite_no_invitee">Pozvání od uživatele %s</string>
|
||||||
<string name="notice_room_invite">Uživatel %1$s pozval uživatele %2$s</string>
|
<string name="notice_room_invite">Uživatel %1$s pozval uživatele %2$s</string>
|
||||||
<string name="notice_room_invite_you">Uživatel %1$s vás pozval</string>
|
<string name="notice_room_invite_you">Uživatel %1$s vás pozval</string>
|
||||||
|
@ -31,51 +30,36 @@
|
||||||
<string name="notice_room_visibility_world_readable">kohokoliv.</string>
|
<string name="notice_room_visibility_world_readable">kohokoliv.</string>
|
||||||
<string name="notice_room_visibility_unknown">neznámým (%s).</string>
|
<string name="notice_room_visibility_unknown">neznámým (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s zapnuli end-to-end šifrování (%2$s)</string>
|
<string name="notice_end_to_end">%1$s zapnuli end-to-end šifrování (%2$s)</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s požádali o VoIP konferenci</string>
|
<string name="notice_requested_voip_conference">%1$s požádali o VoIP konferenci</string>
|
||||||
<string name="notice_voip_started">Začala VoIP konference</string>
|
<string name="notice_voip_started">Začala VoIP konference</string>
|
||||||
<string name="notice_voip_finished">VoIP konference skončila</string>
|
<string name="notice_voip_finished">VoIP konference skončila</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(profilový obrázek byl také změněn)</string>
|
<string name="notice_avatar_changed_too">(profilový obrázek byl také změněn)</string>
|
||||||
<string name="notice_room_name_removed">%1$s odstranili název místnosti</string>
|
<string name="notice_room_name_removed">%1$s odstranili název místnosti</string>
|
||||||
<string name="notice_room_topic_removed">%1$s odstranili téma místnosti</string>
|
<string name="notice_room_topic_removed">%1$s odstranili téma místnosti</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s aktualizovali svůj profil %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s aktualizovali svůj profil %2$s</string>
|
||||||
<string name="notice_room_third_party_invite">%1$s do této místnosti pozvali %2$s</string>
|
<string name="notice_room_third_party_invite">%1$s do této místnosti pozvali %2$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s přijali pozvání pro %2$s</string>
|
<string name="notice_room_third_party_registered_invite">%1$s přijali pozvání pro %2$s</string>
|
||||||
|
|
||||||
<string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string>
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu.</string>
|
||||||
|
|
||||||
<string name="could_not_redact">Nelze vymazat</string>
|
<string name="could_not_redact">Nelze vymazat</string>
|
||||||
<string name="unable_to_send_message">Zprávu nelze odeslat</string>
|
<string name="unable_to_send_message">Zprávu nelze odeslat</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">Obrázek nelze nahrát</string>
|
<string name="message_failed_to_upload">Obrázek nelze nahrát</string>
|
||||||
|
|
||||||
<string name="network_error">Chyba sítě</string>
|
<string name="network_error">Chyba sítě</string>
|
||||||
<string name="matrix_error">Chyba v Matrixu</string>
|
<string name="matrix_error">Chyba v Matrixu</string>
|
||||||
|
|
||||||
<string name="room_error_join_failed_empty_room">V současnosti není možné znovu vstoupit do prázdné místnosti.</string>
|
<string name="room_error_join_failed_empty_room">V současnosti není možné znovu vstoupit do prázdné místnosti.</string>
|
||||||
|
|
||||||
<string name="encrypted_message">Šifrovaná zpráva</string>
|
<string name="encrypted_message">Šifrovaná zpráva</string>
|
||||||
|
|
||||||
<string name="medium_email">E-mailová adresa</string>
|
<string name="medium_email">E-mailová adresa</string>
|
||||||
<string name="medium_phone_number">Telefonní číslo</string>
|
<string name="medium_phone_number">Telefonní číslo</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">Pozvání od %s</string>
|
<string name="room_displayname_invite_from">Pozvání od %s</string>
|
||||||
<string name="room_displayname_room_invite">Pozvání do místnosti</string>
|
<string name="room_displayname_room_invite">Pozvání do místnosti</string>
|
||||||
|
|
||||||
<string name="room_displayname_two_members">%1$s a %2$s</string>
|
<string name="room_displayname_two_members">%1$s a %2$s</string>
|
||||||
|
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s a jeden další</item>
|
<item quantity="one">%1$s a jeden další</item>
|
||||||
<item quantity="few">%1$s a %2$d další</item>
|
<item quantity="few">%1$s a %2$d další</item>
|
||||||
<item quantity="other">%1$s a %2$d dalších</item>
|
<item quantity="other">%1$s a %2$d dalších</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="room_displayname_empty_room">Prázdná místnost</string>
|
<string name="room_displayname_empty_room">Prázdná místnost</string>
|
||||||
|
|
||||||
<string name="notice_room_update">%s povýšili tuto místnost.</string>
|
<string name="notice_room_update">%s povýšili tuto místnost.</string>
|
||||||
|
|
||||||
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
|
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
|
||||||
<string name="notice_event_redacted_by_with_reason">Zpráva smazána uživatelem %1$s [důvod: %2$s]</string>
|
<string name="notice_event_redacted_by_with_reason">Zpráva smazána uživatelem %1$s [důvod: %2$s]</string>
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s zrušili pozvánku do místnosti pro %2$s</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s zrušili pozvánku do místnosti pro %2$s</string>
|
||||||
|
@ -93,13 +77,10 @@
|
||||||
\nImportuji komunity</string>
|
\nImportuji komunity</string>
|
||||||
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
|
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
|
||||||
\nImportuji data účtu</string>
|
\nImportuji data účtu</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">Odesílám zprávu…</string>
|
<string name="event_status_sending_message">Odesílám zprávu…</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account_invited_rooms">Úvodní synchronizace:
|
<string name="initial_sync_start_importing_account_invited_rooms">Úvodní synchronizace:
|
||||||
\nImportuji pozvání</string>
|
\nImportuji pozvání</string>
|
||||||
<string name="clear_timeline_send_queue">Vymazat frontu neodeslaných zpráv</string>
|
<string name="clear_timeline_send_queue">Vymazat frontu neodeslaných zpráv</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_with_reason">%1$s pozvali %2$s. Důvod: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s pozvali %2$s. Důvod: %3$s</string>
|
||||||
<string name="notice_room_invite_you_with_reason">%1$s vás pozvali. Důvod: %2$s</string>
|
<string name="notice_room_invite_you_with_reason">%1$s vás pozvali. Důvod: %2$s</string>
|
||||||
<string name="notice_room_leave_with_reason">%1$s opustil místnost. Důvod: %2$s</string>
|
<string name="notice_room_leave_with_reason">%1$s opustil místnost. Důvod: %2$s</string>
|
||||||
|
@ -107,7 +88,6 @@
|
||||||
<string name="notice_event_redacted_by">Zprávu odstranil/a %1$s</string>
|
<string name="notice_event_redacted_by">Zprávu odstranil/a %1$s</string>
|
||||||
<string name="summary_you_sent_image">Poslali jste obrázek.</string>
|
<string name="summary_you_sent_image">Poslali jste obrázek.</string>
|
||||||
<string name="summary_you_sent_sticker">Poslali jste nálepku.</string>
|
<string name="summary_you_sent_sticker">Poslali jste nálepku.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_by_you">Vaše pozvání</string>
|
<string name="notice_room_invite_no_invitee_by_you">Vaše pozvání</string>
|
||||||
<string name="notice_room_created">%1$s založil místnost</string>
|
<string name="notice_room_created">%1$s založil místnost</string>
|
||||||
<string name="notice_room_created_by_you">Vy jste založili místnost</string>
|
<string name="notice_room_created_by_you">Vy jste založili místnost</string>
|
||||||
|
@ -136,7 +116,6 @@
|
||||||
<string name="notice_made_future_room_visibility_by_you">Učinili jste budoucí historii místnosti viditelnou pro %1$s</string>
|
<string name="notice_made_future_room_visibility_by_you">Učinili jste budoucí historii místnosti viditelnou pro %1$s</string>
|
||||||
<string name="notice_end_to_end_by_you">Zapnuli jste end-to-end šifrování (%1$s)</string>
|
<string name="notice_end_to_end_by_you">Zapnuli jste end-to-end šifrování (%1$s)</string>
|
||||||
<string name="notice_room_update_by_you">Povýšili jste tuto místnost.</string>
|
<string name="notice_room_update_by_you">Povýšili jste tuto místnost.</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference_by_you">Požádali jste o VoIP konferenci</string>
|
<string name="notice_requested_voip_conference_by_you">Požádali jste o VoIP konferenci</string>
|
||||||
<string name="notice_room_name_removed_by_you">Odstranili jste jméno místnosti</string>
|
<string name="notice_room_name_removed_by_you">Odstranili jste jméno místnosti</string>
|
||||||
<string name="notice_room_topic_removed_by_you">Odstranili jste téma místnosti</string>
|
<string name="notice_room_topic_removed_by_you">Odstranili jste téma místnosti</string>
|
||||||
|
@ -146,24 +125,20 @@
|
||||||
<string name="notice_room_third_party_invite_by_you">Poslali jste %1$s pozvání ke vstupu do místnosti</string>
|
<string name="notice_room_third_party_invite_by_you">Poslali jste %1$s pozvání ke vstupu do místnosti</string>
|
||||||
<string name="notice_room_third_party_revoked_invite_by_you">Zrušili jste pozvánku ke vstupu do místnosti pro %1$s</string>
|
<string name="notice_room_third_party_revoked_invite_by_you">Zrušili jste pozvánku ke vstupu do místnosti pro %1$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_by_you">Přijali jste pozvání pro %1$s</string>
|
<string name="notice_room_third_party_registered_invite_by_you">Přijali jste pozvání pro %1$s</string>
|
||||||
|
|
||||||
<string name="notice_widget_added">%1$s přidali widget %2$s</string>
|
<string name="notice_widget_added">%1$s přidali widget %2$s</string>
|
||||||
<string name="notice_widget_added_by_you">Přidali jste widget %1$s</string>
|
<string name="notice_widget_added_by_you">Přidali jste widget %1$s</string>
|
||||||
<string name="notice_widget_removed">%1$s odstranili widget %2$s</string>
|
<string name="notice_widget_removed">%1$s odstranili widget %2$s</string>
|
||||||
<string name="notice_widget_removed_by_you">Odstranili jste widget %1$s</string>
|
<string name="notice_widget_removed_by_you">Odstranili jste widget %1$s</string>
|
||||||
<string name="notice_widget_modified">%1$s změnil widget %2$s</string>
|
<string name="notice_widget_modified">%1$s změnil widget %2$s</string>
|
||||||
<string name="notice_widget_modified_by_you">Změnili jste widget %1$s</string>
|
<string name="notice_widget_modified_by_you">Změnili jste widget %1$s</string>
|
||||||
|
|
||||||
<string name="power_level_admin">Správce</string>
|
<string name="power_level_admin">Správce</string>
|
||||||
<string name="power_level_moderator">Moderátor</string>
|
<string name="power_level_moderator">Moderátor</string>
|
||||||
<string name="power_level_default">Výchozí</string>
|
<string name="power_level_default">Výchozí</string>
|
||||||
<string name="power_level_custom">Vlastní (%1$d)</string>
|
<string name="power_level_custom">Vlastní (%1$d)</string>
|
||||||
<string name="power_level_custom_no_value">Vlastní</string>
|
<string name="power_level_custom_no_value">Vlastní</string>
|
||||||
|
|
||||||
<string name="notice_power_level_changed_by_you">Změnili jste %1$s stupeň oprávnění.</string>
|
<string name="notice_power_level_changed_by_you">Změnili jste %1$s stupeň oprávnění.</string>
|
||||||
<string name="notice_power_level_changed">%1$s změnili %2$s stupeň oprávnění.</string>
|
<string name="notice_power_level_changed">%1$s změnili %2$s stupeň oprávnění.</string>
|
||||||
<string name="notice_power_level_diff">%1$s z %2$s na %3$s</string>
|
<string name="notice_power_level_diff">%1$s z %2$s na %3$s</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">Pozvání od %1$s. Důvod: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">Pozvání od %1$s. Důvod: %2$s</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Vaše pozvání. Důvod: %1$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">Vaše pozvání. Důvod: %1$s</string>
|
||||||
<string name="notice_room_invite_with_reason_by_you">Pozvali jste %1$s. Důvod: %2$s</string>
|
<string name="notice_room_invite_with_reason_by_you">Pozvali jste %1$s. Důvod: %2$s</string>
|
||||||
|
@ -186,49 +161,39 @@
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Přijali jste pozvání pro %1$s. Důvod: %2$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Přijali jste pozvání pro %1$s. Důvod: %2$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s zrušili pozvání pro %2$s. Důvod: %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s zrušili pozvání pro %2$s. Důvod: %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason_by_you">Zrušili jste pozvání od %1$s. Důvod: %2$s</string>
|
<string name="notice_room_withdraw_with_reason_by_you">Zrušili jste pozvání od %1$s. Důvod: %2$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s přidali %2$s jako adresu pro tuto místnost.</item>
|
<item quantity="one">%1$s přidali %2$s jako adresu pro tuto místnost.</item>
|
||||||
<item quantity="few">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
<item quantity="few">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
||||||
<item quantity="other">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
<item quantity="other">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added_by_you">
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
<item quantity="one">Přidali jste %1$s jako adresu pro tuto místnost.</item>
|
<item quantity="one">Přidali jste %1$s jako adresu pro tuto místnost.</item>
|
||||||
<item quantity="few">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
<item quantity="few">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
||||||
<item quantity="other">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
<item quantity="other">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s odstranili %2$s jako adresu pro tuto místnost.</item>
|
<item quantity="one">%1$s odstranili %2$s jako adresu pro tuto místnost.</item>
|
||||||
<item quantity="few">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
<item quantity="few">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
||||||
<item quantity="other">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
<item quantity="other">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Odstranili jste %2$s jako adresu pro tuto místnost.</item>
|
<item quantity="one">Odstranili jste %1$s jako adresu pro tuto místnost.</item>
|
||||||
<item quantity="few">Odstranili jste %2$s jako adresuy pro tuto místnost.</item>
|
<item quantity="few">Odstranili jste %1$s jako adresuy pro tuto místnost.</item>
|
||||||
<item quantity="other">Odstranili jste %2$s jako adresy pro tuto místnost.</item>
|
<item quantity="other">Odstranili jste %1$s jako adresy pro tuto místnost.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="notice_room_aliases_added_and_removed">%1$s přidali %2$s a odstranili %3$s jako adresy pro tuto místnost.</string>
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s přidali %2$ a odstranili %3$s jako adresy pro tuto místnost.</string>
|
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Přidali jste %1$s a odstranili %2$s jako adresy pro tuto místnost.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Přidali jste %1$s a odstranili %2$s jako adresy pro tuto místnost.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set">%1$s nastavili hlavní adresu této místnosti na %2$s.</string>
|
<string name="notice_room_canonical_alias_set">%1$s nastavili hlavní adresu této místnosti na %2$s.</string>
|
||||||
<string name="notice_room_canonical_alias_set_by_you">Nastavili jste %1$s na hlavní adresu této místnosti.</string>
|
<string name="notice_room_canonical_alias_set_by_you">Nastavili jste %1$s na hlavní adresu této místnosti.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s odstranili hlavní adresu této místnosti.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s odstranili hlavní adresu této místnosti.</string>
|
||||||
<string name="notice_room_canonical_alias_unset_by_you">Odstranili jste hlavní adresu této místnosti.</string>
|
<string name="notice_room_canonical_alias_unset_by_you">Odstranili jste hlavní adresu této místnosti.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join">"%1$s povolili hostům vstoupit do místnosti."</string>
|
<string name="notice_room_guest_access_can_join">"%1$s povolili hostům vstoupit do místnosti."</string>
|
||||||
<string name="notice_room_guest_access_can_join_by_you">Povolili jste hostům vstoupit do místnosti.</string>
|
<string name="notice_room_guest_access_can_join_by_you">Povolili jste hostům vstoupit do místnosti.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s zamezili hostům vstoupit do místnosti.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s zamezili hostům vstoupit do místnosti.</string>
|
||||||
<string name="notice_room_guest_access_forbidden_by_you">Zamezili jste hostům vstoupit do místnosti.</string>
|
<string name="notice_room_guest_access_forbidden_by_you">Zamezili jste hostům vstoupit do místnosti.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s zapnuli end-to-end šifrování.</string>
|
<string name="notice_end_to_end_ok">%1$s zapnuli end-to-end šifrování.</string>
|
||||||
<string name="notice_end_to_end_ok_by_you">Zapnuli jste end-to-end šifrování.</string>
|
<string name="notice_end_to_end_ok_by_you">Zapnuli jste end-to-end šifrování.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s zapnuli end-to-end šifrování (neznámý algoritmus %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s zapnuli end-to-end šifrování (neznámý algoritmus %2$s).</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Zapnuli jste end-to-end šifrování (neznámý algoritmus %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm_by_you">Zapnuli jste end-to-end šifrování (neznámý algoritmus %1$s).</string>
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s žádá ověření Vašeho klíče, ale Váš klient nepodporuje ověření klíče v chatu. Budete muset k ověření klíčů použít zastaralý způsob ověření.</string>
|
<string name="key_verification_request_fallback_message">%s žádá ověření Vašeho klíče, ale Váš klient nepodporuje ověření klíče v chatu. Budete muset k ověření klíčů použít zastaralý způsob ověření.</string>
|
||||||
|
</resources>
|
||||||
</resources>
|
|
|
@ -73,14 +73,22 @@
|
||||||
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
|
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
|
||||||
<string name="event_status_sending_message">Sende eine Nachricht…</string>
|
<string name="event_status_sending_message">Sende eine Nachricht…</string>
|
||||||
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
|
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
|
||||||
<string name="initial_sync_start_importing_account">Erste Synchronisation: Importiere Benutzerkonto…</string>
|
<string name="initial_sync_start_importing_account">Erste Synchronisation:
|
||||||
<string name="initial_sync_start_importing_account_crypto">Erste Synchronisation: Importiere Cryptoschlüssel</string>
|
\nImportiere Benutzerkonto…</string>
|
||||||
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation: Importiere Räume</string>
|
<string name="initial_sync_start_importing_account_crypto">Erste Synchronisation:
|
||||||
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation: Importiere betretene Räume</string>
|
\nImportiere Cryptoschlüssel</string>
|
||||||
<string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation: Importiere eingeladene Räume</string>
|
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation:
|
||||||
<string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation: Importiere verlassene Räume</string>
|
\nImportiere Räume</string>
|
||||||
<string name="initial_sync_start_importing_account_groups">Erste Synchronisation: Importiere Gemeinschaften</string>
|
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation:
|
||||||
<string name="initial_sync_start_importing_account_data">Erste Synchronisation: Importiere Benutzerdaten</string>
|
\nImportiere betretene Räume</string>
|
||||||
|
<string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation:
|
||||||
|
\nImportiere eingeladene Räume</string>
|
||||||
|
<string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation:
|
||||||
|
\nImportiere verlassene Räume</string>
|
||||||
|
<string name="initial_sync_start_importing_account_groups">Erste Synchronisation:
|
||||||
|
\nImportiere Communities</string>
|
||||||
|
<string name="initial_sync_start_importing_account_data">Erste Synchronisation:
|
||||||
|
\nImportiere Benutzerdaten</string>
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string>
|
||||||
|
@ -107,7 +115,7 @@
|
||||||
<string name="notice_room_canonical_alias_set">%1$s legt die Hauptadresse fest für diesen Raum als %2$s fest.</string>
|
<string name="notice_room_canonical_alias_set">%1$s legt die Hauptadresse fest für diesen Raum als %2$s fest.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s entfernt die Hauptadresse für diesen Raum.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s entfernt die Hauptadresse für diesen Raum.</string>
|
||||||
<string name="notice_room_guest_access_can_join">%1$s hat Gästen erlaubt den Raum zu betreten.</string>
|
<string name="notice_room_guest_access_can_join">%1$s hat Gästen erlaubt den Raum zu betreten.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s hat Gäste unterbunden den Raum zu betreten.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s hat Gästen untersagt den Raum zu betreten.</string>
|
||||||
<string name="notice_end_to_end_ok">%1$s aktivierte Ende-zu-Ende-Verschlüsselung.</string>
|
<string name="notice_end_to_end_ok">%1$s aktivierte Ende-zu-Ende-Verschlüsselung.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s).</string>
|
||||||
<string name="key_verification_request_fallback_message">%s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen.</string>
|
<string name="key_verification_request_fallback_message">%s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen.</string>
|
||||||
|
@ -199,7 +207,7 @@
|
||||||
<string name="notice_direct_room_third_party_revoked_invite">%1$s hat die Einladung für %2$s zurückgezogen</string>
|
<string name="notice_direct_room_third_party_revoked_invite">%1$s hat die Einladung für %2$s zurückgezogen</string>
|
||||||
<string name="notice_direct_room_third_party_invite_by_you">Du hast %1$s eingeladen</string>
|
<string name="notice_direct_room_third_party_invite_by_you">Du hast %1$s eingeladen</string>
|
||||||
<string name="notice_direct_room_third_party_invite">%1$s hat %2$s eingeladen</string>
|
<string name="notice_direct_room_third_party_invite">%1$s hat %2$s eingeladen</string>
|
||||||
<string name="notice_made_future_direct_room_visibility_by_you">Du hast zukünftige Nachrichten für %2$s sichtbar gemacht</string>
|
<string name="notice_made_future_direct_room_visibility_by_you">Du hast zukünftige Nachrichten für %1$s sichtbar gemacht</string>
|
||||||
<string name="notice_made_future_direct_room_visibility">%1$s hat zukünftige Nachrichten für %2$s sichtbar gemacht</string>
|
<string name="notice_made_future_direct_room_visibility">%1$s hat zukünftige Nachrichten für %2$s sichtbar gemacht</string>
|
||||||
<string name="notice_direct_room_leave_by_you">Du hast den Raum verlassen</string>
|
<string name="notice_direct_room_leave_by_you">Du hast den Raum verlassen</string>
|
||||||
<string name="notice_direct_room_leave">%1$s hat den Raum verlassen</string>
|
<string name="notice_direct_room_leave">%1$s hat den Raum verlassen</string>
|
||||||
|
@ -207,4 +215,10 @@
|
||||||
<string name="notice_direct_room_join">%1$s ist beigetreten</string>
|
<string name="notice_direct_room_join">%1$s ist beigetreten</string>
|
||||||
<string name="notice_direct_room_created_by_you">Du hast eine Diskussion erstellt</string>
|
<string name="notice_direct_room_created_by_you">Du hast eine Diskussion erstellt</string>
|
||||||
<string name="notice_direct_room_created">%1$s hat eine Diskussion erstellt</string>
|
<string name="notice_direct_room_created">%1$s hat eine Diskussion erstellt</string>
|
||||||
|
<string name="notice_direct_room_update">%s hat hier ein Upgrade durchgeführt.</string>
|
||||||
|
<string name="notice_direct_room_update_by_you">Du hast hier ein Upgrade durchgeführt.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">Du hast Gästen untersagt den Raum zu betreten.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden">%1$s hat Gästen untersagt den Raum zu betreten.</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason_by_you">Du bist gegangen. Grund: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason">%1$s ist gegangen. Grund: %2$s</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,26 +1,24 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_user_sent_image">%1$s sendis bildon.</string>
|
<string name="summary_user_sent_image">%1$s sendis bildon.</string>
|
||||||
<string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string>
|
<string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">Invito de %s</string>
|
<string name="notice_room_invite_no_invitee">Invito de %s</string>
|
||||||
<string name="notice_room_invite">%1$s invitis uzanton %2$s</string>
|
<string name="notice_room_invite">%1$s invitis uzanton %2$s</string>
|
||||||
<string name="notice_room_invite_you">%1$s invitis vin</string>
|
<string name="notice_room_invite_you">%1$s invitis vin</string>
|
||||||
<string name="notice_room_join">%1$s alvenis</string>
|
<string name="notice_room_join">%1$s envenis</string>
|
||||||
<string name="notice_room_leave">%1$s foriris</string>
|
<string name="notice_room_leave">%1$s foriris de la ĉambro</string>
|
||||||
<string name="notice_room_reject">%1$s malakceptis la inviton</string>
|
<string name="notice_room_reject">%1$s rifuzis la inviton</string>
|
||||||
<string name="notice_room_kick">%1$s forpelis uzanton %2$s</string>
|
<string name="notice_room_kick">%1$s forpelis uzanton %2$s</string>
|
||||||
<string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string>
|
<string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string>
|
||||||
<string name="notice_room_ban">%1$s forbaris uzanton %2$s</string>
|
<string name="notice_room_ban">%1$s forbaris uzanton %2$s</string>
|
||||||
<string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string>
|
<string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string>
|
||||||
<string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string>
|
<string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string>
|
||||||
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendinto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
|
||||||
|
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string>
|
<string name="notice_display_name_set">%1$s ŝanĝis sian prezentan nomon al %2$s</string>
|
||||||
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string>
|
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian prezentan nomon de %2$s al %3$s</string>
|
||||||
<string name="notice_display_name_removed">%1$s forigis sian vidigan nomon (%2$s)</string>
|
<string name="notice_display_name_removed">%1$s forigis sian prezentan nomon (%2$s)</string>
|
||||||
<string name="notice_room_topic_changed">%1$s ŝanĝis la temon al: %2$s</string>
|
<string name="notice_room_topic_changed">%1$s ŝanĝis la temon al: %2$s</string>
|
||||||
<string name="notice_room_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</string>
|
<string name="notice_room_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</string>
|
||||||
<string name="notice_placed_video_call">%s vidvokis.</string>
|
<string name="notice_placed_video_call">%s vidvokis.</string>
|
||||||
|
@ -28,50 +26,38 @@
|
||||||
<string name="notice_answered_call">%s respondis la vokon.</string>
|
<string name="notice_answered_call">%s respondis la vokon.</string>
|
||||||
<string name="notice_ended_call">%s finis la vokon.</string>
|
<string name="notice_ended_call">%s finis la vokon.</string>
|
||||||
<string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string>
|
<string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string>
|
||||||
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string>
|
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde siaj invitoj.</string>
|
||||||
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliĝoj.</string>
|
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde siaj aliĝoj.</string>
|
||||||
<string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string>
|
<string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string>
|
||||||
<string name="notice_room_visibility_world_readable">ĉiu ajn.</string>
|
<string name="notice_room_visibility_world_readable">ĉiu ajn.</string>
|
||||||
<string name="notice_room_visibility_unknown">nekonata (%s).</string>
|
<string name="notice_room_visibility_unknown">nekonata (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s ŝaltis tutvojan ĉifradon (%2$s)</string>
|
<string name="notice_end_to_end">%1$s ŝaltis tutvojan ĉifradon (%2$s)</string>
|
||||||
<string name="notice_room_update">%s gradaltigis la ĉambron.</string>
|
<string name="notice_room_update">%s gradaltigis la ĉambron.</string>
|
||||||
|
|
||||||
<string name="notice_event_redacted">Mesaĝo foriĝis</string>
|
<string name="notice_event_redacted">Mesaĝo foriĝis</string>
|
||||||
<string name="notice_event_redacted_by">Mesaĝo foriĝis de %1$s</string>
|
<string name="notice_event_redacted_by">Mesaĝon forigis %1$s</string>
|
||||||
<string name="notice_event_redacted_with_reason">Mesaĝo foriĝis [kialo: %1$s]</string>
|
<string name="notice_event_redacted_with_reason">Mesaĝo foriĝis [kialo: %1$s]</string>
|
||||||
<string name="notice_event_redacted_by_with_reason">Mesaĝo foriĝis de %1$s [kialo: %2$s]</string>
|
<string name="notice_event_redacted_by_with_reason">Mesaĝon forigis %1$s [kialo: %2$s]</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s ĝisdatigis sian profilon %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s ĝisdatigis sian profilon %2$s</string>
|
||||||
<string name="notice_room_third_party_invite">%1$s sendis aliĝan inviton al %2$s</string>
|
<string name="notice_room_third_party_invite">%1$s sendis aliĝan inviton al %2$s</string>
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliĝan inviton por %2$s</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliĝan inviton por %2$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string>
|
<string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string>
|
||||||
|
|
||||||
<string name="could_not_redact">Ne povis redakti</string>
|
<string name="could_not_redact">Ne povis redakti</string>
|
||||||
<string name="unable_to_send_message">Ne povas sendi mesaĝon</string>
|
<string name="unable_to_send_message">Ne povas sendi mesaĝon</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">Malsukcesis alŝuti bildon</string>
|
<string name="message_failed_to_upload">Malsukcesis alŝuti bildon</string>
|
||||||
|
|
||||||
<string name="network_error">Reta eraro</string>
|
<string name="network_error">Reta eraro</string>
|
||||||
<string name="matrix_error">Matrix-eraro</string>
|
<string name="matrix_error">Matrix-eraro</string>
|
||||||
|
<string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro.</string>
|
||||||
<string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro</string>
|
|
||||||
|
|
||||||
<string name="encrypted_message">Ĉifrita mesaĝo</string>
|
<string name="encrypted_message">Ĉifrita mesaĝo</string>
|
||||||
|
|
||||||
<string name="medium_email">Retpoŝtadreso</string>
|
<string name="medium_email">Retpoŝtadreso</string>
|
||||||
<string name="medium_phone_number">Telefonnumero</string>
|
<string name="medium_phone_number">Telefonnumero</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">Invito de %s</string>
|
<string name="room_displayname_invite_from">Invito de %s</string>
|
||||||
<string name="room_displayname_room_invite">Ĉambra invito</string>
|
<string name="room_displayname_room_invite">Invito al ĉambro</string>
|
||||||
|
|
||||||
<string name="room_displayname_two_members">%1$s kaj %2$s</string>
|
<string name="room_displayname_two_members">%1$s kaj %2$s</string>
|
||||||
|
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s kaj 1 alia</item>
|
<item quantity="one">%1$s kaj 1 alia</item>
|
||||||
<item quantity="other">%1$s kaj %2$d aliaj</item>
|
<item quantity="other">%1$s kaj %2$d aliaj</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="room_displayname_empty_room">Malplena ĉambro</string>
|
<string name="room_displayname_empty_room">Malplena ĉambro</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account">Komenca spegulado:
|
<string name="initial_sync_start_importing_account">Komenca spegulado:
|
||||||
\nEnportante konton…</string>
|
\nEnportante konton…</string>
|
||||||
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:
|
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:
|
||||||
|
@ -88,52 +74,143 @@
|
||||||
\nEnportante komunumojn</string>
|
\nEnportante komunumojn</string>
|
||||||
<string name="initial_sync_start_importing_account_data">Komenca spegulado:
|
<string name="initial_sync_start_importing_account_data">Komenca spegulado:
|
||||||
\nEnportante datumojn de konto</string>
|
\nEnportante datumojn de konto</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">Sendante mesaĝon…</string>
|
<string name="event_status_sending_message">Sendante mesaĝon…</string>
|
||||||
<string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string>
|
<string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s petis grupan vokon</string>
|
<string name="notice_requested_voip_conference">%1$s petis grupan vokon</string>
|
||||||
<string name="notice_voip_started">Grupa voko komenciĝis</string>
|
<string name="notice_voip_started">Grupa voko komenciĝis</string>
|
||||||
<string name="notice_voip_finished">Grupa voko finiĝis</string>
|
<string name="notice_voip_finished">Grupa voko finiĝis</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(ankaŭ profilbildo ŝanĝiĝis)</string>
|
<string name="notice_avatar_changed_too">(ankaŭ profilbildo ŝanĝiĝis)</string>
|
||||||
<string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string>
|
<string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string>
|
||||||
<string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string>
|
<string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string>
|
<string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string>
|
||||||
<string name="notice_room_join_with_reason">%1$s aliĝis al la ĉambro. Kialo: %2$s</string>
|
<string name="notice_room_join_with_reason">%1$s envenis. Kialo: %2$s</string>
|
||||||
<string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string>
|
<string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string>
|
||||||
<string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string>
|
<string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string>
|
||||||
<string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string>
|
<string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string>
|
<string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string>
|
<string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_third_party_invite_with_reason">%1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s</string>
|
<string name="notice_room_third_party_invite_with_reason">%1$s sendis al %2$s inviton al la ĉambro. Kialo: %3$s</string>
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro por %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton al %2$s. Kialo: %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton por %2$s. Kialo: %3$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
<item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||||
<item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
<item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
<item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||||
<item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
<item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string>
|
||||||
|
<string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson de ĉi tiu ĉambro al %2$s.</string>
|
||||||
<string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</string>
|
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string>
|
||||||
|
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj enveni.</string>
|
||||||
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj aliĝi al la ĉambro.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj enveni.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi al la ĉambro.</string>
|
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s ŝaltis tutvojan ĉifradon.</string>
|
<string name="notice_end_to_end_ok">%1$s ŝaltis tutvojan ĉifradon.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string>
|
||||||
|
<string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovan kontrolmanieron de ŝlosiloj.</string>
|
||||||
<string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj.</string>
|
<string name="notice_power_level_changed_by_you">Vi ŝanĝis la povnivelon de %1$s.</string>
|
||||||
|
<string name="notice_power_level_changed">%1$s sanĝis la povnivelon de %2$s.</string>
|
||||||
</resources>
|
<string name="notice_end_to_end_unknown_algorithm_by_you">Vi ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %1$s).</string>
|
||||||
|
<string name="notice_end_to_end_ok_by_you">Vi ŝaltis tutvojan ĉifradon.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">Vi malpermesis al gastoj aliĝi.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi.</string>
|
||||||
|
<string name="notice_room_guest_access_forbidden_by_you">Vi malpermesis al gastoj enveni.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join_by_you">Vi permesis al gastoj aliĝi.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join">%1$s permesis al gastoj aliĝi.</string>
|
||||||
|
<string name="notice_room_guest_access_can_join_by_you">Vi permesis al gastoj enveni.</string>
|
||||||
|
<string name="notice_room_canonical_alias_unset_by_you">Vi forigis la ĉefadreson de ĉi tiu ĉambro.</string>
|
||||||
|
<string name="notice_room_canonical_alias_set_by_you">Vi agordis al ĉefadreson de ĉi tiu ĉambro al %1$s.</string>
|
||||||
|
<string name="notice_room_aliases_added_and_removed_by_you">Vi aldonis %1$s kaj forigis %2$s kiel adresojn por ĉi tiu ĉambro.</string>
|
||||||
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
|
<item quantity="one">Vi forigis %1$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||||
|
<item quantity="other">Vi forigis %1$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
|
<item quantity="one">Vi aldonis %1$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||||
|
<item quantity="other">Vi aldonis %1$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="notice_room_withdraw_with_reason_by_you">Vi nuligis la inviton por %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Vi akceptis la inviton por %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Vi nuligis inviton al la ĉambro por %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_with_reason_by_you">Vi sendis al %1$s inviton al la ĉambro. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_ban_with_reason_by_you">Vi forbaris uzanton %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_unban_with_reason_by_you">Vi malforbaris uzanton %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_kick_with_reason_by_you">Vi forpelis uzanton %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_reject_with_reason_by_you">Vi rifuzis la inviton. Kialo: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason_by_you">Vi foriris. Kialo: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason">%1$s foriris. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_leave_with_reason_by_you">Vi foriris de la ĉambro. Kialo: %1$s</string>
|
||||||
|
<string name="notice_room_join_with_reason_by_you">Vi envenis. Kialo: %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason_by_you">Vi aliĝis. Kialo: %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason">%1$s aliĝis. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_invite_with_reason_by_you">Vi invitis uzanton %1$s. Kialo: %2$s</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">Via invito. Kialo: %1$s</string>
|
||||||
|
<string name="notice_power_level_diff">%1$s de %2$s al %3$s</string>
|
||||||
|
<string name="power_level_custom_no_value">Propra</string>
|
||||||
|
<string name="power_level_default">Ordinara</string>
|
||||||
|
<string name="power_level_custom">Propra (%1$d)</string>
|
||||||
|
<string name="power_level_moderator">Reguligisto</string>
|
||||||
|
<string name="power_level_admin">Administranto</string>
|
||||||
|
<string name="notice_widget_modified_by_you">Vi ŝanĝis la fenestraĵon %1$s</string>
|
||||||
|
<string name="notice_widget_modified">%1$s ŝanĝis la fenestraĵon %2$s</string>
|
||||||
|
<string name="notice_widget_removed_by_you">Vi forigis la fenestraĵon %1$s</string>
|
||||||
|
<string name="notice_widget_removed">%1$s forigis la fenestraĵon %2$s</string>
|
||||||
|
<string name="notice_widget_added_by_you">Vi aldonis la fenestraĵon %1$s</string>
|
||||||
|
<string name="notice_widget_added">%1$s aldonis la fenestraĵon %2$s</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_by_you">Vi akceptis la inviton por %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite_by_you">Vi nuligis la inviton por %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite">%1$s nuligis la inviton por %2$s</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_by_you">Vi nuligis la aliĝan inviton por %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite_by_you">Vi invitis uzanton %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite">%1$s invitis uzanton %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_by_you">Vi sendis aliĝan inviton al %1$s</string>
|
||||||
|
<string name="notice_profile_change_redacted_by_you">Vi ĝisdatigis vian profilon %1$s</string>
|
||||||
|
<string name="notice_room_avatar_removed_by_you">Vi forigis bildon de la ĉambro</string>
|
||||||
|
<string name="notice_room_avatar_removed">%1$s forigis bildon de la ĉambro</string>
|
||||||
|
<string name="notice_room_topic_removed_by_you">Vi forigis temon de la ĉambro</string>
|
||||||
|
<string name="notice_room_name_removed_by_you">Vi forigis nomon de la ĉambro</string>
|
||||||
|
<string name="notice_requested_voip_conference_by_you">Vi petis grupan vokon</string>
|
||||||
|
<string name="notice_direct_room_update_by_you">Vi gradaltigis la interparolon.</string>
|
||||||
|
<string name="notice_direct_room_update">%s gradaltigis la interparolon.</string>
|
||||||
|
<string name="notice_room_update_by_you">Vi gradaltigis la ĉambron.</string>
|
||||||
|
<string name="notice_end_to_end_by_you">Vi ŝaltis tutvojan ĉifradon (%1$s)</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility">%1$s videbligis al %2$s estontajn mesaĝojn</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility_by_you">Vi videbligis al %1$s estontajn mesaĝojn</string>
|
||||||
|
<string name="notice_made_future_room_visibility_by_you">Vi videbligis estontan historion de ĉambro al %1$s</string>
|
||||||
|
<string name="notice_ended_call_by_you">Vi finis la vokon.</string>
|
||||||
|
<string name="notice_answered_call_by_you">Vi respondis la vokon.</string>
|
||||||
|
<string name="notice_call_candidates_by_you">Vi sendis datumojn por prepari la vokon.</string>
|
||||||
|
<string name="notice_call_candidates">%s sendis datumojn por prepari la vokon.</string>
|
||||||
|
<string name="notice_placed_voice_call_by_you">Vi voĉvokis.</string>
|
||||||
|
<string name="notice_placed_video_call_by_you">Vi vidvokis.</string>
|
||||||
|
<string name="notice_room_name_changed_by_you">Vi ŝanĝis la nomon de la ĉambro al: %1$s</string>
|
||||||
|
<string name="notice_room_avatar_changed_by_you">Vi ŝanĝis la bildon de la ĉambro</string>
|
||||||
|
<string name="notice_room_avatar_changed">%1$s ŝanĝis la bildon de la ĉambro</string>
|
||||||
|
<string name="notice_room_topic_changed_by_you">Vi ŝanĝis la temon al: %1$s</string>
|
||||||
|
<string name="notice_display_name_removed_by_you">Vi forigis vian prezentan nomon (%1$s)</string>
|
||||||
|
<string name="notice_display_name_changed_from_by_you">Vi ŝanĝis vian prezentan nomon de %1$s al %2$s</string>
|
||||||
|
<string name="notice_display_name_set_by_you">Vi ŝanĝis vian prezentan nomon al %1$s</string>
|
||||||
|
<string name="notice_avatar_url_changed_by_you">Vi ŝanĝis vian profilbildon</string>
|
||||||
|
<string name="notice_room_withdraw_by_you">Vi nuligis inviton por %1$s</string>
|
||||||
|
<string name="notice_room_ban_by_you">Vi forbaris uzanton %1$s</string>
|
||||||
|
<string name="notice_room_unban_by_you">Vi malforbaris uzanton %1$s</string>
|
||||||
|
<string name="notice_room_kick_by_you">Vi forpelis uzanton %1$s</string>
|
||||||
|
<string name="notice_room_reject_by_you">Vi rifuzis la inviton</string>
|
||||||
|
<string name="notice_direct_room_leave_by_you">Vi foriris de la ĉambro</string>
|
||||||
|
<string name="notice_direct_room_leave">%1$s foriris de la ĉambro</string>
|
||||||
|
<string name="notice_room_leave_by_you">Vi foriris de la ĉambro</string>
|
||||||
|
<string name="notice_direct_room_join_by_you">Vi envenis</string>
|
||||||
|
<string name="notice_direct_room_join">%1$s envenis</string>
|
||||||
|
<string name="notice_room_join_by_you">Vi envenis</string>
|
||||||
|
<string name="notice_room_invite_by_you">Vi invitis uzanton %1$s</string>
|
||||||
|
<string name="notice_direct_room_created_by_you">Vi kreis la diskuton</string>
|
||||||
|
<string name="notice_direct_room_created">%1$s kreis la diskuton</string>
|
||||||
|
<string name="notice_room_created_by_you">Vi kreis la ĉambron</string>
|
||||||
|
<string name="notice_room_created">%1$s kreis la ĉambron</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_by_you">Via invito</string>
|
||||||
|
<string name="summary_you_sent_sticker">Vi sendis glumarkon.</string>
|
||||||
|
<string name="summary_you_sent_image">Vi sendis bildon.</string>
|
||||||
|
</resources>
|
|
@ -226,7 +226,7 @@
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Quitaste %1$s como dirección para esta sala.</item>
|
<item quantity="one">Quitaste %1$s como dirección para esta sala.</item>
|
||||||
<item quantity="other">Quitaste %2$s como direcciones para esta sala.</item>
|
<item quantity="other">Quitaste %1$s como direcciones para esta sala.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>
|
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="summary_user_sent_image">%1$s تصویری فرستاد.</string>
|
<string name="summary_user_sent_image">%1$s تصویری فرستاد.</string>
|
||||||
<string name="summary_user_sent_sticker">%1$s برچسبی فرستاد.</string>
|
<string name="summary_user_sent_sticker">%1$s برچسبی فرستاد.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">دعوت %s</string>
|
<string name="notice_room_invite_no_invitee">دعوت %s</string>
|
||||||
<string name="notice_room_invite">%1$s، %2$s را دعوت کرد</string>
|
<string name="notice_room_invite">%1$s، %2$s را دعوت کرد</string>
|
||||||
<string name="notice_room_invite_you">%1$s دعوتتان کرد</string>
|
<string name="notice_room_invite_you">%1$s دعوتتان کرد</string>
|
||||||
|
@ -32,11 +31,9 @@
|
||||||
<string name="notice_room_visibility_unknown">ناشناخته (%s).</string>
|
<string name="notice_room_visibility_unknown">ناشناخته (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)</string>
|
<string name="notice_end_to_end">%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)</string>
|
||||||
<string name="notice_room_update">%s این اتاق را ارتقا داد.</string>
|
<string name="notice_room_update">%s این اتاق را ارتقا داد.</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s درخواست یک گردهمایی صوتی داد</string>
|
<string name="notice_requested_voip_conference">%1$s درخواست یک گردهمایی صوتی داد</string>
|
||||||
<string name="notice_voip_started">گردهمایی صوتی آغاز شد</string>
|
<string name="notice_voip_started">گردهمایی صوتی آغاز شد</string>
|
||||||
<string name="notice_voip_finished">گردهمایی صوتی پایان یافت</string>
|
<string name="notice_voip_finished">گردهمایی صوتی پایان یافت</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(تصویر هم عوض شد)</string>
|
<string name="notice_avatar_changed_too">(تصویر هم عوض شد)</string>
|
||||||
<string name="notice_room_name_removed">%1$s نام اتاق را پاک کرد</string>
|
<string name="notice_room_name_removed">%1$s نام اتاق را پاک کرد</string>
|
||||||
<string name="notice_room_topic_removed">%1$s موضوع اتاق را پاک کرد</string>
|
<string name="notice_room_topic_removed">%1$s موضوع اتاق را پاک کرد</string>
|
||||||
|
@ -47,36 +44,24 @@
|
||||||
<string name="notice_room_third_party_invite">%1$s دعوتی برای پیوستن %2$s به اتاق فرستاد</string>
|
<string name="notice_room_third_party_invite">%1$s دعوتی برای پیوستن %2$s به اتاق فرستاد</string>
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s دعوت پیوستن به اتاق %2$s را باطل کرد</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s دعوت پیوستن به اتاق %2$s را باطل کرد</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s دعوت برای %2$s را پذیرفت</string>
|
<string name="notice_room_third_party_registered_invite">%1$s دعوت برای %2$s را پذیرفت</string>
|
||||||
|
|
||||||
<string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string>
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
|
||||||
|
|
||||||
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
|
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
|
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
|
||||||
|
|
||||||
<string name="network_error">خطای شبکه</string>
|
<string name="network_error">خطای شبکه</string>
|
||||||
<string name="matrix_error">خطای ماتریکس</string>
|
<string name="matrix_error">خطای ماتریکس</string>
|
||||||
|
|
||||||
<string name="room_error_join_failed_empty_room">در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد.</string>
|
<string name="room_error_join_failed_empty_room">در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد.</string>
|
||||||
|
|
||||||
<string name="encrypted_message">پیام رمزنگاری شده</string>
|
<string name="encrypted_message">پیام رمزنگاری شده</string>
|
||||||
|
|
||||||
<string name="medium_email">نشانی رایانامه</string>
|
<string name="medium_email">نشانی رایانامه</string>
|
||||||
<string name="medium_phone_number">شماره تلفن</string>
|
<string name="medium_phone_number">شماره تلفن</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">دعوت از %s</string>
|
<string name="room_displayname_invite_from">دعوت از %s</string>
|
||||||
<string name="room_displayname_room_invite">دعوت اتاق</string>
|
<string name="room_displayname_room_invite">دعوت اتاق</string>
|
||||||
|
|
||||||
<string name="room_displayname_two_members">%1$s و %2$s</string>
|
<string name="room_displayname_two_members">%1$s و %2$s</string>
|
||||||
|
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s و ۱ نفر دیگر</item>
|
<item quantity="one">%1$s و ۱ نفر دیگر</item>
|
||||||
<item quantity="other">%1$s و %2$d نفر دیگر</item>
|
<item quantity="other">%1$s و %2$d نفر دیگر</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="room_displayname_empty_room">اتاق خالی</string>
|
<string name="room_displayname_empty_room">اتاق خالی</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account">همگامسازی نخستین:
|
<string name="initial_sync_start_importing_account">همگامسازی نخستین:
|
||||||
\nدر حال درونریزی حساب…</string>
|
\nدر حال درونریزی حساب…</string>
|
||||||
<string name="initial_sync_start_importing_account_crypto">همگامسازی نخستین:
|
<string name="initial_sync_start_importing_account_crypto">همگامسازی نخستین:
|
||||||
|
@ -93,10 +78,8 @@
|
||||||
\nدر حال درونریزی انجمنها</string>
|
\nدر حال درونریزی انجمنها</string>
|
||||||
<string name="initial_sync_start_importing_account_data">همگامسازی نخستین:
|
<string name="initial_sync_start_importing_account_data">همگامسازی نخستین:
|
||||||
\nدر حال درونریزی دادههای حساب</string>
|
\nدر حال درونریزی دادههای حساب</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">در حال فرستادن پیام…</string>
|
<string name="event_status_sending_message">در حال فرستادن پیام…</string>
|
||||||
<string name="clear_timeline_send_queue">پاکسازی صفِ در حال ارسال</string>
|
<string name="clear_timeline_send_queue">پاکسازی صفِ در حال ارسال</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">دعوت %1$s. دلیل: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">دعوت %1$s. دلیل: %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s، %2$s را دعوت کرد. دلیل: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s، %2$s را دعوت کرد. دلیل: %3$s</string>
|
||||||
<string name="notice_room_invite_you_with_reason">%1$s دعوتتان کرد. دلیل: %2$s</string>
|
<string name="notice_room_invite_you_with_reason">%1$s دعوتتان کرد. دلیل: %2$s</string>
|
||||||
|
@ -110,36 +93,27 @@
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason">%1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s دعوت %2$s را نپذیرفت. دلیل: %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s دعوت %2$s را نپذیرفت. دلیل: %3$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق افزود.</item>
|
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق افزود.</item>
|
||||||
<item quantity="other">%1$s، %2$s را به عنوان نشانیهایی برای این اتاق افزود.</item>
|
<item quantity="other">%1$s، %2$s را به عنوان نشانیهایی برای این اتاق افزود.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق پاک کرد.</item>
|
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق پاک کرد.</item>
|
||||||
<item quantity="other">%1$s، %3$s را به عنوان نشانیهایی برای این اتاق پاک کرد.</item>
|
<item quantity="other">%1$s، %3$s را به عنوان نشانیهایی برای این اتاق پاک کرد.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set">%1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد.</string>
|
<string name="notice_room_canonical_alias_set">%1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s نشانی اصلی را برای این اتاق پاک کرد.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s نشانی اصلی را برای این اتاق پاک کرد.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join">%1$s اجازه داد میمهانان به گروه بپیوندند.</string>
|
<string name="notice_room_guest_access_can_join">%1$s اجازه داد میمهانان به گروه بپیوندند.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s جلوی پیوستن میمهانان به گروه را گرفت.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s جلوی پیوستن میمهانان به گروه را گرفت.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s رمزنگاری سرتاسری را روشن کرد.</string>
|
<string name="notice_end_to_end_ok">%1$s رمزنگاری سرتاسری را روشن کرد.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیصدادهنشده %2$s ).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیصدادهنشده %2$s ).</string>
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمیکند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید.</string>
|
<string name="key_verification_request_fallback_message">%s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمیکند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید.</string>
|
||||||
|
|
||||||
<string name="notice_room_created">%1$s اتاق را ایجاد کرد</string>
|
<string name="notice_room_created">%1$s اتاق را ایجاد کرد</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s نمایهاش را بهروز کرد %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s نمایهاش را بهروز کرد %2$s</string>
|
||||||
<string name="could_not_redact">نمیتوان ویرایش کرد</string>
|
<string name="could_not_redact">نمیتوان ویرایش کرد</string>
|
||||||
<string name="summary_you_sent_image">تصویری فرستادید.</string>
|
<string name="summary_you_sent_image">تصویری فرستادید.</string>
|
||||||
<string name="summary_you_sent_sticker">برچسبی فرستادید.</string>
|
<string name="summary_you_sent_sticker">برچسبی فرستادید.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_by_you">دعوتتان</string>
|
<string name="notice_room_invite_no_invitee_by_you">دعوتتان</string>
|
||||||
<string name="notice_room_created_by_you">اتاق را ایجاد کردید</string>
|
<string name="notice_room_created_by_you">اتاق را ایجاد کردید</string>
|
||||||
<string name="notice_room_invite_by_you">از %1$s دعوت کردید</string>
|
<string name="notice_room_invite_by_you">از %1$s دعوت کردید</string>
|
||||||
|
@ -167,7 +141,6 @@
|
||||||
<string name="notice_made_future_room_visibility_by_you">تاریخچهٔ آتی اتاق را برای %1$s نمایان کردید</string>
|
<string name="notice_made_future_room_visibility_by_you">تاریخچهٔ آتی اتاق را برای %1$s نمایان کردید</string>
|
||||||
<string name="notice_end_to_end_by_you">رمزنگاری سرتاسری را روشن کردید (%1$s)</string>
|
<string name="notice_end_to_end_by_you">رمزنگاری سرتاسری را روشن کردید (%1$s)</string>
|
||||||
<string name="notice_room_update_by_you">این اتاق را ارتقا دادید.</string>
|
<string name="notice_room_update_by_you">این اتاق را ارتقا دادید.</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference_by_you">دارخواست کنفرانس ویپ دادید</string>
|
<string name="notice_requested_voip_conference_by_you">دارخواست کنفرانس ویپ دادید</string>
|
||||||
<string name="notice_room_name_removed_by_you">نام اتاق را برداشتید</string>
|
<string name="notice_room_name_removed_by_you">نام اتاق را برداشتید</string>
|
||||||
<string name="notice_room_topic_removed_by_you">موضوع اتاق را برداشتید</string>
|
<string name="notice_room_topic_removed_by_you">موضوع اتاق را برداشتید</string>
|
||||||
|
@ -177,24 +150,20 @@
|
||||||
<string name="notice_room_third_party_invite_by_you">برای %1$s دعوت پیوستن به اتاق فرستادید</string>
|
<string name="notice_room_third_party_invite_by_you">برای %1$s دعوت پیوستن به اتاق فرستادید</string>
|
||||||
<string name="notice_room_third_party_revoked_invite_by_you">دعوت پیوستن %1$s به اتاق را پس گرفتید</string>
|
<string name="notice_room_third_party_revoked_invite_by_you">دعوت پیوستن %1$s به اتاق را پس گرفتید</string>
|
||||||
<string name="notice_room_third_party_registered_invite_by_you">دعوت برای %1$s را پذیرفتید</string>
|
<string name="notice_room_third_party_registered_invite_by_you">دعوت برای %1$s را پذیرفتید</string>
|
||||||
|
|
||||||
<string name="notice_widget_added">%1$s ابزارک %2$s را افزود</string>
|
<string name="notice_widget_added">%1$s ابزارک %2$s را افزود</string>
|
||||||
<string name="notice_widget_added_by_you">ابزارک %1$s را افزودید</string>
|
<string name="notice_widget_added_by_you">ابزارک %1$s را افزودید</string>
|
||||||
<string name="notice_widget_removed">%1$s ابزارک %2$s را برداشت</string>
|
<string name="notice_widget_removed">%1$s ابزارک %2$s را برداشت</string>
|
||||||
<string name="notice_widget_removed_by_you">ابزارک %1$s را برداشتید</string>
|
<string name="notice_widget_removed_by_you">ابزارک %1$s را برداشتید</string>
|
||||||
<string name="notice_widget_modified">%1$s ابزارک %2$s را دستکاری کرد</string>
|
<string name="notice_widget_modified">%1$s ابزارک %2$s را دستکاری کرد</string>
|
||||||
<string name="notice_widget_modified_by_you">ابزارک %1$s را دستکاری کردید</string>
|
<string name="notice_widget_modified_by_you">ابزارک %1$s را دستکاری کردید</string>
|
||||||
|
|
||||||
<string name="power_level_admin">مدیر</string>
|
<string name="power_level_admin">مدیر</string>
|
||||||
<string name="power_level_moderator">ناظم</string>
|
<string name="power_level_moderator">ناظم</string>
|
||||||
<string name="power_level_default">پیشگزیده</string>
|
<string name="power_level_default">پیشگزیده</string>
|
||||||
<string name="power_level_custom">سفارشی (%1$d)</string>
|
<string name="power_level_custom">سفارشی (%1$d)</string>
|
||||||
<string name="power_level_custom_no_value">سفارشی</string>
|
<string name="power_level_custom_no_value">سفارشی</string>
|
||||||
|
|
||||||
<string name="notice_power_level_changed_by_you">سطح قدرت %1$s را تغییر دادید.</string>
|
<string name="notice_power_level_changed_by_you">سطح قدرت %1$s را تغییر دادید.</string>
|
||||||
<string name="notice_power_level_changed">%1$s سطح قدرت %2$s را تغییر داد.</string>
|
<string name="notice_power_level_changed">%1$s سطح قدرت %2$s را تغییر داد.</string>
|
||||||
<string name="notice_power_level_diff">%1$s از %2$s به %3$s</string>
|
<string name="notice_power_level_diff">%1$s از %2$s به %3$s</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">دعوتتان. دلیل: %1$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">دعوتتان. دلیل: %1$s</string>
|
||||||
<string name="notice_room_invite_with_reason_by_you">%1$s را دعوت کردید. دلیل: %2$s</string>
|
<string name="notice_room_invite_with_reason_by_you">%1$s را دعوت کردید. دلیل: %2$s</string>
|
||||||
<string name="notice_room_join_with_reason_by_you">به اتاق پیوستید. دلیل: %1$s</string>
|
<string name="notice_room_join_with_reason_by_you">به اتاق پیوستید. دلیل: %1$s</string>
|
||||||
|
@ -207,26 +176,41 @@
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">دعوت %1$s برای پیوستن به اتاق را پس گرفتید. دلیل: %2$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">دعوت %1$s برای پیوستن به اتاق را پس گرفتید. دلیل: %2$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">دعوت برای %1$s را پذیرفتید. دلیل: %2$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">دعوت برای %1$s را پذیرفتید. دلیل: %2$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason_by_you">دعوت %1$s را رد کردید. دلیل: %2$s</string>
|
<string name="notice_room_withdraw_with_reason_by_you">دعوت %1$s را رد کردید. دلیل: %2$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added_by_you">
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
<item quantity="one">نشانی %1$s را به این اتاق افزودید.</item>
|
<item quantity="one">نشانی %1$s را به این اتاق افزودید.</item>
|
||||||
<item quantity="other">نشانیهای %1$s را به این اتاق افزودید.</item>
|
<item quantity="other">نشانیهای %1$s را به این اتاق افزودید.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">نشانی %1$s ار از این اتاق برداشتید.</item>
|
<item quantity="one">نشانی %1$s ار از این اتاق برداشتید.</item>
|
||||||
<item quantity="other">نشانیهای %1$s ار از این اتاق برداشتید.</item>
|
<item quantity="other">نشانیهای %1$s ار از این اتاق برداشتید.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set_by_you">نشانی اصلی این اتاق را به %1$s تنظیم کردید.</string>
|
<string name="notice_room_canonical_alias_set_by_you">نشانی اصلی این اتاق را به %1$s تنظیم کردید.</string>
|
||||||
<string name="notice_room_canonical_alias_unset_by_you">نشانی اصلی این اتاق را برداشتید.</string>
|
<string name="notice_room_canonical_alias_unset_by_you">نشانی اصلی این اتاق را برداشتید.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join_by_you">به میهمانان اجازهٔ پیوستن به گروه دادید.</string>
|
<string name="notice_room_guest_access_can_join_by_you">به میهمانان اجازهٔ پیوستن به گروه دادید.</string>
|
||||||
<string name="notice_room_guest_access_forbidden_by_you">میمهانان را از پیوستن به گروه بازداشتید.</string>
|
<string name="notice_room_guest_access_forbidden_by_you">میمهانان را از پیوستن به گروه بازداشتید.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok_by_you">رمزنگاری سرتاسری را روشن کردید.</string>
|
<string name="notice_end_to_end_ok_by_you">رمزنگاری سرتاسری را روشن کردید.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm_by_you">رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm_by_you">رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s).</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">مهمانها را از پیوستن به اتاق بازداشتید.</string>
|
||||||
</resources>
|
<string name="notice_direct_room_guest_access_forbidden">%1$s مهمانها را از پیوستن به اتاق بازداشت.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join_by_you">به مهمانها اجازه دادید به اینجا بپیوندند.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join">%1$s به مهمانها اجازه داد به اینجا بپیوندند.</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason_by_you">رفتید. دلیل: %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason">%1$s رفت. دلیل: %2$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason_by_you">پیوستید. دلیل: %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason">%1$sپیوست. دلیل: %2$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite_by_you">دعوت %1$s را پس گرفتید</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite">%1$s دعوت %2$s را پس گرفت</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite_by_you">%1$s را دعوت کردید</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite">%1$s، %2$s را دعوت کرد</string>
|
||||||
|
<string name="notice_direct_room_update_by_you">اینجا را ارتقا دادید.</string>
|
||||||
|
<string name="notice_direct_room_update">%s اینجا را ارتقا داد.</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility_by_you">پیامهای آینده را برای %1$s نمایان کردید</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility">%1$s پیامهای آینده را برای %2$s نمایان کرد</string>
|
||||||
|
<string name="notice_direct_room_leave_by_you">اتاق را ترک کردید</string>
|
||||||
|
<string name="notice_direct_room_leave">%1$s اتاق را ترک کرد</string>
|
||||||
|
<string name="notice_direct_room_join_by_you">پیوستید</string>
|
||||||
|
<string name="notice_direct_room_join">%1$s پیوست</string>
|
||||||
|
<string name="notice_direct_room_created_by_you">گفتوگو را ایجاد کردید</string>
|
||||||
|
<string name="notice_direct_room_created">%1$s گفتوگو را ایجاد کرد</string>
|
||||||
|
</resources>
|
|
@ -1,9 +1,7 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="summary_message">%1$s : %2$s</string>
|
<string name="summary_message">%1$s : %2$s</string>
|
||||||
<string name="summary_user_sent_image">%1$s a envoyé une image.</string>
|
<string name="summary_user_sent_image">%1$s a envoyé une image.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">invitation de %s</string>
|
<string name="notice_room_invite_no_invitee">invitation de %s</string>
|
||||||
<string name="notice_room_invite">%1$s a invité %2$s</string>
|
<string name="notice_room_invite">%1$s a invité %2$s</string>
|
||||||
<string name="notice_room_invite_you">%1$s vous a invité</string>
|
<string name="notice_room_invite_you">%1$s vous a invité</string>
|
||||||
|
@ -11,13 +9,13 @@
|
||||||
<string name="notice_room_leave">%1$s est parti du salon</string>
|
<string name="notice_room_leave">%1$s est parti du salon</string>
|
||||||
<string name="notice_room_reject">%1$s a rejeté l’invitation</string>
|
<string name="notice_room_reject">%1$s a rejeté l’invitation</string>
|
||||||
<string name="notice_room_kick">%1$s a expulsé %2$s</string>
|
<string name="notice_room_kick">%1$s a expulsé %2$s</string>
|
||||||
<string name="notice_room_unban">%1$s a révoqué le bannissement de %2$s</string>
|
<string name="notice_room_unban">%1$s a révoqué l\'exclusion de %2$s</string>
|
||||||
<string name="notice_room_ban">%1$s a banni %2$s</string>
|
<string name="notice_room_ban">%1$s a exclus %2$s</string>
|
||||||
<string name="notice_room_withdraw">%1$s a annulé l’invitation de %2$s</string>
|
<string name="notice_room_withdraw">%1$s a annulé l’invitation de %2$s</string>
|
||||||
<string name="notice_avatar_url_changed">%1$s a changé d’avatar</string>
|
<string name="notice_avatar_url_changed">%1$s a changé d’avatar</string>
|
||||||
<string name="notice_display_name_set">%1$s a modifié son nom affiché en %2$s</string>
|
<string name="notice_display_name_set">%1$s a modifié son nom affiché en %2$s</string>
|
||||||
<string name="notice_display_name_changed_from">%1$s a modifié son nom affiché %2$s en %3$s</string>
|
<string name="notice_display_name_changed_from">%1$s a modifié son nom affiché de %2$s en %3$s</string>
|
||||||
<string name="notice_display_name_removed">%1$s a supprimé son nom affiché (%2$s)</string>
|
<string name="notice_display_name_removed">%1$s a supprimé son nom affiché (précédemment %2$s)</string>
|
||||||
<string name="notice_room_topic_changed">%1$s a changé le sujet en : %2$s</string>
|
<string name="notice_room_topic_changed">%1$s a changé le sujet en : %2$s</string>
|
||||||
<string name="notice_room_name_changed">%1$s a changé le nom du salon en : %2$s</string>
|
<string name="notice_room_name_changed">%1$s a changé le nom du salon en : %2$s</string>
|
||||||
<string name="notice_placed_video_call">%s a passé un appel vidéo.</string>
|
<string name="notice_placed_video_call">%s a passé un appel vidéo.</string>
|
||||||
|
@ -31,54 +29,39 @@
|
||||||
<string name="notice_room_visibility_world_readable">n’importe qui.</string>
|
<string name="notice_room_visibility_world_readable">n’importe qui.</string>
|
||||||
<string name="notice_room_visibility_unknown">inconnu (%s).</string>
|
<string name="notice_room_visibility_unknown">inconnu (%s).</string>
|
||||||
<string name="notice_end_to_end">%1$s a activé le chiffrement de bout en bout (%2$s)</string>
|
<string name="notice_end_to_end">%1$s a activé le chiffrement de bout en bout (%2$s)</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference">%1$s a demandé une téléconférence VoIP</string>
|
<string name="notice_requested_voip_conference">%1$s a demandé une téléconférence VoIP</string>
|
||||||
<string name="notice_voip_started">Téléconférence VoIP démarrée</string>
|
<string name="notice_voip_started">Téléconférence VoIP démarrée</string>
|
||||||
<string name="notice_voip_finished">Téléconférence VoIP terminée</string>
|
<string name="notice_voip_finished">Téléconférence VoIP terminée</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(l’avatar a aussi changé)</string>
|
<string name="notice_avatar_changed_too">(l’avatar a aussi changé)</string>
|
||||||
<string name="notice_room_name_removed">%1$s a supprimé le nom du salon</string>
|
<string name="notice_room_name_removed">%1$s a supprimé le nom du salon</string>
|
||||||
<string name="notice_room_topic_removed">%1$s a supprimé le sujet du salon</string>
|
<string name="notice_room_topic_removed">%1$s a supprimé le sujet du salon</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s a mis à jour son profil %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s a mis à jour son profil %2$s</string>
|
||||||
<string name="notice_room_third_party_invite">%1$s a envoyé une invitation à %2$s pour rejoindre le salon</string>
|
<string name="notice_room_third_party_invite">%1$s a envoyé une invitation à %2$s pour rejoindre le salon</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s a accepté l’invitation pour %2$s</string>
|
<string name="notice_room_third_party_registered_invite">%1$s a accepté l’invitation pour %2$s</string>
|
||||||
|
|
||||||
<string name="notice_crypto_unable_to_decrypt">** Déchiffrement impossible : %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** Déchiffrement impossible : %s **</string>
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message.</string>
|
||||||
|
|
||||||
<string name="could_not_redact">Effacement impossible</string>
|
<string name="could_not_redact">Effacement impossible</string>
|
||||||
<string name="unable_to_send_message">Envoi du message impossible</string>
|
<string name="unable_to_send_message">Envoi du message impossible</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">L’envoi de l’image a échoué</string>
|
<string name="message_failed_to_upload">L’envoi de l’image a échoué</string>
|
||||||
|
|
||||||
<string name="network_error">Erreur de réseau</string>
|
<string name="network_error">Erreur de réseau</string>
|
||||||
<string name="matrix_error">Erreur de Matrix</string>
|
<string name="matrix_error">Erreur de Matrix</string>
|
||||||
|
|
||||||
<string name="room_error_join_failed_empty_room">Il est impossible pour le moment de revenir dans un salon vide.</string>
|
<string name="room_error_join_failed_empty_room">Il est impossible pour le moment de revenir dans un salon vide.</string>
|
||||||
|
|
||||||
<string name="encrypted_message">Message chiffré</string>
|
<string name="encrypted_message">Message chiffré</string>
|
||||||
|
|
||||||
<string name="medium_email">Adresse e-mail</string>
|
<string name="medium_email">Adresse e-mail</string>
|
||||||
<string name="medium_phone_number">Numéro de téléphone</string>
|
<string name="medium_phone_number">Numéro de téléphone</string>
|
||||||
|
|
||||||
<string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string>
|
<string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">Invitation de %s</string>
|
<string name="room_displayname_invite_from">Invitation de %s</string>
|
||||||
<string name="room_displayname_room_invite">Invitation au salon</string>
|
<string name="room_displayname_room_invite">Invitation au salon</string>
|
||||||
<string name="room_displayname_empty_room">Salon vide</string>
|
<string name="room_displayname_empty_room">Salon vide</string>
|
||||||
<string name="room_displayname_two_members">%1$s et %2$s</string>
|
<string name="room_displayname_two_members">%1$s et %2$s</string>
|
||||||
|
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s et 1 autre</item>
|
<item quantity="one">%1$s et 1 autre</item>
|
||||||
<item quantity="other">%1$s et %2$d autres</item>
|
<item quantity="other">%1$s et %2$d autres</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
|
||||||
<string name="notice_event_redacted">Message supprimé</string>
|
<string name="notice_event_redacted">Message supprimé</string>
|
||||||
<string name="notice_event_redacted_by">Message supprimé par %1$s</string>
|
<string name="notice_event_redacted_by">Message supprimé par %1$s</string>
|
||||||
<string name="notice_event_redacted_with_reason">Message supprimé [motif : %1$s]</string>
|
<string name="notice_event_redacted_with_reason">Message supprimé [motif : %1$s]</string>
|
||||||
<string name="notice_event_redacted_by_with_reason">Message supprimé par %1$s [motif : %2$s]</string>
|
<string name="notice_event_redacted_by_with_reason">Message supprimé par %1$s [motif : %2$s]</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account">Synchronisation initiale :
|
<string name="initial_sync_start_importing_account">Synchronisation initiale :
|
||||||
\nImportation du compte…</string>
|
\nImportation du compte…</string>
|
||||||
<string name="initial_sync_start_importing_account_crypto">Synchronisation initiale :
|
<string name="initial_sync_start_importing_account_crypto">Synchronisation initiale :
|
||||||
|
@ -95,12 +78,9 @@
|
||||||
\nImportation des communautés</string>
|
\nImportation des communautés</string>
|
||||||
<string name="initial_sync_start_importing_account_data">Synchronisation initiale :
|
<string name="initial_sync_start_importing_account_data">Synchronisation initiale :
|
||||||
\nImportation des données du compte</string>
|
\nImportation des données du compte</string>
|
||||||
|
|
||||||
<string name="notice_room_update">%s a mis à niveau ce salon.</string>
|
<string name="notice_room_update">%s a mis à niveau ce salon.</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">Envoi du message…</string>
|
<string name="event_status_sending_message">Envoi du message…</string>
|
||||||
<string name="clear_timeline_send_queue">Vider la file d’envoi</string>
|
<string name="clear_timeline_send_queue">Vider la file d’envoi</string>
|
||||||
|
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s a révoqué l’invitation pour %2$s à rejoindre le salon</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s a révoqué l’invitation pour %2$s à rejoindre le salon</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">Invitation de %1$s. Raison : %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">Invitation de %1$s. Raison : %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s a invité %2$s. Raison : %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s a invité %2$s. Raison : %3$s</string>
|
||||||
|
@ -109,35 +89,128 @@
|
||||||
<string name="notice_room_leave_with_reason">%1$s est parti du salon. Raison : %2$s</string>
|
<string name="notice_room_leave_with_reason">%1$s est parti du salon. Raison : %2$s</string>
|
||||||
<string name="notice_room_reject_with_reason">%1$s a refusé l’invitation. Raison : %2$s</string>
|
<string name="notice_room_reject_with_reason">%1$s a refusé l’invitation. Raison : %2$s</string>
|
||||||
<string name="notice_room_kick_with_reason">%1$s a expulsé %2$s. Raison : %3$s</string>
|
<string name="notice_room_kick_with_reason">%1$s a expulsé %2$s. Raison : %3$s</string>
|
||||||
<string name="notice_room_unban_with_reason">%1$s a révoqué le bannissement de %2$s. Raison : %3$s</string>
|
<string name="notice_room_unban_with_reason">%1$s a révoqué l\'exclusion de %2$s. Raison : %3$s</string>
|
||||||
<string name="notice_room_ban_with_reason">%1$s a banni %2$s. Raison : %3$s</string>
|
<string name="notice_room_ban_with_reason">%1$s a exclus %2$s. Raison : %3$s</string>
|
||||||
<string name="notice_room_third_party_invite_with_reason">%1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s</string>
|
<string name="notice_room_third_party_invite_with_reason">%1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s</string>
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s a accepté l’invitation pour %2$s. Raison : %3$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason">%1$s a accepté l’invitation pour %2$s. Raison : %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s a annulé l’invitation de %2$s. Raison : %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s a annulé l’invitation de %2$s. Raison : %3$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s a ajouté %2$s comme adresse pour ce salon.</item>
|
<item quantity="one">%1$s a ajouté %2$s comme adresse pour ce salon.</item>
|
||||||
<item quantity="other">%1$s a ajouté %2$s comme adresses pour ce salon.</item>
|
<item quantity="other">%1$s a ajouté %2$s comme adresses pour ce salon.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s a supprimé %2$s comme adresse pour ce salon.</item>
|
<item quantity="one">%1$s a supprimé %2$s comme adresse pour ce salon.</item>
|
||||||
<item quantity="other">%1$s a supprimé %3$s comme adresses pour ce salon.</item>
|
<item quantity="other">%1$s a supprimé %3$s comme adresses pour ce salon.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set">%1$s a défini %2$s comme adresse principale pour ce salon.</string>
|
<string name="notice_room_canonical_alias_set">%1$s a défini %2$s comme adresse principale pour ce salon.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s a supprimé l’adresse principale de ce salon.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s a supprimé l’adresse principale de ce salon.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join">%1$s a autorisé les visiteurs à rejoindre le salon.</string>
|
<string name="notice_room_guest_access_can_join">%1$s a autorisé les visiteurs à rejoindre le salon.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s a activé le chiffrement de bout en bout.</string>
|
<string name="notice_end_to_end_ok">%1$s a activé le chiffrement de bout en bout.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu).</string>
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés.</string>
|
<string name="key_verification_request_fallback_message">%s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés.</string>
|
||||||
|
|
||||||
<string name="notice_room_created">%1$s a créé le salon</string>
|
<string name="notice_room_created">%1$s a créé le salon</string>
|
||||||
</resources>
|
<string name="notice_direct_room_update_by_you">Vous avez mis cet endroit à niveau.</string>
|
||||||
|
<string name="notice_direct_room_update">%s a mis cet endroit à niveau.</string>
|
||||||
|
<string name="notice_room_update_by_you">Vous avez mis à niveau ce salon.</string>
|
||||||
|
<string name="notice_room_kick_by_you">Vous avez expulsé %1$s</string>
|
||||||
|
<string name="notice_room_reject_by_you">Vous avez rejeté l\'invitation</string>
|
||||||
|
<string name="notice_direct_room_leave_by_you">Vous avez quitté le salon</string>
|
||||||
|
<string name="notice_direct_room_leave">%1$s a quitté le salon</string>
|
||||||
|
<string name="notice_room_leave_by_you">Vous avez quitté le salon</string>
|
||||||
|
<string name="notice_direct_room_join_by_you">Vous avez rejoint le salon</string>
|
||||||
|
<string name="notice_direct_room_join">%1$s a rejoint le salon</string>
|
||||||
|
<string name="notice_room_join_by_you">Vous avez rejoint le salon</string>
|
||||||
|
<string name="notice_room_invite_by_you">Vous avez invité %1$s</string>
|
||||||
|
<string name="notice_direct_room_created_by_you">Vous avez créé la discussion</string>
|
||||||
|
<string name="notice_direct_room_created">%1$s a créé la discussion</string>
|
||||||
|
<string name="notice_room_created_by_you">Vous avez créé le salon</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_by_you">Votre invitation</string>
|
||||||
|
<string name="summary_you_sent_sticker">Vous avez envoyé un autocollant.</string>
|
||||||
|
<string name="summary_you_sent_image">Vous avez envoyé une image.</string>
|
||||||
|
<string name="notice_end_to_end_unknown_algorithm_by_you">Vous avez activé le chiffrement de bout en bout (algorithme %1$s inconnu).</string>
|
||||||
|
<string name="notice_end_to_end_ok_by_you">Vous avez activé le chiffrement de bout en bout.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">Vous avez empêché les visiteurs de rejoindre le salon.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string>
|
||||||
|
<string name="notice_room_guest_access_forbidden_by_you">Vous avez empêché les visiteurs de rejoindre le salon.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join_by_you">Vous avez autorisé les visiteurs à venir ici.</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join">%1$s a autorisé les visiteurs à venir ici.</string>
|
||||||
|
<string name="notice_room_guest_access_can_join_by_you">Vous avez autorisé les visiteurs à rejoindre le salon.</string>
|
||||||
|
<string name="notice_room_canonical_alias_unset_by_you">Vous avez supprimé l’adresse principale de ce salon.</string>
|
||||||
|
<string name="notice_room_canonical_alias_set_by_you">Vous avez défini %1$s comme adresse principale pour ce salon.</string>
|
||||||
|
<string name="notice_room_aliases_added_and_removed_by_you">Vous avez ajouté %1$s et supprimé %2$s comme adresses pour ce salon.</string>
|
||||||
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
|
<item quantity="one">Vous avez supprimé %1$s comme adresse pour ce salon.</item>
|
||||||
|
<item quantity="other">Vous avez supprimé %1$s comme adresses pour ce salon.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
|
<item quantity="one">Vous avez ajouté %1$s comme adresse pour ce salon.</item>
|
||||||
|
<item quantity="other">Vous avez ajouté %1$s comme adresses pour ce salon.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="notice_room_withdraw_with_reason_by_you">Vous avez annulé l’invitation de %1$s. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Vous avez accepté l’invitation pour %1$s. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Vous avez révoqué l’invitation de %1$s à rejoindre le salon. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_with_reason_by_you">Vous avez envoyé une invitation à %1$s pour rejoindre le salon. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_reject_with_reason_by_you">Vous avez refusé l’invitation. Raison : %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason_by_you">Vous êtes parti. Raison : %1$s</string>
|
||||||
|
<string name="notice_direct_room_leave_with_reason">%1$s est parti. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_leave_with_reason_by_you">Vous êtes parti du salon. Raison : %1$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason">%1$s rejoint. Raison : %2$s</string>
|
||||||
|
<string name="notice_direct_room_join_with_reason_by_you">Vous avez rejoint. Raison : %1$s</string>
|
||||||
|
<string name="notice_room_join_with_reason_by_you">Vous avez rejoint le salon. Raison : %1$s</string>
|
||||||
|
<string name="notice_room_invite_with_reason_by_you">Vous avez invité %1$s. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">Votre invitation. Raison %1$s</string>
|
||||||
|
<string name="notice_power_level_diff">%1$s de %2$s à %3$s</string>
|
||||||
|
<string name="notice_power_level_changed">%1$s a modifié le niveau de pouvoir de %2$s.</string>
|
||||||
|
<string name="notice_power_level_changed_by_you">Vous avez modifié le niveau de pouvoir de %1$s.</string>
|
||||||
|
<string name="power_level_custom_no_value">Personnalisé</string>
|
||||||
|
<string name="power_level_custom">Personnalisé (%1$d)</string>
|
||||||
|
<string name="power_level_default">Défaut</string>
|
||||||
|
<string name="power_level_moderator">Modérateur</string>
|
||||||
|
<string name="power_level_admin">Admin</string>
|
||||||
|
<string name="notice_widget_modified_by_you">Vous avez modifié le widget %1$s</string>
|
||||||
|
<string name="notice_widget_modified">%1$s a modifié le widget %2$s</string>
|
||||||
|
<string name="notice_widget_removed_by_you">Vous avez supprimé le widget %1$s</string>
|
||||||
|
<string name="notice_widget_removed">%1$s a supprimé le widget %2$s</string>
|
||||||
|
<string name="notice_widget_added_by_you">Vous avez ajouté le widget %1$s</string>
|
||||||
|
<string name="notice_widget_added">%1$s a ajouté le widget %2$s</string>
|
||||||
|
<string name="notice_room_third_party_registered_invite_by_you">Vous avez accepté l’invitation pour %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite_by_you">Vous avez révoqué l\'invitation pour %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_revoked_invite">%1$s a révoqué l\'invitation pour %2$s</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility_by_you">Vous avez rendu les futurs messages visible pour %1$s</string>
|
||||||
|
<string name="notice_made_future_direct_room_visibility">%1$s a rendu les futurs messages visible pour %2$s</string>
|
||||||
|
<string name="notice_room_avatar_removed_by_you">Vous avez supprimé l\'avatar du salon</string>
|
||||||
|
<string name="notice_room_avatar_removed">%1$s a supprimé l\'avatar du salon</string>
|
||||||
|
<string name="notice_room_name_removed_by_you">Vous avez supprimé le nom du salon</string>
|
||||||
|
<string name="notice_requested_voip_conference_by_you">Vous avez demandé une téléconférence VoIP</string>
|
||||||
|
<string name="notice_end_to_end_by_you">Vous avez activé le chiffrement de bout en bout (%1$s)</string>
|
||||||
|
<string name="notice_made_future_room_visibility_by_you">Vous avez rendu l’historique futur du salon visible pour %1$s</string>
|
||||||
|
<string name="notice_ended_call_by_you">Vous avez raccroché.</string>
|
||||||
|
<string name="notice_answered_call_by_you">Vous avez répondu à l’appel.</string>
|
||||||
|
<string name="notice_call_candidates_by_you">Vous avez envoyé les données pour configurer l\'appel.</string>
|
||||||
|
<string name="notice_call_candidates">%s a envoyé les données pour configurer l\'appel.</string>
|
||||||
|
<string name="notice_placed_voice_call_by_you">Vous avez passé un appel vocal.</string>
|
||||||
|
<string name="notice_placed_video_call_by_you">Vous avez passé un appel vidéo.</string>
|
||||||
|
<string name="notice_room_name_changed_by_you">Vous avez changé le nom du salon en : %1$s</string>
|
||||||
|
<string name="notice_room_avatar_changed_by_you">Vous avez modifié l\'avatar du salon</string>
|
||||||
|
<string name="notice_room_avatar_changed">%1$s a modifié l\'avatar du salon</string>
|
||||||
|
<string name="notice_display_name_changed_from_by_you">Vous avez modifié votre nom affiché de %1$s en %2$s</string>
|
||||||
|
<string name="notice_display_name_set_by_you">Vous avez modifié votre nom affiché en %1$s</string>
|
||||||
|
<string name="notice_avatar_url_changed_by_you">Vous avez changé votre avatar</string>
|
||||||
|
<string name="notice_room_withdraw_by_you">Vous avez annulé l’invitation de %1$s</string>
|
||||||
|
<string name="notice_room_topic_changed_by_you">Vous avez changé le sujet en : %1$s</string>
|
||||||
|
<string name="notice_display_name_removed_by_you">Vous avez supprimé votre nom affiché (précédemment %1$s)</string>
|
||||||
|
<string name="notice_room_third_party_revoked_invite_by_you">Vous avez révoqué l’invitation pour %1$s à rejoindre le salon</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite_by_you">Vous avez invité %1$s</string>
|
||||||
|
<string name="notice_direct_room_third_party_invite">%1$s a invité %2$s</string>
|
||||||
|
<string name="notice_room_third_party_invite_by_you">Vous avez envoyé une invitation à %1$s pour rejoindre le salon</string>
|
||||||
|
<string name="notice_profile_change_redacted_by_you">Vous avez mis à jour votre profile %1$s</string>
|
||||||
|
<string name="notice_room_topic_removed_by_you">Vous avez supprimé le sujet du salon</string>
|
||||||
|
<string name="notice_room_ban_with_reason_by_you">Vous avez exclus %1$s. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_unban_with_reason_by_you">Vous avez révoqué l\'exclusion de %1$s. Raison : %2$s</string>
|
||||||
|
<string name="notice_room_ban_by_you">Vous avez exclus %1$s</string>
|
||||||
|
<string name="notice_room_unban_by_you">Vous avez révoqué l\'exclusion de %1$s</string>
|
||||||
|
<string name="notice_room_kick_with_reason_by_you">Vous avez expulsé %1$s. Raison : %2$s</string>
|
||||||
|
</resources>
|
|
@ -183,7 +183,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Hai rimosso %1$s come indirizzo per questa stanza.</item>
|
<item quantity="one">Hai rimosso %1$s come indirizzo per questa stanza.</item>
|
||||||
<item quantity="other">Hai rimosso %2$s come indirizzi per questa stanza.</item>
|
<item quantity="other">Hai rimosso %1$s come indirizzi per questa stanza.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza.</string>
|
||||||
<string name="notice_room_canonical_alias_set_by_you">Hai impostato l\'indirizzo principale per questa stanza a %1$s.</string>
|
<string name="notice_room_canonical_alias_set_by_you">Hai impostato l\'indirizzo principale per questa stanza a %1$s.</string>
|
||||||
|
|
|
@ -172,7 +172,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Tekkseḍ %1$s am tansa i texxamt-a.</item>
|
<item quantity="one">Tekkseḍ %1$s am tansa i texxamt-a.</item>
|
||||||
<item quantity="other">Tekkseḍ %2$s am tansiwin i texxamt-a.</item>
|
<item quantity="other">Tekkseḍ %1$s am tansiwin i texxamt-a.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s yerna %2$s terniḍ tekkseḍ %3$s am tansiwin i texxamt-a.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s yerna %2$s terniḍ tekkseḍ %3$s am tansiwin i texxamt-a.</string>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a.</string>
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="summary_user_sent_image">%1$s išsiuntė atvaizdą.</string>
|
<string name="summary_user_sent_image">%1$s išsiuntė vaizdą.</string>
|
||||||
<string name="summary_user_sent_sticker">%1$s išsiuntė lipduką.</string>
|
<string name="summary_user_sent_sticker">%1$s išsiuntė lipduką.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">%s pakvietimas</string>
|
<string name="notice_room_invite_no_invitee">%s pakvietimas</string>
|
||||||
</resources>
|
<string name="notice_room_join_by_you">Jūs prisijungėte prie kambario</string>
|
||||||
|
<string name="notice_room_join">%1$s prisijungė prie kambario</string>
|
||||||
|
<string name="notice_room_invite_you">%1$s pakvietė jus</string>
|
||||||
|
<string name="notice_room_invite_by_you">Jūs pakvietėte %1$s</string>
|
||||||
|
<string name="notice_room_invite">%1$s pakvietė %2$s</string>
|
||||||
|
<string name="notice_direct_room_created_by_you">Jūs sukūrėte diskusiją</string>
|
||||||
|
<string name="notice_direct_room_created">%1$s sukūrė diskusiją</string>
|
||||||
|
<string name="notice_room_created_by_you">Jūs sukūrėte kambarį</string>
|
||||||
|
<string name="notice_room_created">%1$s sukūrė kambarį</string>
|
||||||
|
<string name="notice_room_invite_no_invitee_by_you">Jūsų pakvietimas</string>
|
||||||
|
<string name="summary_you_sent_sticker">Jūs išsiuntėte lipduką.</string>
|
||||||
|
<string name="summary_you_sent_image">Jūs išsiuntėte vaizdą.</string>
|
||||||
|
</resources>
|
|
@ -182,7 +182,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Você removeu %1$s como um endereço desta sala.</item>
|
<item quantity="one">Você removeu %1$s como um endereço desta sala.</item>
|
||||||
<item quantity="other">Você removeu %2$s como endereços desta sala.</item>
|
<item quantity="other">Você removeu %1$s como endereços desta sala.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s adicionou %2$s e removeu %3$s como endereços desta sala.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s adicionou %2$s e removeu %3$s como endereços desta sala.</string>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Você adicionou %1$s e removeu %2$s como endereços desta sala.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Você adicionou %1$s e removeu %2$s como endereços desta sala.</string>
|
||||||
|
|
|
@ -225,4 +225,6 @@
|
||||||
<string name="notice_direct_room_join">%1$s вошел(ла)</string>
|
<string name="notice_direct_room_join">%1$s вошел(ла)</string>
|
||||||
<string name="notice_direct_room_created_by_you">Вы создали обсуждение</string>
|
<string name="notice_direct_room_created_by_you">Вы создали обсуждение</string>
|
||||||
<string name="notice_direct_room_created">%1$s создал(а) обсуждение</string>
|
<string name="notice_direct_room_created">%1$s создал(а) обсуждение</string>
|
||||||
|
<string name="notice_direct_room_update_by_you">Вы обновили.</string>
|
||||||
|
<string name="notice_direct_room_update">%s обновлена.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -183,8 +183,8 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Odstránili ste adresu %1$s pre túto miestnosť.</item>
|
<item quantity="one">Odstránili ste adresu %1$s pre túto miestnosť.</item>
|
||||||
<item quantity="few">Odstránili ste adresy %2$s pre túto miestnosť.</item>
|
<item quantity="few">Odstránili ste adresy %1$s pre túto miestnosť.</item>
|
||||||
<item quantity="other">Odstránili ste adresy %2$s pre túto miestnosť.</item>
|
<item quantity="other">Odstránili ste adresy %1$s pre túto miestnosť.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Pridali ste %1$s a odstránili adresy %2$s pre túto miestnosť.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Pridali ste %1$s a odstránili adresy %2$s pre túto miestnosť.</string>
|
||||||
<string name="notice_room_canonical_alias_set_by_you">Nastavili ste hlavnú adresu tejto miestnosti %1$s.</string>
|
<string name="notice_room_canonical_alias_set_by_you">Nastavili ste hlavnú adresu tejto miestnosti %1$s.</string>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="summary_user_sent_image">%1$s dërgoi një figurë.</string>
|
<string name="summary_user_sent_image">%1$s dërgoi një figurë.</string>
|
||||||
|
@ -25,39 +25,26 @@
|
||||||
<string name="notice_requested_voip_conference">%1$s kërkoi një konferencë VoIP</string>
|
<string name="notice_requested_voip_conference">%1$s kërkoi një konferencë VoIP</string>
|
||||||
<string name="notice_voip_started">Konferenca VoIP filloi</string>
|
<string name="notice_voip_started">Konferenca VoIP filloi</string>
|
||||||
<string name="notice_voip_finished">Konferenca VoIP përfundoi</string>
|
<string name="notice_voip_finished">Konferenca VoIP përfundoi</string>
|
||||||
|
|
||||||
<string name="notice_avatar_changed_too">(u ndryshua edhe avatari)</string>
|
<string name="notice_avatar_changed_too">(u ndryshua edhe avatari)</string>
|
||||||
<string name="notice_room_name_removed">%1$s hoqi emrin e dhomës</string>
|
<string name="notice_room_name_removed">%1$s hoqi emrin e dhomës</string>
|
||||||
<string name="notice_profile_change_redacted">%1$s përditësoi profilin e tij %2$s</string>
|
<string name="notice_profile_change_redacted">%1$s përditësoi profilin e tij %2$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite">%1$s pranoi ftesën tuaj për %2$s</string>
|
<string name="notice_room_third_party_registered_invite">%1$s pranoi ftesën tuaj për %2$s</string>
|
||||||
|
|
||||||
<string name="notice_crypto_unable_to_decrypt">** S’arrihet të shfshehtëzohet: %s **</string>
|
<string name="notice_crypto_unable_to_decrypt">** S’arrihet të shfshehtëzohet: %s **</string>
|
||||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string>
|
<string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string>
|
||||||
|
|
||||||
<string name="could_not_redact">S’u redaktua dot</string>
|
<string name="could_not_redact">S’u redaktua dot</string>
|
||||||
<string name="unable_to_send_message">S’arrihet të dërgohet mesazh</string>
|
<string name="unable_to_send_message">S’arrihet të dërgohet mesazh</string>
|
||||||
|
|
||||||
<string name="message_failed_to_upload">Ngarkimi i figurës dështoi</string>
|
<string name="message_failed_to_upload">Ngarkimi i figurës dështoi</string>
|
||||||
|
|
||||||
<string name="network_error">Gabim rrjeti</string>
|
<string name="network_error">Gabim rrjeti</string>
|
||||||
<string name="matrix_error">Gabim Matrix</string>
|
<string name="matrix_error">Gabim Matrix</string>
|
||||||
|
|
||||||
<string name="room_error_join_failed_empty_room">Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët.</string>
|
<string name="room_error_join_failed_empty_room">Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët.</string>
|
||||||
|
<string name="encrypted_message">Mesazh i fshehtëzuar</string>
|
||||||
<string name="encrypted_message">U fshehtëzua mesazhi</string>
|
|
||||||
|
|
||||||
<string name="medium_email">Adresë email</string>
|
<string name="medium_email">Adresë email</string>
|
||||||
<string name="medium_phone_number">Numër telefoni</string>
|
<string name="medium_phone_number">Numër telefoni</string>
|
||||||
|
|
||||||
<string name="room_displayname_invite_from">Ftesë nga %s</string>
|
<string name="room_displayname_invite_from">Ftesë nga %s</string>
|
||||||
<string name="room_displayname_room_invite">Ftesë Dhome</string>
|
<string name="room_displayname_room_invite">Ftesë Dhome</string>
|
||||||
|
|
||||||
<string name="room_displayname_two_members">%1$s dhe %2$s</string>
|
<string name="room_displayname_two_members">%1$s dhe %2$s</string>
|
||||||
|
|
||||||
<string name="room_displayname_empty_room">Dhomë e zbrazët</string>
|
<string name="room_displayname_empty_room">Dhomë e zbrazët</string>
|
||||||
|
|
||||||
<string name="summary_user_sent_sticker">%1$s dërgoi një ngjitës.</string>
|
<string name="summary_user_sent_sticker">%1$s dërgoi një ngjitës.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">Ftesë e %s</string>
|
<string name="notice_room_invite_no_invitee">Ftesë e %s</string>
|
||||||
<string name="notice_room_unban">%1$s hoqi dëbimin për %2$s</string>
|
<string name="notice_room_unban">%1$s hoqi dëbimin për %2$s</string>
|
||||||
<string name="notice_room_withdraw">%1$s tërhoqi mbrapsht ftesën për %2$s</string>
|
<string name="notice_room_withdraw">%1$s tërhoqi mbrapsht ftesën për %2$s</string>
|
||||||
|
@ -65,20 +52,17 @@
|
||||||
<string name="notice_display_name_changed_from">%1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s</string>
|
<string name="notice_display_name_changed_from">%1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s</string>
|
||||||
<string name="notice_display_name_removed">%1$s hoqi emrin e tij në ekran (%2$s)</string>
|
<string name="notice_display_name_removed">%1$s hoqi emrin e tij në ekran (%2$s)</string>
|
||||||
<string name="notice_end_to_end">%1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s)</string>
|
<string name="notice_end_to_end">%1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s)</string>
|
||||||
|
|
||||||
<string name="notice_room_topic_removed">%1$s hoqi temën e dhomës</string>
|
<string name="notice_room_topic_removed">%1$s hoqi temën e dhomës</string>
|
||||||
<string name="notice_room_third_party_invite">%1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë</string>
|
<string name="notice_room_third_party_invite">%1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë</string>
|
||||||
<plurals name="room_displayname_three_and_more_members">
|
<plurals name="room_displayname_three_and_more_members">
|
||||||
<item quantity="one">%1$s dhe 1 tjetër</item>
|
<item quantity="one">%1$s dhe 1 tjetër</item>
|
||||||
<item quantity="other">%1$s dhe %2$d të tjerë</item>
|
<item quantity="other">%1$s dhe %2$d të tjerë</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_event_redacted">Mesazhi u hoq</string>
|
<string name="notice_event_redacted">Mesazhi u hoq</string>
|
||||||
<string name="notice_event_redacted_by">Mesazhi u hoq nga %1$s</string>
|
<string name="notice_event_redacted_by">Mesazhi u hoq nga %1$s</string>
|
||||||
<string name="notice_event_redacted_with_reason">Mesazh i hequr [arsye: %1$s]</string>
|
<string name="notice_event_redacted_with_reason">Mesazh i hequr [arsye: %1$s]</string>
|
||||||
<string name="notice_event_redacted_by_with_reason">Mesazh i hequr nga %1$s [arsye: %2$s]</string>
|
<string name="notice_event_redacted_by_with_reason">Mesazh i hequr nga %1$s [arsye: %2$s]</string>
|
||||||
<string name="notice_room_update">%s e përmirësoi këtë dhomë.</string>
|
<string name="notice_room_update">%s e përmirësoi këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
|
<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
|
||||||
\nPo importohet llogaria…</string>
|
\nPo importohet llogaria…</string>
|
||||||
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
|
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
|
||||||
|
@ -95,10 +79,8 @@
|
||||||
\nPo importohen Bashkësi</string>
|
\nPo importohen Bashkësi</string>
|
||||||
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
|
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
|
||||||
\nPo importohet të Dhëna Llogarie</string>
|
\nPo importohet të Dhëna Llogarie</string>
|
||||||
|
|
||||||
<string name="event_status_sending_message">Po dërgohet mesazh…</string>
|
<string name="event_status_sending_message">Po dërgohet mesazh…</string>
|
||||||
<string name="clear_timeline_send_queue">Spastro radhë pritjeje</string>
|
<string name="clear_timeline_send_queue">Spastro radhë pritjeje</string>
|
||||||
|
|
||||||
<string name="notice_room_third_party_revoked_invite">%1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma</string>
|
<string name="notice_room_third_party_revoked_invite">%1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma</string>
|
||||||
<string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string>
|
||||||
<string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string>
|
<string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string>
|
||||||
|
@ -113,34 +95,25 @@
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s pranoi ftesën për %2$s. Arsye: %3$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason">%1$s pranoi ftesën për %2$s. Arsye: %3$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason">%1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s</string>
|
<string name="notice_room_withdraw_with_reason">%1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added">
|
<plurals name="notice_room_aliases_added">
|
||||||
<item quantity="one">%1$s shtoi %2$s si një adresë për këtë dhomë.</item>
|
<item quantity="one">%1$s shtoi %2$s si një adresë për këtë dhomë.</item>
|
||||||
<item quantity="other">%1$s shtoi %2$s si adresa për këtë dhomë.</item>
|
<item quantity="other">%1$s shtoi %2$s si adresa për këtë dhomë.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed">
|
<plurals name="notice_room_aliases_removed">
|
||||||
<item quantity="one">%1$s hoqi %2$s si adresë për këtë dhomë.</item>
|
<item quantity="one">%1$s hoqi %2$s si adresë për këtë dhomë.</item>
|
||||||
<item quantity="other">%1$s hoqi %3$s si adresa për këtë dhomë.</item>
|
<item quantity="other">%1$s hoqi %3$s si adresa për këtë dhomë.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set">%1$s caktoi %2$s si adresë kryesore për këtë dhomë.</string>
|
<string name="notice_room_canonical_alias_set">%1$s caktoi %2$s si adresë kryesore për këtë dhomë.</string>
|
||||||
<string name="notice_room_canonical_alias_unset">%1$s hoqi adresën kryesore për këtë dhomë.</string>
|
<string name="notice_room_canonical_alias_unset">%1$s hoqi adresën kryesore për këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join">%1$s ka lejuar vizitorë të marrin pjesë në dhomë.</string>
|
<string name="notice_room_guest_access_can_join">%1$s ka lejuar vizitorë të marrin pjesë në dhomë.</string>
|
||||||
<string name="notice_room_guest_access_forbidden">%1$s ka penguar vizitorë të marrin pjesë në dhomë.</string>
|
<string name="notice_room_guest_access_forbidden">%1$s ka penguar vizitorë të marrin pjesë në dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok">%1$s aktivizoi fshehtëzim skaj-më-skaj.</string>
|
<string name="notice_end_to_end_ok">%1$s aktivizoi fshehtëzim skaj-më-skaj.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm">%1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s).</string>
|
<string name="notice_end_to_end_unknown_algorithm">%1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s).</string>
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh.</string>
|
<string name="key_verification_request_fallback_message">%s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh.</string>
|
||||||
|
|
||||||
<string name="notice_room_created">%1$s krijo dhomën</string>
|
<string name="notice_room_created">%1$s krijo dhomën</string>
|
||||||
<string name="summary_you_sent_image">Dërguat një figurë.</string>
|
<string name="summary_you_sent_image">Dërguat një figurë.</string>
|
||||||
<string name="summary_you_sent_sticker">Dërguat një ngjitës.</string>
|
<string name="summary_you_sent_sticker">Dërguat një ngjitës.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_by_you">Ftesa juaj</string>
|
<string name="notice_room_invite_no_invitee_by_you">Ftesa juaj</string>
|
||||||
<string name="notice_room_created_by_you">Krijuat dhomën</string>
|
<string name="notice_room_created_by_you">Krijuat dhomën</string>
|
||||||
<string name="notice_room_invite_by_you">Ftuat %1$s</string>
|
<string name="notice_room_invite_by_you">Ftuat %1$s</string>
|
||||||
|
@ -168,7 +141,6 @@
|
||||||
<string name="notice_made_future_room_visibility_by_you">E bëtë historikun e ardhshëm të dhomë të dukshëm për %1$s</string>
|
<string name="notice_made_future_room_visibility_by_you">E bëtë historikun e ardhshëm të dhomë të dukshëm për %1$s</string>
|
||||||
<string name="notice_end_to_end_by_you">Aktivizuat fshehtëzim skaj-më-skaj (%1$s)</string>
|
<string name="notice_end_to_end_by_you">Aktivizuat fshehtëzim skaj-më-skaj (%1$s)</string>
|
||||||
<string name="notice_room_update_by_you">Përmirësuat këtë dhomë.</string>
|
<string name="notice_room_update_by_you">Përmirësuat këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_requested_voip_conference_by_you">Kërkuat një konferencë VoIP</string>
|
<string name="notice_requested_voip_conference_by_you">Kërkuat një konferencë VoIP</string>
|
||||||
<string name="notice_room_name_removed_by_you">Hoqët emrin e dhomës</string>
|
<string name="notice_room_name_removed_by_you">Hoqët emrin e dhomës</string>
|
||||||
<string name="notice_room_topic_removed_by_you">Hoqët temën e dhomës</string>
|
<string name="notice_room_topic_removed_by_you">Hoqët temën e dhomës</string>
|
||||||
|
@ -178,24 +150,20 @@
|
||||||
<string name="notice_room_third_party_invite_by_you">Dërguat një ftesë te %1$s për të ardhur te dhoma</string>
|
<string name="notice_room_third_party_invite_by_you">Dërguat një ftesë te %1$s për të ardhur te dhoma</string>
|
||||||
<string name="notice_room_third_party_revoked_invite_by_you">Shfuqizuat ftesën për ardhjen në dhomë të %1$s</string>
|
<string name="notice_room_third_party_revoked_invite_by_you">Shfuqizuat ftesën për ardhjen në dhomë të %1$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_by_you">Pranuat ftesën për %1$s</string>
|
<string name="notice_room_third_party_registered_invite_by_you">Pranuat ftesën për %1$s</string>
|
||||||
|
|
||||||
<string name="notice_widget_added">%1$s shtoi widget-in %2$s</string>
|
<string name="notice_widget_added">%1$s shtoi widget-in %2$s</string>
|
||||||
<string name="notice_widget_added_by_you">Shtuat widget-in %1$s</string>
|
<string name="notice_widget_added_by_you">Shtuat widget-in %1$s</string>
|
||||||
<string name="notice_widget_removed">%1$s hoqi widget-in %2$s</string>
|
<string name="notice_widget_removed">%1$s hoqi widget-in %2$s</string>
|
||||||
<string name="notice_widget_removed_by_you">Hoqët widget-in %1$s</string>
|
<string name="notice_widget_removed_by_you">Hoqët widget-in %1$s</string>
|
||||||
<string name="notice_widget_modified">%1$s ndryshoi widget-in %2$s</string>
|
<string name="notice_widget_modified">%1$s ndryshoi widget-in %2$s</string>
|
||||||
<string name="notice_widget_modified_by_you">Ndryshuat widget-in %1$s</string>
|
<string name="notice_widget_modified_by_you">Ndryshuat widget-in %1$s</string>
|
||||||
|
|
||||||
<string name="power_level_admin">Përgjegjës</string>
|
<string name="power_level_admin">Përgjegjës</string>
|
||||||
<string name="power_level_moderator">Moderator</string>
|
<string name="power_level_moderator">Moderator</string>
|
||||||
<string name="power_level_default">Parazgjedhje</string>
|
<string name="power_level_default">Parazgjedhje</string>
|
||||||
<string name="power_level_custom">Vetjake (%1$d)</string>
|
<string name="power_level_custom">Vetjake (%1$d)</string>
|
||||||
<string name="power_level_custom_no_value">Vetjake</string>
|
<string name="power_level_custom_no_value">Vetjake</string>
|
||||||
|
|
||||||
<string name="notice_power_level_changed_by_you">Ndryshuat shkallën e pushtetit për %1$s.</string>
|
<string name="notice_power_level_changed_by_you">Ndryshuat shkallën e pushtetit për %1$s.</string>
|
||||||
<string name="notice_power_level_changed">%1$s ndryshoi shkallën e pushtetit për %2$s.</string>
|
<string name="notice_power_level_changed">%1$s ndryshoi shkallën e pushtetit për %2$s.</string>
|
||||||
<string name="notice_power_level_diff">%1$s nga %2$s në %3$s</string>
|
<string name="notice_power_level_diff">%1$s nga %2$s në %3$s</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Ftesa juaj. Arsye: %1$s</string>
|
<string name="notice_room_invite_no_invitee_with_reason_by_you">Ftesa juaj. Arsye: %1$s</string>
|
||||||
<string name="notice_room_invite_with_reason_by_you">Ftuat %1$s. Arsye: %2$s</string>
|
<string name="notice_room_invite_with_reason_by_you">Ftuat %1$s. Arsye: %2$s</string>
|
||||||
<string name="notice_room_join_with_reason_by_you">Erdhët në dhomë, Arsye: %1$s</string>
|
<string name="notice_room_join_with_reason_by_you">Erdhët në dhomë, Arsye: %1$s</string>
|
||||||
|
@ -208,26 +176,19 @@
|
||||||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Shfuqizuat ftesën për ardhjen në dhomë të %1$s. Arsye: %2$s</string>
|
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Shfuqizuat ftesën për ardhjen në dhomë të %1$s. Arsye: %2$s</string>
|
||||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Pranuat ftesën për %1$s. Arsye: %2$s</string>
|
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Pranuat ftesën për %1$s. Arsye: %2$s</string>
|
||||||
<string name="notice_room_withdraw_with_reason_by_you">Tërhoqët mbrapsht ftesën për %1$s. Arsye: %2$s</string>
|
<string name="notice_room_withdraw_with_reason_by_you">Tërhoqët mbrapsht ftesën për %1$s. Arsye: %2$s</string>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_added_by_you">
|
<plurals name="notice_room_aliases_added_by_you">
|
||||||
<item quantity="one">Shtuat %1$s si një adresë për këtë dhomë.</item>
|
<item quantity="one">Shtuat %1$s si një adresë për këtë dhomë.</item>
|
||||||
<item quantity="other">Shtuat %1$s si adresa për këtë dhomë.</item>
|
<item quantity="other">Shtuat %1$s si adresa për këtë dhomë.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Hoqët %1$s si një adresë për këtë dhomë.</item>
|
<item quantity="one">Hoqët %1$s si një adresë për këtë dhomë.</item>
|
||||||
<item quantity="other">Hoqët %1$s si adresa për këtë dhomë.</item>
|
<item quantity="other">Hoqët %1$s si adresa për këtë dhomë.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Shtuat %1$s dhe hoqët %2$s si adresa për këtë dhomë.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Shtuat %1$s dhe hoqët %2$s si adresa për këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_room_canonical_alias_set_by_you">Caktuat si adresë kryesore për këtë dhomë %1$s.</string>
|
<string name="notice_room_canonical_alias_set_by_you">Caktuat si adresë kryesore për këtë dhomë %1$s.</string>
|
||||||
<string name="notice_room_canonical_alias_unset_by_you">Hoqët adresën kryesore për këtë dhomë.</string>
|
<string name="notice_room_canonical_alias_unset_by_you">Hoqët adresën kryesore për këtë dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_room_guest_access_can_join_by_you">Keni lejuar të vijnë mysafirë në dhomë.</string>
|
<string name="notice_room_guest_access_can_join_by_you">Keni lejuar të vijnë mysafirë në dhomë.</string>
|
||||||
<string name="notice_room_guest_access_forbidden_by_you">U keni penguar mysafirëve të vijnë në dhomë.</string>
|
<string name="notice_room_guest_access_forbidden_by_you">U keni penguar mysafirëve të vijnë në dhomë.</string>
|
||||||
|
|
||||||
<string name="notice_end_to_end_ok_by_you">Aktivizuat fshehtëzimin skaj-më-skaj.</string>
|
<string name="notice_end_to_end_ok_by_you">Aktivizuat fshehtëzimin skaj-më-skaj.</string>
|
||||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Aktivizuat fshehtëzimin skaj-më-skaj (algoritëm %1$s i panjohur).</string>
|
<string name="notice_end_to_end_unknown_algorithm_by_you">Aktivizuat fshehtëzimin skaj-më-skaj (algoritëm %1$s i panjohur).</string>
|
||||||
|
</resources>
|
||||||
</resources>
|
|
|
@ -174,7 +174,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">Du tog bort %1$s som en adress för det här rummet.</item>
|
<item quantity="one">Du tog bort %1$s som en adress för det här rummet.</item>
|
||||||
<item quantity="other">Du tog bort %2$s som adresser för det här rummet.</item>
|
<item quantity="other">Du tog bort %1$s som adresser för det här rummet.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s lade till %2$s och tog bort %3$s som adresser för det här rummet.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s lade till %2$s och tog bort %3$s som adresser för det här rummet.</string>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">Du lade till %1$s och tog bort %2$s som adresser för det här rummet.</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">Du lade till %1$s och tog bort %2$s som adresser för det här rummet.</string>
|
||||||
|
|
|
@ -177,7 +177,7 @@
|
||||||
<item quantity="other">您新增了 %1$s 为此聊天室的地址。</item>
|
<item quantity="other">您新增了 %1$s 为此聊天室的地址。</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="other">您移除了此聊天室的 %2$s 地址。</item>
|
<item quantity="other">您移除了此聊天室的 %1$s 地址。</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">您为此聊天室新增了 %1$s 并移除了 %2$s 地址。</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">您为此聊天室新增了 %1$s 并移除了 %2$s 地址。</string>
|
||||||
<string name="notice_room_canonical_alias_set_by_you">您将此聊天室的主地址设为了 %1$s。</string>
|
<string name="notice_room_canonical_alias_set_by_you">您将此聊天室的主地址设为了 %1$s。</string>
|
||||||
|
@ -196,7 +196,7 @@
|
||||||
<string name="notice_direct_room_third_party_invite">%1$s 邀请了 %2$s</string>
|
<string name="notice_direct_room_third_party_invite">%1$s 邀请了 %2$s</string>
|
||||||
<string name="notice_direct_room_update_by_you">您在此处升级。</string>
|
<string name="notice_direct_room_update_by_you">您在此处升级。</string>
|
||||||
<string name="notice_direct_room_update">%s 在此处升级。</string>
|
<string name="notice_direct_room_update">%s 在此处升级。</string>
|
||||||
<string name="notice_made_future_direct_room_visibility_by_you">您使未来的消息对 %2$s 可见</string>
|
<string name="notice_made_future_direct_room_visibility_by_you">您使未来的消息对 %1$s 可见</string>
|
||||||
<string name="notice_made_future_direct_room_visibility">%1$s 使未来的消息对 %2$s 可见</string>
|
<string name="notice_made_future_direct_room_visibility">%1$s 使未来的消息对 %2$s 可见</string>
|
||||||
<string name="notice_direct_room_leave_by_you">您离开了聊天室</string>
|
<string name="notice_direct_room_leave_by_you">您离开了聊天室</string>
|
||||||
<string name="notice_direct_room_leave">%1$s 离开了聊天室</string>
|
<string name="notice_direct_room_leave">%1$s 离开了聊天室</string>
|
||||||
|
@ -204,4 +204,8 @@
|
||||||
<string name="notice_direct_room_join">%1$s 已加入</string>
|
<string name="notice_direct_room_join">%1$s 已加入</string>
|
||||||
<string name="notice_direct_room_created_by_you">您创建了讨论</string>
|
<string name="notice_direct_room_created_by_you">您创建了讨论</string>
|
||||||
<string name="notice_direct_room_created">%1$s 创建了讨论</string>
|
<string name="notice_direct_room_created">%1$s 创建了讨论</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden_by_you">你已阻止客人加入房间。</string>
|
||||||
|
<string name="notice_direct_room_guest_access_forbidden">%1$s已阻止客人加入房间。</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join_by_you">你已允许客人加入这里。</string>
|
||||||
|
<string name="notice_direct_room_guest_access_can_join">%1$s 已允许客人加入这里。</string>
|
||||||
</resources>
|
</resources>
|
|
@ -177,7 +177,7 @@
|
||||||
<item quantity="other">您為此聊天室新增了 %1$s 作為地址。</item>
|
<item quantity="other">您為此聊天室新增了 %1$s 作為地址。</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="other">您為此聊天室移除了 %2$s 作為地址。</item>
|
<item quantity="other">您為此聊天室移除了 %1$s 作為地址。</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notice_room_aliases_added_and_removed_by_you">您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。</string>
|
<string name="notice_room_aliases_added_and_removed_by_you">您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。</string>
|
||||||
<string name="notice_room_canonical_alias_set_by_you">您將此聊天室的主要地址設定為 %1$s。</string>
|
<string name="notice_room_canonical_alias_set_by_you">您將此聊天室的主要地址設定為 %1$s。</string>
|
||||||
|
|
|
@ -225,7 +225,7 @@
|
||||||
|
|
||||||
<plurals name="notice_room_aliases_removed_by_you">
|
<plurals name="notice_room_aliases_removed_by_you">
|
||||||
<item quantity="one">You removed %1$s as an address for this room.</item>
|
<item quantity="one">You removed %1$s as an address for this room.</item>
|
||||||
<item quantity="other">You removed %2$s as addresses for this room.</item>
|
<item quantity="other">You removed %1$s as addresses for this room.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string>
|
<string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string>
|
||||||
|
|
|
@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt
|
||||||
./vector/src/main/res/color \
|
./vector/src/main/res/color \
|
||||||
./vector/src/main/res/layout \
|
./vector/src/main/res/layout \
|
||||||
./vector/src/main/res/values \
|
./vector/src/main/res/values \
|
||||||
./vector/src/main/res/values-v21 \
|
|
||||||
./vector/src/main/res/xml
|
./vector/src/main/res/xml
|
||||||
|
|
||||||
resultForbiddenStringInResource=$?
|
resultForbiddenStringInResource=$?
|
||||||
|
|
|
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
|
||||||
# android\.text\.TextUtils
|
# android\.text\.TextUtils
|
||||||
|
|
||||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||||
enum class===82
|
enum class===83
|
||||||
|
|
||||||
### Do not import temporary legacy classes
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
|
|
@ -45,6 +45,10 @@ parser.add_argument('-e',
|
||||||
'--expecting',
|
'--expecting',
|
||||||
type=int,
|
type=int,
|
||||||
help='the expected number of artifacts. If omitted, no check will be done.')
|
help='the expected number of artifacts. If omitted, no check will be done.')
|
||||||
|
parser.add_argument('-i',
|
||||||
|
'--ignoreErrors',
|
||||||
|
help='Ignore errors that can be ignored. Build state and number of artifacts.',
|
||||||
|
action="store_true")
|
||||||
parser.add_argument('-d',
|
parser.add_argument('-d',
|
||||||
'--directory',
|
'--directory',
|
||||||
default="",
|
default="",
|
||||||
|
@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit'))
|
||||||
print(" git commit message : \"%s\"" % data0.get('message'))
|
print(" git commit message : \"%s\"" % data0.get('message'))
|
||||||
print(" build state : %s" % data0.get('state'))
|
print(" build state : %s" % data0.get('state'))
|
||||||
|
|
||||||
|
error = False
|
||||||
|
|
||||||
if data0.get('state') != 'passed':
|
if data0.get('state') != 'passed':
|
||||||
print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state'))
|
print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state'))
|
||||||
exit(1)
|
if args.ignoreErrors:
|
||||||
|
error = True
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
### Fetch artifacts list
|
### Fetch artifacts list
|
||||||
|
|
||||||
|
@ -110,8 +119,11 @@ data = json.loads(r.content.decode())
|
||||||
print(" %d artifact(s) found." % len(data))
|
print(" %d artifact(s) found." % len(data))
|
||||||
|
|
||||||
if args.expecting is not None and args.expecting != len(data):
|
if args.expecting is not None and args.expecting != len(data):
|
||||||
print("Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
|
print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
|
||||||
exit(1)
|
if args.ignoreErrors:
|
||||||
|
error = True
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
print("Json data:")
|
print("Json data:")
|
||||||
|
@ -128,8 +140,6 @@ else:
|
||||||
if not args.simulate:
|
if not args.simulate:
|
||||||
os.mkdir(targetDir)
|
os.mkdir(targetDir)
|
||||||
|
|
||||||
error = False
|
|
||||||
|
|
||||||
for elt in data:
|
for elt in data:
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
print()
|
print()
|
||||||
|
@ -157,7 +167,7 @@ for elt in data:
|
||||||
print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash))
|
print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash))
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
print("❌ Error(s) occurred, check the log")
|
print("❌ Error(s) occurred, please check the log")
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
print("Done!")
|
print("Done!")
|
||||||
|
|
|
@ -39,9 +39,9 @@ def generateVersionCodeFromVersionName() {
|
||||||
|
|
||||||
def getVersionCode() {
|
def getVersionCode() {
|
||||||
if (gitBranchName() == "develop") {
|
if (gitBranchName() == "develop") {
|
||||||
return generateVersionCodeFromTimestamp() * 10
|
return generateVersionCodeFromTimestamp()
|
||||||
} else {
|
} else {
|
||||||
return generateVersionCodeFromVersionName() * 10
|
return generateVersionCodeFromVersionName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,13 +166,14 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
|
// assign different version code for each output
|
||||||
|
def baseVariantVersion = variant.versionCode * 10
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
|
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
|
||||||
// Known limitation: it does not modify the value in the BuildConfig.java generated file
|
// Known limitation: it does not modify the value in the BuildConfig.java generated file
|
||||||
print "ABI " + output.getFilter(OutputFile.ABI) + " \tvariant.versionCode " + variant.versionCode
|
|
||||||
// See https://issuetracker.google.com/issues/171133218
|
// See https://issuetracker.google.com/issues/171133218
|
||||||
output.versionCodeOverride = variant.versionCode + baseAbiVersionCode
|
output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode
|
||||||
print " \t-> VersionCode = " + output.versionCodeOverride + "\n"
|
print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ import im.vector.app.core.di.HasVectorInjector
|
||||||
import im.vector.app.core.di.VectorComponent
|
import im.vector.app.core.di.VectorComponent
|
||||||
import im.vector.app.core.extensions.configureAndStart
|
import im.vector.app.core.extensions.configureAndStart
|
||||||
import im.vector.app.core.rx.RxConfig
|
import im.vector.app.core.rx.RxConfig
|
||||||
|
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||||
import im.vector.app.features.configuration.VectorConfiguration
|
import im.vector.app.features.configuration.VectorConfiguration
|
||||||
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
||||||
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
|
@ -89,6 +90,7 @@ class VectorApplication :
|
||||||
@Inject lateinit var rxConfig: RxConfig
|
@Inject lateinit var rxConfig: RxConfig
|
||||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
@Inject lateinit var pinLocker: PinLocker
|
@Inject lateinit var pinLocker: PinLocker
|
||||||
|
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
|
|
||||||
|
@ -173,6 +175,7 @@ class VectorApplication :
|
||||||
})
|
})
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager)
|
||||||
// This should be done as early as possible
|
// This should be done as early as possible
|
||||||
// initKnownEmojiHashSet(appContext)
|
// initKnownEmojiHashSet(appContext)
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,7 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
|
||||||
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
|
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
|
||||||
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
|
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
|
||||||
import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
||||||
|
import im.vector.app.features.settings.VectorSettingsGeneralFragment
|
||||||
import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
|
import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
|
||||||
import im.vector.app.features.settings.VectorSettingsLabsFragment
|
import im.vector.app.features.settings.VectorSettingsLabsFragment
|
||||||
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
|
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
|
||||||
|
@ -292,6 +293,11 @@ interface FragmentModule {
|
||||||
@FragmentKey(VectorSettingsPinFragment::class)
|
@FragmentKey(VectorSettingsPinFragment::class)
|
||||||
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
|
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VectorSettingsGeneralFragment::class)
|
||||||
|
fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(PushRulesFragment::class)
|
@FragmentKey(PushRulesFragment::class)
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.core.dialogs
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.yalantis.ucrop.UCrop
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||||
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
|
import im.vector.app.features.media.createUCropWithDefaultSettings
|
||||||
|
import im.vector.lib.multipicker.MultiPicker
|
||||||
|
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use to let the user choose between Camera (with permission handling) and Gallery (with single image selection),
|
||||||
|
* then edit the image
|
||||||
|
* [Listener.onImageReady] will be called with an uri of a square image store in the cache of the application.
|
||||||
|
* It's up to the caller to delete the file.
|
||||||
|
*/
|
||||||
|
class GalleryOrCameraDialogHelper(
|
||||||
|
// must implement GalleryOrCameraDialogHelper.Listener
|
||||||
|
private val fragment: Fragment,
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) {
|
||||||
|
interface Listener {
|
||||||
|
fun onImageReady(uri: Uri?)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val activity
|
||||||
|
get() = fragment.requireActivity()
|
||||||
|
|
||||||
|
private val listener = fragment as? Listener ?: error("Fragment must implement GalleryOrCameraDialogHelper.Listener")
|
||||||
|
|
||||||
|
private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted ->
|
||||||
|
if (allGranted) {
|
||||||
|
doOpenCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val takePhotoActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
avatarCameraUri?.let { uri ->
|
||||||
|
MultiPicker.get(MultiPicker.CAMERA)
|
||||||
|
.getTakenPhoto(activity, uri)
|
||||||
|
?.let { startUCrop(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pickImageActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
MultiPicker
|
||||||
|
.get(MultiPicker.IMAGE)
|
||||||
|
.getSelectedFiles(activity, activityResult.data)
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { startUCrop(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val uCropActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
activityResult.data?.let { listener.onImageReady(UCrop.getOutput(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startUCrop(image: MultiPickerImageType) {
|
||||||
|
val destinationFile = File(activity.cacheDir, "${image.displayName}_e_${System.currentTimeMillis()}")
|
||||||
|
val uri = image.contentUri
|
||||||
|
createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title))
|
||||||
|
.withAspectRatio(1f, 1f)
|
||||||
|
.getIntent(activity)
|
||||||
|
.let { uCropActivityResultLauncher.launch(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Type {
|
||||||
|
Camera,
|
||||||
|
Gallery
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.attachment_type_dialog_title)
|
||||||
|
.setItems(arrayOf(
|
||||||
|
fragment.getString(R.string.attachment_type_camera),
|
||||||
|
fragment.getString(R.string.attachment_type_gallery)
|
||||||
|
)) { _, which ->
|
||||||
|
onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAvatarTypeSelected(type: Type) {
|
||||||
|
when (type) {
|
||||||
|
Type.Camera ->
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) {
|
||||||
|
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher)
|
||||||
|
}
|
||||||
|
Type.Gallery ->
|
||||||
|
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarCameraUri: Uri? = null
|
||||||
|
private fun doOpenCamera() {
|
||||||
|
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher)
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,8 +44,10 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
|
||||||
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
@EpoxyAttribute lateinit var stringProvider: StringProvider
|
@EpoxyAttribute lateinit var stringProvider: StringProvider
|
||||||
|
@EpoxyAttribute var izLowPriority: Boolean = false
|
||||||
@EpoxyAttribute var izFavorite: Boolean = false
|
@EpoxyAttribute var izFavorite: Boolean = false
|
||||||
@EpoxyAttribute var settingsClickListener: ClickListener? = null
|
@EpoxyAttribute var settingsClickListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute var lowPriorityClickListener: ClickListener? = null
|
||||||
@EpoxyAttribute var favoriteClickListener: ClickListener? = null
|
@EpoxyAttribute var favoriteClickListener: ClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
|
@ -53,17 +55,44 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
|
||||||
avatarRenderer.render(matrixItem, holder.avatar)
|
avatarRenderer.render(matrixItem, holder.avatar)
|
||||||
holder.avatar.onClick(settingsClickListener)
|
holder.avatar.onClick(settingsClickListener)
|
||||||
holder.roomName.setTextOrHide(matrixItem.displayName)
|
holder.roomName.setTextOrHide(matrixItem.displayName)
|
||||||
|
setLowPriorityState(holder, izLowPriority)
|
||||||
setFavoriteState(holder, izFavorite)
|
setFavoriteState(holder, izFavorite)
|
||||||
|
|
||||||
|
holder.roomLowPriority.setOnClickListener {
|
||||||
|
// Immediate echo
|
||||||
|
setLowPriorityState(holder, !izLowPriority)
|
||||||
|
if (!izLowPriority) {
|
||||||
|
// If we put the room in low priority, it will also remove the favorite tag
|
||||||
|
setFavoriteState(holder, false)
|
||||||
|
}
|
||||||
|
// And do the action
|
||||||
|
lowPriorityClickListener?.invoke()
|
||||||
|
}
|
||||||
holder.roomFavorite.setOnClickListener {
|
holder.roomFavorite.setOnClickListener {
|
||||||
// Immediate echo
|
// Immediate echo
|
||||||
setFavoriteState(holder, !izFavorite)
|
setFavoriteState(holder, !izFavorite)
|
||||||
|
if (!izFavorite) {
|
||||||
|
// If we put the room in favorite, it will also remove the low priority tag
|
||||||
|
setLowPriorityState(holder, false)
|
||||||
|
}
|
||||||
// And do the action
|
// And do the action
|
||||||
favoriteClickListener?.invoke()
|
favoriteClickListener?.invoke()
|
||||||
}
|
}
|
||||||
holder.roomSettings.onClick(settingsClickListener)
|
holder.roomSettings.onClick(settingsClickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setLowPriorityState(holder: Holder, isLowPriority: Boolean) {
|
||||||
|
val tintColor: Int
|
||||||
|
if (isLowPriority) {
|
||||||
|
holder.roomLowPriority.contentDescription = stringProvider.getString(R.string.room_list_quick_actions_low_priority_remove)
|
||||||
|
tintColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
|
||||||
|
} else {
|
||||||
|
holder.roomLowPriority.contentDescription = stringProvider.getString(R.string.room_list_quick_actions_low_priority_add)
|
||||||
|
tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary)
|
||||||
|
}
|
||||||
|
ImageViewCompat.setImageTintList(holder.roomLowPriority, ColorStateList.valueOf(tintColor))
|
||||||
|
}
|
||||||
|
|
||||||
private fun setFavoriteState(holder: Holder, isFavorite: Boolean) {
|
private fun setFavoriteState(holder: Holder, isFavorite: Boolean) {
|
||||||
val tintColor: Int
|
val tintColor: Int
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
|
@ -81,6 +110,7 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
val avatar by bind<ImageView>(R.id.bottomSheetRoomPreviewAvatar)
|
val avatar by bind<ImageView>(R.id.bottomSheetRoomPreviewAvatar)
|
||||||
val roomName by bind<TextView>(R.id.bottomSheetRoomPreviewName)
|
val roomName by bind<TextView>(R.id.bottomSheetRoomPreviewName)
|
||||||
|
val roomLowPriority by bind<ImageView>(R.id.bottomSheetRoomPreviewLowPriority)
|
||||||
val roomFavorite by bind<ImageView>(R.id.bottomSheetRoomPreviewFavorite)
|
val roomFavorite by bind<ImageView>(R.id.bottomSheetRoomPreviewFavorite)
|
||||||
val roomSettings by bind<View>(R.id.bottomSheetRoomPreviewSettings)
|
val roomSettings by bind<View>(R.id.bottomSheetRoomPreviewSettings)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix {
|
||||||
|
|
||||||
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
|
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
|
||||||
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
|
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
|
||||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
val colorBuffer = IntArray(width * height)
|
||||||
for (x in 0 until width) {
|
var rowOffset = 0
|
||||||
for (y in 0 until height) {
|
for (y in 0 until height) {
|
||||||
bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor)
|
for (x in 0 until width) {
|
||||||
|
val arrayIndex = x + rowOffset
|
||||||
|
colorBuffer[arrayIndex] = if (get(x, y)) foregroundColor else backgroundColor
|
||||||
}
|
}
|
||||||
|
rowOffset += width
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp
|
return Bitmap.createBitmap(colorBuffer, width, height, Bitmap.Config.ARGB_8888)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package im.vector.app.core.services
|
package im.vector.app.core.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.Ringtone
|
||||||
|
import android.media.RingtoneManager
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
|
@ -25,7 +27,26 @@ import androidx.core.content.getSystemService
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class CallRingPlayer(
|
class CallRingPlayerIncoming(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
private var r: Ringtone? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
||||||
|
r = RingtoneManager.getRingtone(applicationContext, notification)
|
||||||
|
Timber.v("## VOIP Starting ringing incomming")
|
||||||
|
r?.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
r?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallRingPlayerOutgoing(
|
||||||
context: Context
|
context: Context
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -44,12 +65,12 @@ class CallRingPlayer(
|
||||||
try {
|
try {
|
||||||
if (player?.isPlaying == false) {
|
if (player?.isPlaying == false) {
|
||||||
player?.start()
|
player?.start()
|
||||||
Timber.v("## VOIP Starting ringing")
|
Timber.v("## VOIP Starting ringing outgoing")
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## VOIP already playing")
|
Timber.v("## VOIP already playing")
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "## VOIP Failed to start ringing")
|
Timber.e(failure, "## VOIP Failed to start ringing outgoing")
|
||||||
player = null
|
player = null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,7 +95,7 @@ class CallRingPlayer(
|
||||||
} else {
|
} else {
|
||||||
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||||
.build())
|
.build())
|
||||||
}
|
}
|
||||||
return mediaPlayer
|
return mediaPlayer
|
||||||
|
|