Merge branch 'develop' into feature/db_perf_research

This commit is contained in:
ganfra 2020-11-02 19:23:10 +01:00
commit afa1bee02f
204 changed files with 7017 additions and 4721 deletions

View File

@ -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:
@ -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.
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)

View File

@ -6,9 +6,24 @@ Features ✨:
Improvements 🙌:
- 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 🐛:
-
- 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 🗣:
-

View File

@ -65,9 +65,8 @@ allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them
// 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) {

View File

@ -0,0 +1 @@
// TODO

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View 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.

View File

@ -0,0 +1 @@
Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.

View File

@ -0,0 +1 @@
Element (antaŭe Riot.im)

View 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.

View File

@ -0,0 +1,30 @@
المنت گونه‌ای جدید از کاره‌های پیام‌رسانی و همکاری است که:
۱. کنترل محرمانگیتان را در دست خودتان می‌گذارد
۲. می‌گذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید
۳. از شما در برابر تبلیغات، داده‌کاوری و دیوارهای پرداختی، محافظت می‌کند
۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان می‌کند
المنت به خاطر نامتمرکز و نرم‌افزار آزاد بودن، کاملاً با دیگر کاره‌های پیام‌رسانی و همکاری، فرق دارد.
المنت می‌گذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش داده‌ها و گفت‌وگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکه‌ای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید.
المنت می‌تواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفت‌وگوی باز و نامتمرکز عمل می‌کند.
المنت با اجازه برای گزینش کسی که گفت‌وگوهایتان را میزبانی می‌کند، کنترل را به شما می‌دهد. با کارهٔ المنت، می‌توانید برگزینید که به روش‌های مختلفی میزبانی شوید:
۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعه‌دهندگان ماتریکس میزبانی می‌شود، یا گرینش از میان هزاران کارساز عمومی میزبانی‌شده به دست داوطلبان
۲. خودمیزبانی حسابتان با اجرای کراسازی روی سخت‌افزار خودتان
۳. ثبت‌نام برای حسابی روی یک کارساز سفارشی با اشتراک در بن‌یازهٔ میزبانی خدمات ماتریکس المنت
<b>چرا المنت را برگزینیم؟</b>
<b>مالک داده‌هایتان باشید</b>: خوتان تصمیم می‌گیرید که داده‌ها و پیام‌هایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان می‌کنید، نه شرکت‌های بزرگی که داده‌هایتان را کاویده و به شرکت‌های دیگر دسترسی می‌دهند.
<b>پیام‌رسانی و همکاری باز</b>: می‌توانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیام‌رسانی متفاوتی مثل اسلک، آی‌آرسی یا جبر استفاده کنند.
<b>فوق امن</b>: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفت‌وگو هستند،‌می‌توانند پیام‌ها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزاره‌های شرکت‌کنندگان در گفت‌وگو.
<b>ارتباط کامل</b>: پیام‌رسانی، تماس‌های صوتی و تصویری،‌هم‌رسانی پرونده، هم‌رسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید.
<b>هرجا که هستید</b>: هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید.

View File

@ -0,0 +1 @@
گپ و تماس نامتمرکز امن. داده‌هایتان را از شرکت‌ها امن نگه دارید.

View File

@ -0,0 +1 @@
المنت (ریوت سابق)

View File

@ -0,0 +1 @@
Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité.

View File

@ -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>Ö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.

View File

@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it)
}
fun deleteAvatar(): Completable = completableBuilder<Unit> {
room.deleteAvatar(it)
}
}
fun Room.rx(): RxRoom {

View File

@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
fun syncSession(session: Session) {
val lock = CountDownLatch(1)
GlobalScope.launch(Dispatchers.Main) { session.open() }
val job = GlobalScope.launch(Dispatchers.Main) {
session.open()
}
runBlocking { job.join() }
session.startSync(true)
@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
}
// 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)
var result: T? = null
@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
block.invoke(callback)
await(lock)
await(lock, timeout)
assertNotNull(result)
return result!!
@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) {
doSync<Unit> { session.signOut(true, it) }
session.close()
doSync<Unit>(60_000) { session.signOut(true, it) }
// no need signout will close
// session.close()
}
}

View File

@ -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.create.CreateRoomParams
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_BACKUP
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 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
roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
// Bob send 3 messages
roomFromBobPOV.sendTextMessage(messagesFromBob[0])
roomFromBobPOV.sendTextMessage(messagesFromBob[1])
roomFromBobPOV.sendTextMessage(messagesFromBob[2])
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
// Alice sends a message
roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
mTestHelper.await(lock)
bobTimeline.removeListener(bobEventsListener)
bobTimeline.dispose()
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
return cryptoTestData
}
@ -285,14 +251,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync<String> {
alice.createRoom(
CreateRoomParams().apply {
invitedUserIds.add(bob.myUserId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = true
},
it
)
alice.createDirectRoom(bob.myUserId, it)
}
mTestHelper.waitWithLatch { latch ->

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener
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.DevicesListResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import kotlin.jvm.Throws
interface CryptoService {
@ -142,10 +144,13 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
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
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>

View File

@ -35,6 +35,22 @@ interface RoomService {
fun createRoom(createRoomParams: CreateRoomParams,
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
* @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 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?
}

View File

@ -63,8 +63,13 @@ data class RoomSummary constructor(
val hasNewMessages: Boolean
get() = notificationCount != 0
val isLowPriority: Boolean
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
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
get() = joinedMembersCount == 2

View File

@ -16,6 +16,7 @@
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.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@ -51,6 +52,11 @@ class CreateRoomParams {
*/
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.
* This will tell the server to invite everyone in the list to the newly created room.

View File

@ -58,6 +58,11 @@ interface StateService {
*/
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 getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -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
}
}

View File

@ -17,12 +17,10 @@
package org.matrix.android.sdk.internal.crypto
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import kotlinx.coroutines.CancellationException
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.RoomHistoryVisibilityContent
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.MessageEncrypter
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.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.MXUsersDevicesMap
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.RoomKeyWithHeldContent
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.GetDeviceInfoTask
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.UploadKeysTask
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.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.foldToCallback
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.RoomMemberHelper
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.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.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.fetchCopied
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.jvm.Throws
import kotlin.math.max
/**
@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
@SessionDatabase private val monarchy: Monarchy,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter
private val eventDecryptor: EventDecryptor
) : CryptoService {
init {
verificationService.cryptoService = this
}
private val uiHandler = Handler(Looper.getMainLooper())
private val isStarting = 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) {
when (event.getClearType()) {
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>) {
setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
@ -410,7 +389,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingGossipingRequestManager.close()
olmDevice.release()
cryptoStore.close()
}
@ -452,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor(
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
*/
override 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)
.findFirst()
}
return encryptionEvent != null
return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
}
/**
@ -660,11 +640,8 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
// moved to crypto scope to have uptodate values
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// if (!isStarted()) {
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
// internalStart(false)
// }
val userIds = getRoomUserIds(roomId)
var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
@ -720,14 +697,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
cryptoCoroutineScope.launch {
val result = runCatching {
withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
}
}
result.foldToCallback(callback)
}
eventDecryptor.decryptEventAsync(event, timeline, callback)
}
/**
@ -739,42 +709,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
@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
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
}
}
}
return eventDecryptor.decryptEvent(event, timeline)
}
/**
@ -796,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
cryptoStore.saveGossipingEvent(event)
gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event)
}
EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> {
// 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)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
}
EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event)
gossipingBuffer.add(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
@ -828,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
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()) {
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
return
@ -935,19 +870,9 @@ internal class DefaultCryptoService @Inject constructor(
}
private fun getRoomUserIds(roomId: String): List<String> {
var userIds: List<String> = emptyList()
monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
userIds = if (encryptForInvitedMembers) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
}
}
return userIds
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
}
/**
@ -1257,38 +1182,38 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
}
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 < 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)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
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}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
}
// 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 < 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)
//
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// 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}")
// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
// sendToDeviceTask.execute(sendToDeviceParams)
// }
// }
/**
* Provides the list of unknown devices
@ -1339,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor(
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> {
return cryptoStore.getIncomingRoomKeyRequests()
}
override fun getGossipingEventsTrail(): List<Event> {
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
return cryptoStore.getGossipingEventsTrail()
}
override fun getGossipingEvents(): List<Event> {
return cryptoStore.getGossipingEvents()
}
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
return cryptoStore.getSharedWithInfo(roomId, sessionId)
}

View File

@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
// Update devices trust for these users
dispatchDeviceChange(downloadUsers)
// dispatchDeviceChange(downloadUsers)
return onKeysDownloadSucceed(filteredUsers, response.failures)
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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.worker.WorkerParamsFactory
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
@SessionScope
@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope) {
private val executor = Executors.newSingleThreadExecutor()
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync.
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
fun close() {
executor.shutdownNow()
}
// Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap<String, Long>()
@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun onGossipingRequestEvent(event: Event) {
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
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) {
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
if (event.getClearType() == EventType.REQUEST_SECRET) {
@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
// save in DB
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
// // save in DB
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun processReceivedGossipingRequests() {
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
receivedGossipingRequests.clear()
for (request in roomKeyRequestsToProcess) {
if (request is IncomingRoomKeyRequest) {
processIncomingRoomKeyRequest(request)
} else if (request is IncomingSecretShareRequest) {
processIncomingSecretShareRequest(request)
}
}
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
}
receivedRequestCancellations?.forEach { request ->
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
// ignore remote echo
return@forEach
executor.execute {
cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
for (request in roomKeyRequestsToProcess) {
if (request is IncomingRoomKeyRequest) {
processIncomingRoomKeyRequest(request)
} else if (request is IncomingSecretShareRequest) {
processIncomingSecretShareRequest(request)
}
}
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)
receivedRequestCancellations?.forEach { request ->
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
// ignore remote echo
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)
}
}
}
}

View File

@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
/**
* 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.
@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
timelineSet.add(messageIndexKey)
}
store.storeInboundGroupSessions(listOf(session))
inboundGroupSessionStore.storeInBoundGroupSession(session)
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
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)
}
val session = store.getInboundGroupSession(sessionId, senderKey)
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
if (session != null) {
// Check that the room id matches the original one for the session. This stops

View File

@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, false)
}
}
@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, true)
}
}

View File

@ -16,10 +16,13 @@
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.auth.data.Credentials
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.Event
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.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.configureWith
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 timber.log.Timber
@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val taskExecutor: TaskExecutor
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : IMXEncrypting {
// 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) {
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
devices.forEach { userId, deviceId, withheldCode ->
this.add(UserDevice(userId, deviceId) to withheldCode)
// offload to computation thread
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
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 ((deviceId) in devicesToShareWith) {
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"
}
))
}
}

View File

@ -16,6 +16,7 @@
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.internal.crypto.DeviceListManager
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.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
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 {
return MXMegolmEncryption(
@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
sendToDeviceTask,
messageEncrypter,
warnOnUnknownDevicesRepository,
taskExecutor
taskExecutor,
coroutineDispatchers,
cryptoCoroutineScope
)
}
}

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.crosssigning
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.extensions.orFalse
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 kotlinx.coroutines.CoroutineScope
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.OlmUtility
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class DefaultCrossSigningService @Inject constructor(
@UserId private val userId: String,
@SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val initializeCrossSigningTask: InitializeCrossSigningTask,
@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
// First let's get my user key
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
return UserTrustResult.Success
}
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(userId)
@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
}
// Let's get the other user master key
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
val otherMasterKey = otherInfo?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(userId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
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)
}
@ -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)
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()
?: return UserTrustResult.CrossSigningNotConfigured(userId)
@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// Maybe it's signed by a locally trusted device?
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
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) {
// Check signature validity?
try {
@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
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))
}
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 {
return if (locallyTrusted == true) {
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
}
override fun onUsersDeviceUpdate(userIds: List<String>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
userIds.forEach { otherUserId ->
checkUserTrust(otherUserId).let {
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
}
}
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
val workerData = WorkerParamsFactory.toData(workerParams)
// now check device trust
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
userIds.forEach { otherUserId ->
// TODO if my keys have changes, i should recheck all devices of all users?
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())
}
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
.setInputData(workerData)
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.build()
if (otherUserId == userId) {
// It's me, i should check if a newly trusted device is signing my master key
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
}
}
eventBus.post(CryptoToSessionUserTrustChange(userIds))
}
workManagerProvider.workManager
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue()
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {

View File

@ -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)
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor(
this.callback = object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) {
// Reset backup markers.
cryptoStore.resetBackupMarkers()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// move tx out of UI thread
cryptoStore.resetBackupMarkers()
}
val keyBackupVersion = KeysVersionResult(
algorithm = createKeysBackupVersionBody.algorithm,
@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor(
val importResult = awaitCallback<ImportRoomKeysResult> {
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}")
} else {
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
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
withContext(coroutineDispatchers.crypto) {
withContext(coroutineDispatchers.computation) {
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor(
if (retrievedMegolmBackupAuthData != null) {
keysBackupVersion = keysVersionResult
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
}
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.store
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.events.model.Event
import org.matrix.android.sdk.api.util.Optional
@ -126,7 +127,10 @@ internal interface IMXCryptoStore {
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/**
@ -364,6 +368,7 @@ internal interface IMXCryptoStore {
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
fun saveGossipingEvent(event: Event)
fun saveGossipingEvents(events: List<Event>)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
updateGossipingRequestState(
@ -441,10 +446,13 @@ internal interface IMXCryptoStore {
// Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
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 getDeviceKeysUploaded(): Boolean

View File

@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
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.DeviceInfoEntityFields
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.IncomingGossipingRequestEntityFields
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 ->
realm.where<GossipingEventEntity>()
}.map {
@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor(
return request
}
override fun saveGossipingEvent(event: Event) {
override fun saveGossipingEvents(events: List<Event>) {
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>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name
}
doRealmTransaction(realmConfiguration) { realm ->
realm.insertOrUpdate(entity)
monarchy.writeAsync { realm ->
events.forEach { event ->
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 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? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// 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> {
// return doRealmQueryAndCopyList(realmConfiguration) {
// it.where<GossipingEventEntity>()
@ -1417,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor(
.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? {
return doWithRealm(realmConfiguration) { realm ->
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)

View File

@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
import org.matrix.android.sdk.internal.di.SerializeNulls
import io.realm.DynamicRealm
import io.realm.RealmMigration
import io.realm.RealmObjectSchema
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
import timber.log.Timber
import javax.inject.Inject
@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
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) {
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")
realm.schema.get("IncomingRoomKeyRequestEntity")
?.addField("requestBodyAlgorithm", String::class.java)
?.addField("requestBodyRoomId", String::class.java)
?.addField("requestBodySenderKey", String::class.java)
?.addField("requestBodySessionId", String::class.java)
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject ->
val requestBodyString = dynamicObject.getString("requestBodyString")
try {
val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error")
}
}
?.removeField("requestBodyString")
?.removeFieldIfExists("requestBodyString")
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
realm.schema.get("OutgoingRoomKeyRequestEntity")
?.addField("requestBodyAlgorithm", String::class.java)
?.addField("requestBodyRoomId", String::class.java)
?.addField("requestBodySenderKey", String::class.java)
?.addField("requestBodySessionId", String::class.java)
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject ->
val requestBodyString = dynamicObject.getString("requestBodyString")
try {
val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
@ -134,16 +156,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error")
}
}
?.removeField("requestBodyString")
?.removeFieldIfExists("requestBodyString")
Timber.d("Create KeysBackupDataEntity")
realm.schema.create("KeysBackupDataEntity")
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
if (!realm.schema.contains("KeysBackupDataEntity")) {
realm.schema.create("KeysBackupDataEntity")
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
.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) {
@ -151,8 +175,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.d("Migrate to RiotX model")
realm.schema.get("CryptoRoomEntity")
?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
// Convert format of MXDeviceInfo, package has to be the same.
realm.schema.get("DeviceInfoEntity")
@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 4L added Cross Signing info persistence
private fun migrateTo4(realm: DynamicRealm) {
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")
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)

View File

@ -33,14 +33,13 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
data class Params(val roomId: String,
val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
val keepKeys: List<String>? = null,
val crypto: CryptoService
val keepKeys: List<String>? = null
)
}
internal class DefaultEncryptEventTask @Inject constructor(
// private val crypto: CryptoService
private val localEchoRepository: LocalEchoRepository
private val localEchoRepository: LocalEchoRepository,
private val cryptoService: CryptoService
) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event {
// don't want to wait for any query
@ -60,7 +59,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
// try {
// let it throws
awaitCallback<MXEncryptEventContentResult> {
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
}.let { result ->
val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep ->
@ -81,7 +80,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
).toContent(),
forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = params.crypto.getMyDevice().fingerprint()
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
)
} else {
null

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto.tasks
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.room.send.SendState
import org.matrix.android.sdk.internal.network.executeRequest
@ -29,8 +28,7 @@ import javax.inject.Inject
internal interface SendEventTask : Task<SendEventTask.Params, String> {
data class Params(
val event: Event,
val encrypt: Boolean,
val cryptoService: CryptoService?
val encrypt: Boolean
)
}
@ -68,8 +66,7 @@ internal class DefaultSendEventTask @Inject constructor(
return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "",
params.event,
listOf("m.relates_to"),
params.cryptoService!!
listOf("m.relates_to")
))
}
return params.event

View File

@ -15,21 +15,20 @@
*/
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.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.session.room.RoomAPI
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.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
data class Params(
val event: Event,
val cryptoService: CryptoService?
val event: Event
)
}
@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val eventBus: EventBus) : SendVerificationMessageTask {
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 {
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "",
params.event,
listOf("m.relates_to"),
params.cryptoService
listOf("m.relates_to")
))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear

View File

@ -20,7 +20,6 @@ import android.os.Handler
import android.os.Looper
import dagger.Lazy
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.KEYBACKUP_SECRET_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())
// Cannot be injected in constructor as it creates a dependency cycle
lateinit var cryptoService: CryptoService
// map [sender : [transaction]]
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
// Event received from the sync
fun onToDeviceEvent(event: Event) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event)
@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
}
fun onRoomEvent(event: Event) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onRoomStartRequestReceived(event)
@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
}
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
uiHandler.post {
listeners.forEach {
try {
@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here
val otherDeviceId = validRequestInfo.fromDevice
Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
cryptoCoroutineScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
Timber.e("## Verification device $otherDeviceId is not known")
}
}
Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
// Remember this request
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor(
// TODO refactor this with the DM one
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 transport = verificationTransportToDeviceFactory.createTransport(null)

View File

@ -20,7 +20,6 @@ import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
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.session.SessionComponent
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 localEchoRepository: LocalEchoRepository
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
override fun injectWith(injector: SessionComponent) {
@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
return try {
val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params(
event = localEvent,
cryptoService = cryptoService
event = localEvent
)
)

View File

@ -15,7 +15,6 @@
*/
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.verification.VerificationService
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.session.EventInsertLiveProcessor
import io.realm.Realm
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber
import java.util.ArrayList
import javax.inject.Inject
internal class VerificationMessageProcessor @Inject constructor(
private val cryptoService: CryptoService,
private val eventDecryptor: EventDecryptor,
private val verificationService: DefaultVerificationService,
@UserId private val userId: 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?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, "")
val result = eventDecryptor.decryptEvent(event, "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,

View File

@ -17,10 +17,6 @@
package org.matrix.android.sdk.internal.database
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.model.EventEntity
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.RealmResults
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber
import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
private val cryptoService: CryptoService)
private val eventDecryptor: EventDecryptor)
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
override val query = Monarchy.Query {
@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
return@forEach
}
val domainEvent = event.asDomain()
decryptIfNeeded(domainEvent)
// decryptIfNeeded(domainEvent)
processors.filter {
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
}.forEach {
@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
}
}
private fun decryptIfNeeded(event: Event) {
if (event.isEncrypted() && event.mxDecryptionResult == null) {
try {
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.v("Failed to decrypt event")
// TODO -> we should keep track of this and retry, or some processing will never be handled
}
}
}
// private fun decryptIfNeeded(event: Event) {
// if (event.isEncrypted() && event.mxDecryptionResult == null) {
// try {
// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
// event.mxDecryptionResult = OlmDecryptionResult(
// payload = result.clearEvent,
// senderKey = result.senderCurve25519Key,
// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
// )
// } catch (e: MXCryptoError) {
// Timber.v("Failed to decrypt event")
// // TODO -> we should keep track of this and retry, or some processing will never be handled
// }
// }
// }
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any {

View File

@ -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.session.identity.DefaultIdentityService
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.job.SyncThread
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 _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService,
@ -162,7 +160,6 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach { it.onStart() }
}
eventBus.register(this)
timelineEventDecryptor.start()
eventSenderProcessor.start()
}
@ -200,7 +197,7 @@ internal class DefaultSession @Inject constructor(
override fun close() {
assert(isOpen)
stopSync()
timelineEventDecryptor.destroy()
// timelineEventDecryptor.destroy()
uiHandler.post {
lifecycleObservers.forEach { it.onStop() }
}

View File

@ -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.SendGossipRequestWorker
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.di.MatrixComponent
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
@ -128,6 +129,8 @@ internal interface SessionComponent {
fun inject(worker: SendGossipWorker)
fun inject(worker: UpdateTrustWorker)
@Component.Factory
interface Factory {
fun create(

View File

@ -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.SharedSecretStorageService
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.tasks.DefaultRedactEventTask
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
@ -333,10 +332,6 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver

View File

@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor(
return roomGetter.getRoom(roomId)
}
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
return roomGetter.getDirectRoomWith(otherUserId)
}

View File

@ -15,7 +15,7 @@
*/
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.Event
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.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
return newState
}
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
private val cryptoService: CryptoService
) : EventInsertLiveProcessor {
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
: EventInsertLiveProcessor {
private val allowedTypes = listOf(
EventType.MESSAGE,

View File

@ -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.query.where
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import javax.inject.Inject
internal interface RoomGetter {
fun getRoom(roomId: String): Room?
fun getDirectRoomWith(otherUserId: String): Room?
fun getDirectRoomWith(otherUserId: String): String?
}
@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 ->
RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.findAll()
.filter { dm -> dm.otherMemberIds.contains(otherUserId) }
.map { it.roomId }
.firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
?.let { roomId -> createRoom(realm, roomId) }
.firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId }
?.roomId
}
}

View File

@ -16,10 +16,10 @@
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.events.model.Event
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.toMedium
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.di.AuthenticatedIdentity
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.data.IdentityStore
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import java.security.InvalidParameterException
import java.util.UUID
import javax.inject.Inject
internal class CreateRoomBodyBuilder @Inject constructor(
@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private val crossSigningService: CrossSigningService,
private val deviceListManager: DeviceListManager,
private val identityStore: IdentityStore,
private val fileUploader: FileUploader,
@AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider
) {
@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
val initialStates = listOfNotNull(
buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params)
buildHistoryVisibilityEvent(params),
buildAvatarEvent(params)
)
.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? {
return params.historyVisibility
?.let {
val contentMap = mapOf("history_visibility" to it)
Event(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
content = contentMap.toContent())
content = mapOf("history_visibility" to it)
)
}
}
@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
throw InvalidParameterException("Unsupported algorithm: $it")
}
val contentMap = mapOf("algorithm" to it)
Event(
type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
content = contentMap.toContent()
content = mapOf("algorithm" to it)
)
}
}

View File

@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
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.room.model.EventAnnotationsSummary
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.Optional
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.asDomain
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.query.where
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.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor
@ -47,11 +46,9 @@ import timber.log.Timber
internal class DefaultRelationService @AssistedInject constructor(
@Assisted private val roomId: String,
@SessionId private val sessionId: String,
// private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
@ -122,7 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
val event = eventFactory
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.also { saveLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun editReply(replyToEdit: TimelineEvent,
@ -139,11 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
compatibilityBodyText
)
.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>>) {
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
fetchEditHistoryTask
.configureWith(params) {
this.callback = callback
@ -156,7 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
?.also { saveLocalEcho(it) }
?: return null
return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {

View File

@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch
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.isAttachmentMessage
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.JsonDict
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.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@ -64,7 +64,7 @@ internal class DefaultSendService @AssistedInject constructor(
private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository,
private val eventSenderProcessor: EventSenderProcessor,
@ -251,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val cancelableBag = CancelableBag()
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
.apply {
keys.forEach { isRoomEncrypted ->
// Should never be empty
@ -282,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
}
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) {

View File

@ -87,7 +87,7 @@ internal class SendEventWorker(context: Context,
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
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()
} catch (exception: Throwable) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {

View File

@ -38,7 +38,7 @@ internal class SendEventQueuedTask(
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
override suspend fun execute() {
sendEventTask.execute(SendEventTask.Params(event, encrypt, cryptoService))
sendEventTask.execute(SendEventTask.Params(event, encrypt))
}
override fun onTaskFailed() {

View File

@ -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
)
}
}
}

View File

@ -16,10 +16,8 @@
package org.matrix.android.sdk.internal.session.room.summary
import dagger.Lazy
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.extensions.tryOrNull
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.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.RoomTopicContent
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.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.asDomain
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.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.membership.RoomDisplayNameResolver
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.RoomSyncUnreadNotifications
import timber.log.Timber
@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
private val eventBus: EventBus) {
private val eventDecryptor: EventDecryptor,
private val crossSigningService: DefaultCrossSigningService) {
fun update(realm: Realm,
roomId: String,
@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor(
}
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}")
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) {
@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
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.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
}
}
}

View File

@ -157,11 +157,14 @@ internal class DefaultTimeline(
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: 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 ->
uiEchoManager.sentEventsUpdated(events)
postSnapshot()
}
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings()
@ -411,13 +414,16 @@ internal class DefaultTimeline(
val builtSendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
sendingEvents.forEach { timelineEventEntity ->
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
val element = timelineEventMapper.map(timelineEventEntity)
uiEchoManager.updateSentStateWithUiEcho(element)
builtSendingEvents.add(element)
}
}
sendingEvents
.map { timelineEventMapper.map(it) }
// Filter out sending event that are not displayable!
.filterEventsWithSettings()
.forEach { timelineEvent ->
if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
builtSendingEvents.add(timelineEvent)
}
}
}
return builtSendingEvents
}
@ -634,7 +640,7 @@ internal class DefaultTimeline(
if (timelineEvent.isEncrypted()
&& 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
@ -786,8 +792,8 @@ internal class DefaultTimeline(
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false
val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>()
val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
val messageContent = it.root.getClearContent().toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else {
true
@ -817,7 +823,7 @@ internal class DefaultTimeline(
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
fun getInMemorySendingEvents(): List<TimelineEvent> {
return inMemorySendingEvents
return inMemorySendingEvents.toList()
}
/**
@ -835,8 +841,13 @@ internal class DefaultTimeline(
inMemorySendingStates.keys.removeAll { key ->
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
)
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
if (existing == null) {

View File

@ -15,24 +15,22 @@
*/
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.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.internal.crypto.NewSessionListener
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.query.where
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 java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.inject.Inject
@SessionScope
internal class TimelineEventDecryptor @Inject constructor(
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor(
synchronized(unknownSessionsFailure) {
for (requests in unknownSessionsFailure.values) {
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
}
}
}
synchronized(existingRequests) {
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
}
}
@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
val eventId = request.eventId
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
val event = request.event
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 {
val result = cryptoService.decryptEvent(event, timelineId)
Timber.v("Successfully decrypted event $eventId")
eventEntity.setDecryptionResult(result)
val result = cryptoService.decryptEvent(request.event, timelineId)
Timber.v("Successfully decrypted event ${event.eventId}")
realm.executeTransaction {
EventEntity.where(it, eventId = event.eventId ?: "")
.findFirst()
?.setDecryptionResult(result)
}
} 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*/) {
// Keep track of unknown sessions to automatically try to decrypt on new session
eventEntity.decryptionErrorCode = e.errorType.name
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
realm.executeTransaction {
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 ->
content.sessionId?.let { sessionId ->
synchronized(unknownSessionsFailure) {
@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
} catch (t: Throwable) {
Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}")
} finally {
synchronized(existingRequests) {
existingRequests.remove(request)
@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor(
}
data class DecryptionRequest(
val eventId: String,
val event: Event,
val timelineId: String
)
}

View File

@ -16,6 +16,9 @@
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.api.session.crypto.MXCryptoError
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.timeline.DefaultTimeline
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.sync.model.InvitedRoomSync
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.RoomSyncEphemeral
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 javax.inject.Inject
@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
private val eventBus: EventBus,
private val timelineEventDecryptor: TimelineEventDecryptor) {
private val eventBus: EventBus) {
sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()

View File

@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val activeCallHandler: ActiveCallHandler
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>(state)

View File

@ -1,9 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$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">%1$s покани %2$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_placed_video_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_made_future_room_visibility">%1$s направи бъдещата история на стаята видима за %2$s</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_unknown">непозната (%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_voip_started">Започна VoIP групов разговор</string>
<string name="notice_voip_finished">Груповият разговор приключи</string>
<string name="notice_avatar_changed_too">(профилната снимка също беше сменена)</string>
<string name="notice_room_name_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_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_crypto_unable_to_decrypt">** Неуспешно разшифроване: %s **</string>
<string name="could_not_redact">Неуспешно премахване</string>
<string name="unable_to_send_message">Неуспешно изпращане на съобщението</string>
<string name="message_failed_to_upload">Неуспешно качване на снимката</string>
<string name="network_error">Грешка в мрежата</string>
<string name="matrix_error">Matrix грешка</string>
<string name="room_error_join_failed_empty_room">В момента не е възможно да се присъедините отново към празна стая.</string>
<string name="encrypted_message">Шифровано съобщение</string>
<string name="medium_email">Имейл адрес</string>
<string name="medium_phone_number">Телефонен номер</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Устройството на подателя не изпрати ключовете за това съобщение.</string>
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
<string name="room_displayname_invite_from">Покана от %s</string>
<string name="room_displayname_room_invite">Покана за стая</string>
<string name="room_displayname_two_members">%1$s и %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s и 1 друг</item>
<item quantity="other">%1$s и %2$d други</item>
</plurals>
<string name="room_displayname_empty_room">Празна стая</string>
<string name="notice_event_redacted">Премахнато съобщение</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_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string>
<string name="initial_sync_start_importing_account">Начална синхронизация:
\nИмпортиране на профил…</string>
<string name="initial_sync_start_importing_account_crypto">Начална синхронизация:
@ -95,12 +78,9 @@
\nИмпортиране на общности</string>
<string name="initial_sync_start_importing_account_data">Начална синхронизация:
\nИмпортиране на данни за профила</string>
<string name="notice_room_update">%s обнови тази стая.</string>
<string name="event_status_sending_message">Изпращане на съобщение…</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_invite_no_invitee_with_reason">поканата на %1$s. Причина: %2$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_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>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s добави %2$s като адрес за тази стая.</item>
<item quantity="other">%1$s добави %2$s като адреси за тази стая.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s премахна %2$s като адрес за тази стая.</item>
<item quantity="other">%1$s премахна %2$s като адреси за тази стая.</item>
</plurals>
<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_unset">%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_end_to_end_ok">%1$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="notice_room_created">%1$s създаде стаята</string>
<string name="summary_you_sent_image">Изпратихте снимка.</string>
<string name="summary_you_sent_sticker">Изпратихте стикер.</string>
<string name="notice_room_invite_no_invitee_by_you">Ваша покана</string>
<string name="notice_room_created_by_you">Създадохте стаята</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_unban_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>

View File

@ -174,7 +174,7 @@
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।</item>
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন।</item>
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %1$s গুলি সরিয়েছেন।</item>
</plurals>
<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>

View File

@ -1,9 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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_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">Uživatel %1$s pozval uživatele %2$s</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_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_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_finished">VoIP konference skončila</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_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_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_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="could_not_redact">Nelze vymazat</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="network_error">Chyba sítě</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="encrypted_message">Šifrovaná zpráva</string>
<string name="medium_email">E-mailová adresa</string>
<string name="medium_phone_number">Telefonní číslo</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_two_members">%1$s a %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s a jeden další</item>
<item quantity="few">%1$s a %2$d další</item>
<item quantity="other">%1$s a %2$d dalších</item>
</plurals>
<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_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_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>
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
\nImportuji data účtu</string>
<string name="event_status_sending_message">Odesílám zprávu…</string>
<string name="initial_sync_start_importing_account_invited_rooms">Úvodní synchronizace:
\nImportuji pozvání</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_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>
@ -107,7 +88,6 @@
<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_sticker">Poslali jste nálepku.</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_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_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_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_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_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_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_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_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="power_level_admin">Správce</string>
<string name="power_level_moderator">Moderátor</string>
<string name="power_level_default">Výchozí</string>
<string name="power_level_custom">Vlastní (%1$d)</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">%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_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_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_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>
<plurals name="notice_room_aliases_added">
<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="other">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
</plurals>
<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="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>
</plurals>
<plurals name="notice_room_aliases_removed">
<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="other">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">Odstranili jste %2$s jako adresu pro tuto místnost.</item>
<item quantity="few">Odstranili jste %2$s jako adresuy pro tuto místnost.</item>
<item quantity="other">Odstranili jste %2$s jako adresy pro tuto místnost.</item>
<item quantity="one">Odstranili jste %1$s jako adresu pro tuto místnost.</item>
<item quantity="few">Odstranili jste %1$s jako adresuy pro tuto místnost.</item>
<item quantity="other">Odstranili jste %1$s jako adresy pro tuto místnost.</item>
</plurals>
<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">%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_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_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_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_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_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_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_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>
</resources>
</resources>

View File

@ -73,14 +73,22 @@
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
<string name="event_status_sending_message">Sende eine Nachricht…</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_crypto">Erste Synchronisation: Importiere Cryptoschlüssel</string>
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation: Importiere Räume</string>
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation: Importiere betretene Räume</string>
<string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation: Importiere eingeladene Räume</string>
<string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation: Importiere verlassene Räume</string>
<string name="initial_sync_start_importing_account_groups">Erste Synchronisation: Importiere Gemeinschaften</string>
<string name="initial_sync_start_importing_account_data">Erste Synchronisation: Importiere Benutzerdaten</string>
<string name="initial_sync_start_importing_account">Erste Synchronisation:
\nImportiere Benutzerkonto…</string>
<string name="initial_sync_start_importing_account_crypto">Erste Synchronisation:
\nImportiere Cryptoschlüssel</string>
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation:
\nImportiere Räume</string>
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation:
\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_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>
@ -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_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_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_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>
@ -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_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_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_direct_room_leave_by_you">Du hast 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_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_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>

View File

@ -1,26 +1,24 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_user_sent_image">%1$s sendis bildon.</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">%1$s invitis uzanton %2$s</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_leave">%1$s foriris</string>
<string name="notice_room_reject">%1$s malakceptis la inviton</string>
<string name="notice_room_join">%1$s envenis</string>
<string name="notice_room_leave">%1$s foriris de la ĉambro</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_unban">%1$s malforbaris 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_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_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="notice_display_name_set">%1$s ŝanĝis sian vidigan 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_removed">%1$s forigis sian vidigan nomon (%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 prezentan nomon de %2$s al %3$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_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</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_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_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string>
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliĝoj.</string>
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde siaj invitoj.</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_world_readable">ĉiu ajn.</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_room_update">%s gradaltigis la ĉambron.</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_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_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_registered_invite">%1$s akceptis la inviton por %2$s</string>
<string name="could_not_redact">Ne povis redakti</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="network_error">Reta 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="medium_email">Retpoŝtadreso</string>
<string name="medium_phone_number">Telefonnumero</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>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s kaj 1 alia</item>
<item quantity="other">%1$s kaj %2$d aliaj</item>
</plurals>
<string name="room_displayname_empty_room">Malplena ĉambro</string>
<string name="initial_sync_start_importing_account">Komenca spegulado:
\nEnportante konton…</string>
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:
@ -88,52 +74,143 @@
\nEnportante komunumojn</string>
<string name="initial_sync_start_importing_account_data">Komenca spegulado:
\nEnportante datumojn de konto</string>
<string name="event_status_sending_message">Sendante mesaĝon…</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_voip_started">Grupa voko komenciĝ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_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_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_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_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_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_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_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_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 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">
<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>
</plurals>
<plurals name="notice_room_aliases_removed">
<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>
</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_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</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_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</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 aliĝi al la ĉambro.</string>
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj enveni.</string>
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj enveni.</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="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>
</resources>
<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="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>
<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>

View File

@ -226,7 +226,7 @@
<plurals name="notice_room_aliases_removed_by_you">
<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>
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>

View File

@ -1,9 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%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">%1$s، %2$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_end_to_end">%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)</string>
<string name="notice_room_update">%s این اتاق را ارتقا داد.</string>
<string name="notice_requested_voip_conference">%1$s درخواست یک گردهمایی صوتی داد</string>
<string name="notice_voip_started">گردهمایی صوتی آغاز شد</string>
<string name="notice_voip_finished">گردهمایی صوتی پایان یافت</string>
<string name="notice_avatar_changed_too">(تصویر هم عوض شد)</string>
<string name="notice_room_name_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_revoked_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_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
<string name="network_error">خطای شبکه</string>
<string name="matrix_error">خطای ماتریکس</string>
<string name="room_error_join_failed_empty_room">در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد‌‌.</string>
<string name="encrypted_message">پیام رمزنگاری شده</string>
<string name="medium_email">نشانی رایانامه</string>
<string name="medium_phone_number">شماره تلفن</string>
<string name="room_displayname_invite_from">دعوت از %s</string>
<string name="room_displayname_room_invite">دعوت اتاق</string>
<string name="room_displayname_two_members">%1$s و %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s و ۱ نفر دیگر</item>
<item quantity="other">%1$s و %2$d نفر دیگر</item>
</plurals>
<string name="room_displayname_empty_room">اتاق خالی</string>
<string name="initial_sync_start_importing_account">همگام‌سازی نخستین:
\nدر حال درون‌ریزی حساب…</string>
<string name="initial_sync_start_importing_account_crypto">همگام‌سازی نخستین:
@ -93,10 +78,8 @@
\nدر حال درون‌ریزی انجمن‌ها</string>
<string name="initial_sync_start_importing_account_data">همگام‌سازی نخستین:
\nدر حال درون‌ریزی داده‌های حساب</string>
<string name="event_status_sending_message">در حال فرستادن پیام…</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_with_reason">%1$s، %2$s را دعوت کرد. دلیل: %3$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_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>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s، %2$s را به عنوان نشانی‌ای برای این اتاق افزود.</item>
<item quantity="other">%1$s، %2$s را به عنوان نشانی‌هایی برای این اتاق افزود.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s، %2$s را به عنوان نشانی‌ای برای این اتاق پاک کرد.</item>
<item quantity="other">%1$s، %3$s را به عنوان نشانی‌هایی برای این اتاق پاک کرد.</item>
</plurals>
<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_unset">%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_end_to_end_ok">%1$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="notice_room_created">%1$s اتاق را ایجاد کرد</string>
<string name="notice_profile_change_redacted">%1$s نمایه‌اش را به‌روز کرد %2$s</string>
<string name="could_not_redact">نمی‌توان ویرایش کرد</string>
<string name="summary_you_sent_image">تصویری فرستادید.</string>
<string name="summary_you_sent_sticker">برچسبی فرستادید.</string>
<string name="notice_room_invite_no_invitee_by_you">دعوتتان</string>
<string name="notice_room_created_by_you">اتاق را ایجاد کردید</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_end_to_end_by_you">رمزنگاری سرتاسری را روشن کردید (%1$s)</string>
<string name="notice_room_update_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_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_revoked_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_by_you">ابزارک %1$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_modified">%1$s ابزارک %2$s را دستکاری کرد</string>
<string name="notice_widget_modified_by_you">ابزارک %1$s را دستکاری کردید</string>
<string name="power_level_admin">مدیر</string>
<string name="power_level_moderator">ناظم</string>
<string name="power_level_default">پیش‌گزیده</string>
<string name="power_level_custom">سفارشی (%1$d)</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">%1$s سطح قدرت %2$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_with_reason_by_you">%1$s را دعوت کردید. دلیل: %2$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_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>
<plurals name="notice_room_aliases_added_by_you">
<item quantity="one">نشانی %1$s را به این اتاق افزودید.</item>
<item quantity="other">نشانی‌های %1$s را به این اتاق افزودید.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">نشانی %1$s ار از این اتاق برداشتید.</item>
<item quantity="other">نشانی‌های %1$s ار از این اتاق برداشتید.</item>
</plurals>
<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_unset_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_end_to_end_ok_by_you">رمزنگاری سرتاسری را روشن کردید.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s).</string>
</resources>
<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>
<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>

View File

@ -1,9 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s : %2$s</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">%1$s a invité %2$s</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_reject">%1$s a rejeté linvitation</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_ban">%1$s a banni %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 exclus %2$s</string>
<string name="notice_room_withdraw">%1$s a annulé linvitation de %2$s</string>
<string name="notice_avatar_url_changed">%1$s a changé davatar</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_removed">%1$s a supprimé son nom affiché (%2$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é (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_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>
@ -31,54 +29,39 @@
<string name="notice_room_visibility_world_readable">nimporte qui.</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_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_finished">Téléconférence VoIP terminée</string>
<string name="notice_avatar_changed_too">(lavatar a aussi changé)</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_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_registered_invite">%1$s a accepté linvitation pour %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** Déchiffrement impossible : %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Lappareil de lexpéditeur ne nous a pas envoyé les clés pour ce message.</string>
<string name="could_not_redact">Effacement impossible</string>
<string name="unable_to_send_message">Envoi du message impossible</string>
<string name="message_failed_to_upload">Lenvoi de limage a échoué</string>
<string name="network_error">Erreur de réseau</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="encrypted_message">Message chiffré</string>
<string name="medium_email">Adresse e-mail</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="room_displayname_invite_from">Invitation de %s</string>
<string name="room_displayname_room_invite">Invitation au salon</string>
<string name="room_displayname_empty_room">Salon vide</string>
<string name="room_displayname_two_members">%1$s et %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s et 1 autre</item>
<item quantity="other">%1$s et %2$d autres</item>
</plurals>
<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_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="initial_sync_start_importing_account">Synchronisation initiale :
\nImportation du compte…</string>
<string name="initial_sync_start_importing_account_crypto">Synchronisation initiale :
@ -95,12 +78,9 @@
\nImportation des communautés</string>
<string name="initial_sync_start_importing_account_data">Synchronisation initiale :
\nImportation des données du compte</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="clear_timeline_send_queue">Vider la file denvoi</string>
<string name="notice_room_third_party_revoked_invite">%1$s a révoqué linvitation 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_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_reject_with_reason">%1$s a refusé linvitation. 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_unban_with_reason">%1$s a révoqué le bannissement 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_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 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_revoked_invite_with_reason">%1$s a révoqué linvitation de %2$s à rejoindre le salon. Raison : %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s a accepté linvitation pour %2$s. Raison : %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s a annulé linvitation de %2$s. Raison : %3$s</string>
<plurals name="notice_room_aliases_added">
<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>
</plurals>
<plurals name="notice_room_aliases_removed">
<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>
</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_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é ladresse 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_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_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 lancienne vérification de clés pour vérifier les clés.</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é ladresse 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é linvitation de %1$s. Raison : %2$s</string>
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Vous avez accepté linvitation pour %1$s. Raison : %2$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Vous avez révoqué linvitation 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é linvitation. 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é linvitation 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 lhistorique 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 à lappel.</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é linvitation 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é linvitation 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>

View File

@ -183,7 +183,7 @@
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<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>
<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>

View File

@ -172,7 +172,7 @@
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<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>
<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>

View File

@ -1,8 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="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>

View File

@ -182,7 +182,7 @@
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<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>
<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>

View File

@ -225,4 +225,6 @@
<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>
<string name="notice_direct_room_update_by_you">Вы обновили.</string>
<string name="notice_direct_room_update">%s обновлена.</string>
</resources>

View File

@ -183,8 +183,8 @@
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<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="other">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 %1$s pre túto miestnosť.</item>
</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_canonical_alias_set_by_you">Nastavili ste hlavnú adresu tejto miestnosti %1$s.</string>

View File

@ -1,4 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$s</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_voip_started">Konferenca VoIP filloi</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_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_room_third_party_registered_invite">%1$s pranoi ftesën tuaj për %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** Sarrihet 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="could_not_redact">Su redaktua dot</string>
<string name="unable_to_send_message">Sarrihet të dërgohet mesazh</string>
<string name="message_failed_to_upload">Ngarkimi i figurës dështoi</string>
<string name="network_error">Gabim rrjeti</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="encrypted_message">U fshehtëzua mesazhi</string>
<string name="encrypted_message">Mesazh i fshehtëzuar</string>
<string name="medium_email">Adresë email</string>
<string name="medium_phone_number">Numër telefoni</string>
<string name="room_displayname_invite_from">Ftesë nga %s</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_empty_room">Dhomë e zbrazët</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_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>
@ -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_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_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>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s dhe 1 tjetër</item>
<item quantity="other">%1$s dhe %2$d të tjerë</item>
</plurals>
<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_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_room_update">%s e përmirësoi këtë dhomë.</string>
<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
\nPo importohet llogaria…</string>
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
@ -95,10 +79,8 @@
\nPo importohen Bashkësi</string>
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
\nPo importohet të Dhëna Llogarie</string>
<string name="event_status_sending_message">Po dërgohet mesazh…</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_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>
@ -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_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>
<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="other">%1$s shtoi %2$s si adresa për këtë dhomë.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<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>
</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_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_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_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="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 tju duhet të përdorni verifikim të dikurshëm kyçesh.</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_sticker">Dërguat një ngjitës.</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_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_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_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_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_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_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_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_modified">%1$s ndryshoi widget-in %2$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_moderator">Moderator</string>
<string name="power_level_default">Parazgjedhje</string>
<string name="power_level_custom">Vetjake (%1$d)</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">%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_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_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_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>
<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="other">Shtuat %1$s si adresa për këtë dhomë.</item>
</plurals>
<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="other">Hoqët %1$s si adresa për këtë dhomë.</item>
</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_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_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_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>
</resources>
</resources>

View File

@ -174,7 +174,7 @@
</plurals>
<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="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>
<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>

View File

@ -177,7 +177,7 @@
<item quantity="other">您新增了 %1$s 为此聊天室的地址。</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="other">您移除了此聊天室的 %2$s 地址。</item>
<item quantity="other">您移除了此聊天室的 %1$s 地址。</item>
</plurals>
<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>
@ -196,7 +196,7 @@
<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">您使未来的消息对 %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_direct_room_leave_by_you">您离开了聊天室</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_created_by_you">您创建了讨论</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>

View File

@ -177,7 +177,7 @@
<item quantity="other">您為此聊天室新增了 %1$s 作為地址。</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="other">您為此聊天室移除了 %2$s 作為地址。</item>
<item quantity="other">您為此聊天室移除了 %1$s 作為地址。</item>
</plurals>
<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>

View File

@ -225,7 +225,7 @@
<plurals name="notice_room_aliases_removed_by_you">
<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>
<string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string>

View File

@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt
./vector/src/main/res/color \
./vector/src/main/res/layout \
./vector/src/main/res/values \
./vector/src/main/res/values-v21 \
./vector/src/main/res/xml
resultForbiddenStringInResource=$?

View File

@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# 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
enum class===82
enum class===83
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -45,6 +45,10 @@ parser.add_argument('-e',
'--expecting',
type=int,
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',
'--directory',
default="",
@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit'))
print(" git commit message : \"%s\"" % data0.get('message'))
print(" build state : %s" % data0.get('state'))
error = False
if data0.get('state') != 'passed':
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
@ -110,8 +119,11 @@ data = json.loads(r.content.decode())
print(" %d artifact(s) found." % 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)))
exit(1)
print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
if args.ignoreErrors:
error = True
else:
exit(1)
if args.verbose:
print("Json data:")
@ -128,8 +140,6 @@ else:
if not args.simulate:
os.mkdir(targetDir)
error = False
for elt in data:
if args.verbose:
print()
@ -157,7 +167,7 @@ for elt in data:
print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash))
if error:
print("❌ Error(s) occurred, check the log")
print("❌ Error(s) occurred, please check the log")
exit(1)
else:
print("Done!")

View File

@ -39,9 +39,9 @@ def generateVersionCodeFromVersionName() {
def getVersionCode() {
if (gitBranchName() == "develop") {
return generateVersionCodeFromTimestamp() * 10
return generateVersionCodeFromTimestamp()
} else {
return generateVersionCodeFromVersionName() * 10
return generateVersionCodeFromVersionName()
}
}
@ -166,13 +166,14 @@ android {
}
applicationVariants.all { variant ->
// assign different version code for each output
def baseVariantVersion = variant.versionCode * 10
variant.outputs.each { output ->
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
// 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
output.versionCodeOverride = variant.versionCode + baseAbiVersionCode
print " \t-> VersionCode = " + output.versionCodeOverride + "\n"
output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode
print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n"
}
}

View File

@ -42,6 +42,7 @@ import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.di.VectorComponent
import im.vector.app.core.extensions.configureAndStart
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.disclaimer.doNotShowDisclaimerDialog
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
@ -89,6 +90,7 @@ class VectorApplication :
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
@ -173,6 +175,7 @@ class VectorApplication :
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager)
// This should be done as early as possible
// initKnownEmojiHashSet(appContext)

View File

@ -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.media.RoomUploadsMediaFragment
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.VectorSettingsLabsFragment
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
@ -292,6 +293,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsPinFragment::class)
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsGeneralFragment::class)
fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment
@Binds
@IntoMap
@FragmentKey(PushRulesFragment::class)

View File

@ -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)
}
}

View File

@ -44,8 +44,10 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute lateinit var stringProvider: StringProvider
@EpoxyAttribute var izLowPriority: Boolean = false
@EpoxyAttribute var izFavorite: Boolean = false
@EpoxyAttribute var settingsClickListener: ClickListener? = null
@EpoxyAttribute var lowPriorityClickListener: ClickListener? = null
@EpoxyAttribute var favoriteClickListener: ClickListener? = null
override fun bind(holder: Holder) {
@ -53,17 +55,44 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
avatarRenderer.render(matrixItem, holder.avatar)
holder.avatar.onClick(settingsClickListener)
holder.roomName.setTextOrHide(matrixItem.displayName)
setLowPriorityState(holder, izLowPriority)
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 {
// Immediate echo
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
favoriteClickListener?.invoke()
}
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) {
val tintColor: Int
if (isFavorite) {
@ -81,6 +110,7 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
class Holder : VectorEpoxyHolder() {
val avatar by bind<ImageView>(R.id.bottomSheetRoomPreviewAvatar)
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 roomSettings by bind<View>(R.id.bottomSheetRoomPreviewSettings)
}

View File

@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix {
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor)
val colorBuffer = IntArray(width * height)
var rowOffset = 0
for (y in 0 until height) {
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)
}

View File

@ -17,6 +17,8 @@
package im.vector.app.core.services
import android.content.Context
import android.media.Ringtone
import android.media.RingtoneManager
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
@ -25,7 +27,26 @@ import androidx.core.content.getSystemService
import im.vector.app.R
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
) {
@ -44,12 +65,12 @@ class CallRingPlayer(
try {
if (player?.isPlaying == false) {
player?.start()
Timber.v("## VOIP Starting ringing")
Timber.v("## VOIP Starting ringing outgoing")
} else {
Timber.v("## VOIP already playing")
}
} catch (failure: Throwable) {
Timber.e(failure, "## VOIP Failed to start ringing")
Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null
}
} else {
@ -74,7 +95,7 @@ class CallRingPlayer(
} else {
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build())
}
return mediaPlayer

Some files were not shown because too many files have changed in this diff Show More