From 49b7726ac866ccc7add1c516c8865eb3847dcb14 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 15:09:01 +0200 Subject: [PATCH 001/405] - Integrate /relations API to create a live thread timeline --- .../session/room/timeline/ChunkEntityTest.kt | 24 +- .../sdk/api/session/events/model/Event.kt | 6 +- .../session/room/timeline/TimelineEvent.kt | 1 + .../database/RealmSessionStoreMigration.kt | 14 +- .../database/helper/ChunkEntityHelper.kt | 9 +- .../database/helper/ThreadEventsHelper.kt | 2 + .../database/mapper/TimelineEventMapper.kt | 1 + .../internal/database/model/ChunkEntity.kt | 22 +- .../database/model/TimelineEventEntity.kt | 3 + .../database/query/ChunkEntityQueries.kt | 14 +- .../EventAnnotationsSummaryEntityQuery.kt | 2 +- .../EventRelationsAggregationProcessor.kt | 10 +- .../sdk/internal/session/room/RoomAPI.kt | 2 + .../room/relation/DefaultRelationService.kt | 8 +- .../threads/FetchThreadTimelineTask.kt | 208 ++++++++++++------ .../session/room/timeline/DefaultTimeline.kt | 4 + .../room/timeline/DefaultTimelineService.kt | 5 +- .../room/timeline/LoadTimelineStrategy.kt | 58 ++++- .../session/room/timeline/TimelineChunk.kt | 45 +++- .../room/timeline/TokenChunkEventPersistor.kt | 24 +- .../sync/handler/room/RoomSyncHandler.kt | 35 ++- .../list/viewmodel/ThreadListController.kt | 2 +- 22 files changed, 395 insertions(+), 104 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt index 69ae57e644..5c011c8b2f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt @@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest { val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { realm.copyToRealm(it) } - chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent( + roomId = ROOM_ID, + eventEntity = fakeEvent, + direction = PaginationDirection.FORWARDS, + roomMemberContentsByUser = emptyMap()) chunk.timelineEvents.size shouldBeEqualTo 1 } } @@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest { val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { realm.copyToRealm(it) } - chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) - chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent( + roomId = ROOM_ID, + eventEntity = fakeEvent, + direction = PaginationDirection.FORWARDS, + roomMemberContentsByUser = emptyMap()) + chunk.addTimelineEvent( + roomId = ROOM_ID, + eventEntity = fakeEvent, + direction = PaginationDirection.FORWARDS, + roomMemberContentsByUser = emptyMap()) chunk.timelineEvents.size shouldBeEqualTo 1 } } @@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest { val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let { realm.copyToRealm(it) } - addTimelineEvent(roomId, fakeEvent, direction, emptyMap()) + addTimelineEvent( + roomId = roomId, + eventEntity = fakeEvent, + direction = direction, + roomMemberContentsByUser = emptyMap()) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index df57ca5681..ed9a057375 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -201,7 +201,11 @@ data class Event( */ fun getDecryptedTextSummary(): String? { if (isRedacted()) return "Message Deleted" - val text = getDecryptedValue() ?: return null + val text = getDecryptedValue() ?: run { + if (isPoll()) {return getPollQuestion() ?: "created a poll."} + return null + } + return when { isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isFileMessage() -> "sent a file." diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6f8bae876b..c03d0fd17b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -54,6 +54,7 @@ data class TimelineEvent( * It's not unique on the timeline as it's reset on each chunk. */ val displayIndex: Int, + var ownedByThreadChunk: Boolean = false, val senderInfo: SenderInfo, val annotations: EventAnnotationsSummary? = null, val readReceipts: List = emptyList() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index dfb0915566..056c4b0ceb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 24L + const val SESSION_STORE_SCHEMA_VERSION = 26L } /** @@ -94,6 +94,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 21) migrateTo22(realm) if (oldVersion <= 22) migrateTo23(realm) if (oldVersion <= 23) migrateTo24(realm) + if (oldVersion <= 24) migrateTo25(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -489,4 +490,15 @@ internal class RealmSessionStoreMigration @Inject constructor( ?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java) ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true) } + + private fun migrateTo25(realm: DynamicRealm){ + Timber.d("Step 24 -> 25") + realm.schema.get("ChunkEntity") + ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED) + + realm.schema.get("TimelineEventEntity") + ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java) + + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 289db9fa15..007017510c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberContentsByUser: Map? = null) { + ownedByThreadChunk: Boolean = false, + roomMemberContentsByUser: Map? = null): TimelineEventEntity? { val eventId = eventEntity.eventId if (timelineEvents.find(eventId) != null) { - return + return null } val displayIndex = nextDisplayIndex(direction) val localId = TimelineEventEntity.nextId(realm) val senderId = eventEntity.sender ?: "" // Update RR for the sender of a new message with a dummy one - val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId) + val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null val timelineEventEntity = realm.createObject().apply { this.localId = localId this.root = eventEntity @@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, ?.also { it.cleanUp(eventEntity.sender) } this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex + this.ownedByThreadChunk = ownedByThreadChunk val roomMemberContent = roomMemberContentsByUser?.get(senderId) this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName @@ -113,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, } // numberOfTimelineEvents++ timelineEvents.add(timelineEventEntity) + return timelineEventEntity } private fun computeIsUnique( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index f703bfaf82..7f6b64da75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -98,6 +98,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: val messages = TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .distinct(TimelineEventEntityFields.ROOT.EVENT_ID) .count() .toInt() @@ -156,6 +157,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) + .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false) .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt index f3bea68c26..1020fa33da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt @@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, avatarUrl = timelineEventEntity.senderAvatar ), + ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk, readReceipts = readReceipts ?.distinctBy { it.user diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index c45c27ed08..8e4d6fc916 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -23,6 +23,7 @@ import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.extensions.clearWith +import timber.log.Timber internal open class ChunkEntity(@Index var prevToken: String? = null, // Because of gaps we can have several chunks with nextToken == null @@ -33,7 +34,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var timelineEvents: RealmList = RealmList(), // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, - @Index var isLastBackward: Boolean = false + @Index var isLastBackward: Boolean = false, + // Threads + @Index var rootThreadEventId: String? = null, + @Index var isLastForwardThread: Boolean = false, ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" @@ -58,3 +62,19 @@ internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRo } deleteFromRealm() } + +/** + * Delete the chunk along with the thread events that were temporarily created + */ +internal fun ChunkEntity.deleteAndClearThreadEvents() { + assertIsManaged() + timelineEvents + .filter { it.ownedByThreadChunk } + .forEach { + it.deleteOnCascade(false) + } + deleteFromRealm() +} + + + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt index 185f0e2dcc..aacd6570bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, var senderMembershipEventId: String? = null, + // ownedByThreadChunk indicates that the current TimelineEventEntity belongs + // to a thread chunk and is a temporarily event. + var ownedByThreadChunk: Boolean = false, var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt index 156a8dd767..ece46555a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -45,10 +45,22 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) .findFirst() } - +internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? { + return where(realm, roomId) + .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true) + .findFirst() +} +internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? { + return where(realm, roomId) + .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray()) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true) + .findFirst() +} internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults { return realm.where() .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) + .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) .findAll() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index 14cb7e22da..6caa832110 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId this.roomId = roomId } // Denormalization - TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach { it.annotations = obj } return obj diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index acceaf6e24..2eebb70bdc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where @@ -117,8 +118,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() ?.let { - TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst() - ?.let { tet -> tet.annotations = it } + TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() + ?.forEach { tet -> tet.annotations = it } } } @@ -335,7 +336,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } if (!isLocalEcho) { - val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + val replaceEvent = TimelineEventEntity + .where(realm, roomId, eventId) + .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false) + .findFirst() handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 399bfbd0e4..86929e013f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -227,6 +227,8 @@ internal interface RoomAPI { @Path("eventId") eventId: String, @Path("relationType") relationType: String, @Path("eventType") eventType: String, + @Query("from") from: String? = null, + @Query("to") to: String? = null, @Query("limit") limit: Int? = null ): RelationsResponse diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 3abf28fdd4..d22583e8b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -206,7 +206,13 @@ internal class DefaultRelationService @AssistedInject constructor( } override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean { - return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId)) + fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( + roomId, + rootThreadEventId, + null, + 10 + )) + return true } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index e0d501c515..6b071fbd6e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.helper.addTimelineEvent -import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity @@ -36,8 +35,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -47,16 +48,39 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject -internal interface FetchThreadTimelineTask : Task { +/*** + * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API + * + * + * How it works + * + * The problem? + * - We cannot use the existing timeline architecture to paginate through the timeline + * - We want our new events to be live, so any interactions with them like reactions will continue to work. We should + * handle appropriately the existing events from /messages api with the new events from /relations. + * - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response + * + * The solution + * We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api + * We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and + * all reactions, edits etc will continue to work. If the events do not exists we create them + * and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline + * + */ +internal interface FetchThreadTimelineTask : Task { data class Params( val roomId: String, - val rootThreadEventId: String + val rootThreadEventId: String, + val from: String?, + val limit: Int + ) } @@ -69,93 +93,133 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( private val cryptoService: DefaultCryptoService ) : FetchThreadTimelineTask { - override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean { + enum class Result { + SHOULD_FETCH_MORE, + REACHED_END, + SUCCESS + } + + override suspend fun execute(params: FetchThreadTimelineTask.Params): Result { val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val response = executeRequest(globalErrorReceiver) { roomAPI.getRelations( roomId = params.roomId, eventId = params.rootThreadEventId, relationType = RelationType.IO_THREAD, + from = params.from, eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE, - limit = 2000 + limit = params.limit ) } - val threadList = response.chunks + listOfNotNull(response.originalEvent) + Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ") + return handleRelationsResponse(response, params) + } - return storeNewEventsIfNeeded(threadList, params.roomId) + private suspend fun handleRelationsResponse(response: RelationsResponse, + params: FetchThreadTimelineTask.Params): Result { + + val threadList = response.chunks + val threadRootEvent = response.originalEvent + val hasReachEnd = response.nextBatch == null + + monarchy.awaitTransaction { realm -> + + val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId) + ?: run { + return@awaitTransaction + } + + threadChunk.prevToken = response.nextBatch + val roomMemberContentsByUser = HashMap() + + for (event in threadList) { + if (event.eventId == null || event.senderId == null || event.type == null) { + continue + } + + if (threadChunk.timelineEvents.find(event.eventId) != null) { + // Event already exists in thread chunk, skip it + Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it") + continue + } + + val timelineEvent = TimelineEventEntity + .where(realm, roomId = params.roomId, event.eventId) + .findFirst() + + if (timelineEvent != null) { + // Event already exists but not in the thread chunk + // Lets added there + Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end") + threadChunk.timelineEvents.add(timelineEvent) + } else { + Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!") + val eventEntity = createEventEntity(params.roomId, event, realm) + roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId) + threadChunk.addTimelineEvent( + roomId = params.roomId, + eventEntity = eventEntity, + direction = PaginationDirection.FORWARDS, + ownedByThreadChunk = true, + roomMemberContentsByUser = roomMemberContentsByUser) + } + } + + if (hasReachEnd) { + val rootThread = TimelineEventEntity + .where(realm, roomId = params.roomId, params.rootThreadEventId) + .findFirst() + if (rootThread != null) { + // If root thread event already exists add it to our chunk + threadChunk.timelineEvents.add(rootThread) + Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!") + } else if (threadRootEvent?.senderId != null) { + // Case when thread event is not in the device + Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one") + val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm) + roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId) + threadChunk.addTimelineEvent( + roomId = params.roomId, + eventEntity = eventEntity, + direction = PaginationDirection.FORWARDS, + ownedByThreadChunk = true, + roomMemberContentsByUser = roomMemberContentsByUser) + } + } + } + + return if (hasReachEnd) { + Result.REACHED_END + } else { + Result.SHOULD_FETCH_MORE + } + } + + // TODO Reuse this function to all the app + /** + * If we don't have any new state on this user, get it from db + */ + private fun HashMap.addSenderState(realm: Realm, roomId: String, senderId: String) { + getOrPut(senderId) { + CurrentStateEventEntity + .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER) + ?.root?.asDomain() + ?.getFixedRoomMemberContent() + } } /** - * Store new events if they are not already received, and returns weather or not, - * a timeline update should be made - * @param threadList is the list containing the thread replies - * @param roomId the roomId of the the thread - * @return + * Create an EventEntity to be added in the TimelineEventEntity */ - private suspend fun storeNewEventsIfNeeded(threadList: List, roomId: String): Boolean { - var eventsSkipped = 0 - monarchy - .awaitTransaction { realm -> - val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) - - val optimizedThreadSummaryMap = hashMapOf() - val roomMemberContentsByUser = HashMap() - - for (event in threadList.reversed()) { - if (event.eventId == null || event.senderId == null || event.type == null) { - eventsSkipped++ - continue - } - - if (EventEntity.where(realm, event.eventId).findFirst() != null) { - // Skip if event already exists - eventsSkipped++ - continue - } - if (event.isEncrypted()) { - // Decrypt events that will be stored - decryptIfNeeded(event, roomId) - } - - handleReaction(realm, event, roomId) - - val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) - - // Sender info - roomMemberContentsByUser.getOrPut(event.senderId) { - // If we don't have any new state on this user, get it from db - val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root - rootStateEvent?.asDomain()?.getFixedRoomMemberContent() - } - - chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) - eventEntity.rootThreadEventId?.let { - // This is a thread event - optimizedThreadSummaryMap[it] = eventEntity - } ?: run { - // This is a normal event or a root thread one - optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity - } - } - - optimizedThreadSummaryMap.updateThreadSummaryIfNeeded( - roomId = roomId, - realm = realm, - currentUserId = userId, - shouldUpdateNotifications = false - ) - } - Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}") - - return eventsSkipped == threadList.size + private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) } /** * Invoke the event decryption mechanism for a specific event */ - private fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 3dd4225b2c..5662986663 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer @@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + fetchThreadTimelineTask: FetchThreadTimelineTask, timelineEventMapper: TimelineEventMapper, timelineInput: TimelineInput, threadsAwarenessHandler: ThreadsAwarenessHandler, @@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String, realm = backgroundRealm, eventDecryptor = eventDecryptor, paginationTask = paginationTask, + realmConfiguration = realmConfiguration, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + fetchThreadTimelineTask = fetchThreadTimelineTask, getContextOfEventTask = getEventTask, timelineInput = timelineInput, timelineEventMapper = timelineEventMapper, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index d7d61f0b47..df552b6178 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.TaskExecutor @@ -55,6 +56,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val eventDecryptor: TimelineEventDecryptor, private val paginationTask: PaginationTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val timelineEventMapper: TimelineEventMapper, private val loadRoomMembersTask: LoadRoomMembersTask, private val threadsAwarenessHandler: ThreadsAwarenessHandler, @@ -76,10 +78,11 @@ internal class DefaultTimelineService @AssistedInject constructor( realmConfiguration = monarchy.realmConfiguration, coroutineDispatchers = coroutineDispatchers, paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, timelineEventMapper = timelineEventMapper, timelineInput = timelineInput, eventDecryptor = eventDecryptor, - fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + fetchThreadTimelineTask = fetchThreadTimelineTask, loadRoomMembersTask = loadRoomMembersTask, readReceiptHandler = readReceiptHandler, getEventTask = contextOfEventTask, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index f332c4a35f..867589ccc0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -19,20 +19,28 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm +import io.realm.RealmConfiguration import io.realm.RealmResults +import io.realm.kotlin.createObject import kotlinx.coroutines.CompletableDeferred import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState 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.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import timber.log.Timber import java.util.concurrent.atomic.AtomicReference /** @@ -76,6 +84,8 @@ internal class LoadTimelineStrategy( val realm: AtomicReference, val eventDecryptor: TimelineEventDecryptor, val paginationTask: PaginationTask, + val realmConfiguration: RealmConfiguration, + val fetchThreadTimelineTask: FetchThreadTimelineTask, val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, val getContextOfEventTask: GetContextOfEventTask, val timelineInput: TimelineInput, @@ -90,7 +100,6 @@ internal class LoadTimelineStrategy( private var getContextLatch: CompletableDeferred? = null private var chunkEntity: RealmResults? = null private var timelineChunk: TimelineChunk? = null - private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults, changeSet: OrderedCollectionChangeSet -> // Can be call either when you open a permalink on an unknown event // or when there is a gap in the timeline. @@ -170,6 +179,9 @@ internal class LoadTimelineStrategy( getContextLatch?.cancel() chunkEntity = null timelineChunk = null + if(mode is Mode.Thread) { + clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId) + } } suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { @@ -185,6 +197,9 @@ internal class LoadTimelineStrategy( return LoadMoreResult.FAILURE } } + if (mode is Mode.Thread) { + return timelineChunk?.loadMoreThread(count, Timeline.Direction.BACKWARDS) ?: LoadMoreResult.FAILURE + } return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE } @@ -201,7 +216,7 @@ internal class LoadTimelineStrategy( } private fun buildSendingEvents(): List { - return if (hasReachedLastForward()) { + return if (hasReachedLastForward() || mode is Mode.Thread) { sendingEventsDataSource.buildSendingEvents() } else { emptyList() @@ -219,13 +234,48 @@ internal class LoadTimelineStrategy( ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId)) } is Mode.Thread -> { + recreateThreadChunkEntity(realm, mode.rootThreadEventId) ChunkEntity.where(realm, roomId) - .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true) .findAll() } } } + /** + * Clear any existing thread chunk entity and create a new one, with the + * rootThreadEventId included + */ + private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) { + realm.executeTransaction { + // Lets delete the chunk and start a new one + ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let { + Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..") + } + val threadChunk = it.createObject().apply { + Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId") + this.rootThreadEventId = rootThreadEventId + this.isLastForwardThread = true + } + if (threadChunk.isValid) { + RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk) + } + } + } + + /** + * Clear any existing thread chunk + */ + private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) { + realm.executeTransaction { + ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let { + Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..") + } + + } + } + private fun hasReachedLastForward(): Boolean { return timelineChunk?.hasReachedLastForward().orFalse() } @@ -237,8 +287,10 @@ internal class LoadTimelineStrategy( timelineSettings = dependencies.timelineSettings, roomId = roomId, timelineId = timelineId, + fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask, eventDecryptor = dependencies.eventDecryptor, paginationTask = dependencies.paginationTask, + realmConfiguration = dependencies.realmConfiguration, fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask, timelineEventMapper = dependencies.timelineEventMapper, uiEchoManager = uiEchoManager, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 8507b63d1f..7f6e5b6c7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener +import io.realm.RealmConfiguration import io.realm.RealmObjectChangeListener import io.realm.RealmQuery import io.realm.RealmResults @@ -36,6 +37,8 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.Collections @@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, private val timelineSettings: TimelineSettings, private val roomId: String, private val timelineId: String, + private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val eventDecryptor: TimelineEventDecryptor, private val paginationTask: PaginationTask, + private val realmConfiguration: RealmConfiguration, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, private val uiEchoManager: UIEchoManager? = null, @@ -142,6 +147,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, val loadFromStorage = loadFromStorage(count, direction).also { logLoadedFromStorage(it, direction) } + if (loadFromStorage.numberOfEvents == 6) { + Timber.i("here") + } val offsetCount = count - loadFromStorage.numberOfEvents @@ -158,6 +166,29 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } } + /** + * This function will fetch more live thread timeline events using the /relations api. It will + * always fetch results, while we want our data to be up to dated. + */ + suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult { + + return if (direction == Timeline.Direction.BACKWARDS) { + try { + fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( + roomId, + timelineSettings.rootThreadEventId!!, + chunkEntity.prevToken, + count + )).toLoadMoreResult() + } catch (failure: Throwable) { + Timber.e(failure, "Failed to fetch thread timeline events from the server") + LoadMoreResult.FAILURE + } + } else { + LoadMoreResult.FAILURE + } + } + private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult { return if (direction == Timeline.Direction.FORWARDS) { val nextChunkEntity = chunkEntity.nextChunk @@ -287,7 +318,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, * @return the number of events loaded. If we are in a thread timeline it also returns * whether or not we reached the end/root message */ - private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage { + private fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage { val displayIndex = getNextDisplayIndex(direction) ?: return LoadedFromStorage() val baseQuery = timelineEventEntities.where() @@ -414,6 +445,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } } + private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult { + return when (this) { + DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END + DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE, + DefaultFetchThreadTimelineTask.Result.SUCCESS -> LoadMoreResult.SUCCESS + } + } + private fun getOffsetIndex(): Int { var offset = 0 var currentNextChunk = nextChunk @@ -455,6 +494,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } } } + if (insertions.isNotEmpty() || modifications.isNotEmpty()) { onBuiltEvents(true) } @@ -489,6 +529,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, timelineId = timelineId, eventDecryptor = eventDecryptor, paginationTask = paginationTask, + realmConfiguration = realmConfiguration, + fetchThreadTimelineTask = fetchThreadTimelineTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, timelineEventMapper = timelineEventMapper, uiEchoManager = uiEchoManager, @@ -535,7 +577,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR .or() .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId) .endGroup() - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .findAll() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 6607e71bd9..63383a99b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.find @@ -49,10 +50,10 @@ import javax.inject.Inject * Insert Chunk in DB, and eventually link next and previous chunk in db. */ internal class TokenChunkEventPersistor @Inject constructor( - @SessionDatabase private val monarchy: Monarchy, - @UserId private val userId: String, - private val lightweightSettingsStorage: LightweightSettingsStorage, - private val liveEventManager: Lazy) { + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String, + private val lightweightSettingsStorage: LightweightSettingsStorage, + private val liveEventManager: Lazy) { enum class Result { SHOULD_FETCH_MORE, @@ -145,9 +146,12 @@ internal class TokenChunkEventPersistor @Inject constructor( if (event.eventId == null || event.senderId == null) { return@forEach } - // We check for the timeline event with this id + // We check for the timeline event with this id, but not in the thread chunk val eventId = event.eventId - val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + val existingTimelineEvent = TimelineEventEntity + .where(realm, roomId, eventId) + .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false) + .findFirst() // If it exists, we want to stop here, just link the prevChunk val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() if (existingChunk != null) { @@ -173,7 +177,7 @@ internal class TokenChunkEventPersistor @Inject constructor( return@processTimelineEvents } val ageLocalTs = event.unsignedData?.age?.let { now - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { val contentToUse = if (direction == PaginationDirection.BACKWARDS) { event.prevContent @@ -183,7 +187,11 @@ internal class TokenChunkEventPersistor @Inject constructor( roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } liveEventManager.get().dispatchPaginatedEventReceived(event, roomId) - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + currentChunk.addTimelineEvent( + roomId = roomId, + eventEntity = eventEntity, + direction = direction, + roomMemberContentsByUser = roomMemberContentsByUser) if (lightweightSettingsStorage.areThreadMessagesEnabled()) { eventEntity.rootThreadEventId?.let { // This is a thread event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 99e6521eb7..1aa0162354 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -46,10 +46,12 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -343,6 +345,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return roomEntity } + val customList = arrayListOf() private fun handleTimelineEvents(realm: Realm, roomId: String, roomEntity: RoomEntity, @@ -406,11 +409,18 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle rootStateEvent?.asDomain()?.getFixedRoomMemberContent() } - chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + val timelineEventAdded = chunkEntity.addTimelineEvent( + roomId = roomId, + eventEntity = eventEntity, + direction = PaginationDirection.FORWARDS, + roomMemberContentsByUser = roomMemberContentsByUser) if (lightweightSettingsStorage.areThreadMessagesEnabled()) { eventEntity.rootThreadEventId?.let { // This is a thread event optimizedThreadSummaryMap[it] = eventEntity + // Add the same thread timeline event to Thread Chunk + addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity) + } ?: run { // This is a normal event or a root thread one optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity @@ -455,6 +465,29 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return chunkEntity } + /** + * Adds new event to the appropriate thread chunk. If the event is already in + * the thread timeline and /relations api, we should not added it + */ + private fun addToThreadChunkIfNeeded(realm: Realm, + roomId: String, + threadId: String, + timelineEventEntity: TimelineEventEntity?, + roomEntity: RoomEntity) { + + val eventId = timelineEventEntity?.eventId ?: return + + ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk -> + val existingEvent = threadChunk.timelineEvents.find(eventId) + if (existingEvent?.ownedByThreadChunk == true) { + Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add") + return@addToThreadChunkIfNeeded + } + threadChunk.timelineEvents.add(0, timelineEventEntity) + roomEntity.addIfNecessary(threadChunk) + } + } + private fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 8bc6bd73e9..32cb006810 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -65,7 +65,7 @@ class ThreadListController @Inject constructor( id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) matrixItem(timelineEvent.senderInfo.toMatrixItem()) - title(timelineEvent.senderInfo.displayName) + title(timelineEvent.senderInfo.displayName.orEmpty()) date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) From 83d937b842b28acd5f34a1c9120de812dbfaa12b Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 15:10:30 +0200 Subject: [PATCH 002/405] format ktlint --- .../org/matrix/android/sdk/api/session/events/model/Event.kt | 2 +- .../sdk/internal/database/RealmSessionStoreMigration.kt | 3 +-- .../matrix/android/sdk/internal/database/model/ChunkEntity.kt | 4 ---- .../session/room/relation/threads/FetchThreadTimelineTask.kt | 1 - .../internal/session/room/timeline/LoadTimelineStrategy.kt | 3 +-- .../sdk/internal/session/room/timeline/TimelineChunk.kt | 1 - .../sdk/internal/session/sync/handler/room/RoomSyncHandler.kt | 2 -- 7 files changed, 3 insertions(+), 13 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index ed9a057375..97eee9188c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -202,7 +202,7 @@ data class Event( fun getDecryptedTextSummary(): String? { if (isRedacted()) return "Message Deleted" val text = getDecryptedValue() ?: run { - if (isPoll()) {return getPollQuestion() ?: "created a poll."} + if (isPoll()) { return getPollQuestion() ?: "created a poll." } return null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 056c4b0ceb..5706a4ca06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -491,7 +491,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true) } - private fun migrateTo25(realm: DynamicRealm){ + private fun migrateTo25(realm: DynamicRealm) { Timber.d("Step 24 -> 25") realm.schema.get("ChunkEntity") ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) @@ -499,6 +499,5 @@ internal class RealmSessionStoreMigration @Inject constructor( realm.schema.get("TimelineEventEntity") ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 8e4d6fc916..ca8049fd96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -23,7 +23,6 @@ import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.extensions.clearWith -import timber.log.Timber internal open class ChunkEntity(@Index var prevToken: String? = null, // Because of gaps we can have several chunks with nextToken == null @@ -75,6 +74,3 @@ internal fun ChunkEntity.deleteAndClearThreadEvents() { } deleteFromRealm() } - - - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 6b071fbd6e..e7b91ebab7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -118,7 +118,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( private suspend fun handleRelationsResponse(response: RelationsResponse, params: FetchThreadTimelineTask.Params): Result { - val threadList = response.chunks val threadRootEvent = response.originalEvent val hasReachEnd = response.nextBatch == null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 867589ccc0..a26008369a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -179,7 +179,7 @@ internal class LoadTimelineStrategy( getContextLatch?.cancel() chunkEntity = null timelineChunk = null - if(mode is Mode.Thread) { + if (mode is Mode.Thread) { clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId) } } @@ -272,7 +272,6 @@ internal class LoadTimelineStrategy( ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let { Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..") } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 7f6e5b6c7c..864b3f0dd9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -171,7 +171,6 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, * always fetch results, while we want our data to be up to dated. */ suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult { - return if (direction == Timeline.Direction.BACKWARDS) { try { fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 1aa0162354..573af7c696 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -420,7 +420,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle optimizedThreadSummaryMap[it] = eventEntity // Add the same thread timeline event to Thread Chunk addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity) - } ?: run { // This is a normal event or a root thread one optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity @@ -474,7 +473,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle threadId: String, timelineEventEntity: TimelineEventEntity?, roomEntity: RoomEntity) { - val eventId = timelineEventEntity?.eventId ?: return ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk -> From 27bc43c24c5d3ba56c09e7a05f1e3c2e3e5d9288 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 15:33:51 +0200 Subject: [PATCH 003/405] Fix realm migration --- .../database/RealmSessionStoreMigration.kt | 4 ++- .../database/migration/MigrateSessionTo025.kt | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index f4a8ae2c67..12e60da114 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo021 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -56,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 24L + val schemaVersion = 25L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -85,5 +86,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 22) MigrateSessionTo022(realm).perform() if (oldVersion < 23) MigrateSessionTo023(realm).perform() if (oldVersion < 24) MigrateSessionTo024(realm).perform() + if (oldVersion < 25) MigrateSessionTo025(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt new file mode 100644 index 0000000000..2a859d8b5a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import io.realm.FieldAttribute +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +class MigrateSessionTo025(realm: DynamicRealm) : RealmMigrator(realm, 24) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("ChunkEntity") + ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED) + + realm.schema.get("TimelineEventEntity") + ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java) + } +} From e9e5d680a1a247d552c2a2869f7caa1ca69df3b4 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 16:51:56 +0200 Subject: [PATCH 004/405] Fix realm migration from 25 to 26 --- .../database/RealmSessionStoreMigration.kt | 2 ++ .../database/migration/MigrateSessionTo026.kt | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 12e60da114..e84bdc2d30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 23) MigrateSessionTo023(realm).perform() if (oldVersion < 24) MigrateSessionTo024(realm).perform() if (oldVersion < 25) MigrateSessionTo025(realm).perform() + if (oldVersion < 26) MigrateSessionTo026(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt new file mode 100644 index 0000000000..d499365bb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import io.realm.FieldAttribute +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { + + override fun doMigrate(realm: DynamicRealm) { + + realm.schema.get("ChunkEntity") + ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED) + + realm.schema.get("TimelineEventEntity") + ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java) + } +} From 830c38f50b38a47fc8bae0229b9dd15a164d76e3 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 16:53:29 +0200 Subject: [PATCH 005/405] format ktlint --- .../sdk/internal/database/migration/MigrateSessionTo026.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt index d499365bb3..597d6d1cbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { override fun doMigrate(realm: DynamicRealm) { - realm.schema.get("ChunkEntity") ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED) From 83088bbe5ac8d6630d2b7657f6d486c432e46b9f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 18 Feb 2022 17:21:10 +0200 Subject: [PATCH 006/405] Introduce live thread summaries using the enhanced /messages API from MSC 3440 Add capabilities to support local thread list to not supported servers --- .../org/matrix/android/sdk/flow/FlowRoom.kt | 8 +- .../events/model/AggregatedRelations.kt | 3 +- .../model/LatestThreadUnsignedRelation.kt | 30 ++ .../homeserver/HomeServerCapabilities.kt | 6 +- .../android/sdk/api/session/room/Room.kt | 2 + .../room/model/relation/RelationService.kt | 9 - .../session/room/threads/ThreadsService.kt | 50 ++- .../room/threads/local/ThreadsLocalService.kt | 68 ++++ .../room/threads/model/ThreadEditions.kt | 20 ++ .../room/threads/model/ThreadSummary.kt | 33 ++ .../threads/model/ThreadSummaryUpdateType.kt | 22 ++ .../matrix/android/sdk/api/util/MatrixItem.kt | 3 + .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ChunkEntityHelper.kt | 2 +- .../database/helper/RoomEntityHelper.kt | 7 + .../database/helper/ThreadEventsHelper.kt | 6 +- .../database/helper/ThreadSummaryHelper.kt | 332 ++++++++++++++++++ .../mapper/HomeServerCapabilitiesMapper.kt | 3 +- .../database/mapper/ThreadSummaryMapper.kt | 48 +++ .../database/migration/MigrateSessionTo027.kt | 49 +++ .../internal/database/model/ChunkEntity.kt | 7 +- .../internal/database/model/EventEntity.kt | 4 +- .../model/HomeServerCapabilitiesEntity.kt | 3 +- .../sdk/internal/database/model/RoomEntity.kt | 11 + .../database/model/SessionRealmModule.kt | 4 +- .../model/threads/ThreadSummaryEntity.kt | 43 +++ .../query/ThreadSummaryEntityQueries.kt | 59 ++++ .../internal/session/filter/FilterFactory.kt | 16 +- .../session/filter/RoomEventFilter.kt | 7 +- .../homeserver/GetCapabilitiesResult.kt | 8 +- .../GetHomeServerCapabilitiesTask.kt | 1 + .../sdk/internal/session/room/DefaultRoom.kt | 3 + .../EventRelationsAggregationProcessor.kt | 14 +- .../sdk/internal/session/room/RoomAPI.kt | 2 +- .../sdk/internal/session/room/RoomFactory.kt | 3 + .../sdk/internal/session/room/RoomModule.kt | 5 + .../room/relation/DefaultRelationService.kt | 12 - .../threads/FetchThreadSummariesTask.kt | 108 ++++++ .../threads/FetchThreadTimelineTask.kt | 1 - .../room/threads/DefaultThreadsService.kt | 75 ++-- .../local/DefaultThreadsLocalService.kt | 103 ++++++ .../sync/handler/room/RoomSyncHandler.kt | 18 +- .../home/room/threads/ThreadsActivity.kt | 14 +- .../list/viewmodel/ThreadListController.kt | 62 +++- .../list/viewmodel/ThreadListViewModel.kt | 41 ++- .../list/viewmodel/ThreadListViewState.kt | 3 +- .../threads/list/views/ThreadListFragment.kt | 28 +- 47 files changed, 1221 insertions(+), 139 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 826f584f6a..fb8bf2df27 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) { return room.getLiveRoomNotificationState().asFlow() } + fun liveThreadSummaries(): Flow> { + return room.getAllThreadSummariesLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getAllThreadSummaries() + } + } fun liveThreadList(): Flow> { return room.getAllThreadsLive().asFlow() .startWith(room.coroutineDispatchers.io) { room.getAllThreads() } } - fun liveLocalUnreadThreadList(): Flow> { return room.getMarkedThreadNotificationsLive().asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index 34096d603f..7547d1cfe9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class AggregatedRelations( @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, - @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null + @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null, + @Json(name = RelationType.IO_THREAD) val latestThread: LatestThreadUnsignedRelation? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt new file mode 100644 index 0000000000..cc52dfc02c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LatestThreadUnsignedRelation( + override val limited: Boolean? = false, + override val count: Int? = 0, + @Json(name = "latest_event") + val event: Event? = null, + @Json(name = "current_user_participated") + val isUserParticipating: Boolean? = false + +) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 2256dfb8f0..9db3876b74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -50,7 +50,11 @@ data class HomeServerCapabilities( * This capability describes the default and available room versions a server supports, and at what level of stability. * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. */ - val roomVersions: RoomVersionCapabilities? = null + val roomVersions: RoomVersionCapabilities? = null, + /** + * True if the home server support threading + */ + var canUseThreading: Boolean = false ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index d930a5d0fd..be65b883b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.threads.ThreadsService +import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService @@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional interface Room : TimelineService, ThreadsService, + ThreadsLocalService, SendService, DraftService, ReadService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 09114436f0..4409898908 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -163,13 +163,4 @@ interface RelationService { autoMarkdown: Boolean = false, formattedText: String? = null, eventReplied: TimelineEvent? = null): Cancelable? - - /** - * Get all the thread replies for the specified rootThreadEventId - * The return list will contain the original root thread event and all the thread replies to that event - * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready - * from the backend - * @param rootThreadEventId the root thread eventId - */ - suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index e4d1d979e1..99c0dc7d0f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -17,51 +17,43 @@ package org.matrix.android.sdk.api.session.room.threads import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary /** - * This interface defines methods to interact with threads related features. - * It's implemented at the room level within the main timeline. + * This interface defines methods to interact with thread related features. + * It's the dynamic threads implementation and the homeserver must return + * a capability entry for threads. If the server do not support m.thread + * then [ThreadsLocalService] should be used instead */ interface ThreadsService { /** - * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level + * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level */ - fun getAllThreadsLive(): LiveData> + fun getAllThreadSummariesLive(): LiveData> /** - * Returns a list of all the thread root TimelineEvents that exists at the room level + * Returns a list of all the [ThreadSummary] that exists at the room level */ - fun getAllThreads(): List + fun getAllThreadSummaries(): List /** - * Returns a [LiveData] list of all the marked unread threads that exists at the room level - */ - fun getMarkedThreadNotificationsLive(): LiveData> - - /** - * Returns a list of all the marked unread threads that exists at the room level - */ - fun getMarkedThreadNotifications(): List - - /** - * Returns whether or not the current user is participating in the thread - * @param rootThreadEventId the eventId of the current thread - */ - fun isUserParticipatingInThread(rootThreadEventId: String): Boolean - - /** - * Enhance the provided root thread TimelineEvent [List] by adding the latest + * Enhance the provided ThreadSummary[List] by adding the latest * message edition for that thread * @return the enhanced [List] with edited updates */ - fun mapEventsWithEdition(threads: List): List + fun enhanceWithEditions(threads: List): List /** - * Marks the current thread as read in local DB. - * note: read receipts within threads are not yet supported with the API - * @param rootThreadEventId the root eventId of the current thread + * Fetch all thread replies for the specified thread using the /relations api + * @param rootThreadEventId the root thread eventId + * @param from defines the token that will fetch from that position + * @param limit defines the number of max results the api will respond with */ - suspend fun markThreadAsRead(rootThreadEventId: String) + suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) + + /** + * Fetch all thread summaries for the current room using the enhanced /messages api + */ + suspend fun fetchThreadSummaries() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt new file mode 100644 index 0000000000..f7b379e382 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads.local + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This interface defines methods to interact with thread related features. + * It's the local threads implementation and assumes that the homeserver + * do not support threads + */ +interface ThreadsLocalService { + + /** + * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level + */ + fun getAllThreadsLive(): LiveData> + + /** + * Returns a list of all the thread root TimelineEvents that exists at the room level + */ + fun getAllThreads(): List + + /** + * Returns a [LiveData] list of all the marked unread threads that exists at the room level + */ + fun getMarkedThreadNotificationsLive(): LiveData> + + /** + * Returns a list of all the marked unread threads that exists at the room level + */ + fun getMarkedThreadNotifications(): List + + /** + * Returns whether or not the current user is participating in the thread + * @param rootThreadEventId the eventId of the current thread + */ + fun isUserParticipatingInThread(rootThreadEventId: String): Boolean + + /** + * Enhance the provided root thread TimelineEvent [List] by adding the latest + * message edition for that thread + * @return the enhanced [List] with edited updates + */ + fun mapEventsWithEdition(threads: List): List + + /** + * Marks the current thread as read in local DB. + * note: read receipts within threads are not yet supported with the API + * @param rootThreadEventId the root eventId of the current thread + */ + suspend fun markThreadAsRead(rootThreadEventId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt new file mode 100644 index 0000000000..db92e800e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads.model + +data class ThreadEditions(var rootThreadEdition: String? = null, + var latestThreadEdition: String? = null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt new file mode 100644 index 0000000000..f26be85e85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads.model + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +/** + * The main thread Summary model, mainly used to display the thread list + */ +data class ThreadSummary(val roomId: String, + val rootEvent: Event?, + val latestEvent: Event?, + val rootEventId: String, + val rootThreadSenderInfo: SenderInfo, + val latestThreadSenderInfo: SenderInfo, + val isUserParticipating: Boolean, + val numberOfThreads: Int, + val threadEditions: ThreadEditions = ThreadEditions()) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt new file mode 100644 index 0000000000..744265cb94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads.model + +enum class ThreadSummaryUpdateType { + REPLACE, + ADD +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index 3396c4a6c9..17d7d96a38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.util import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -178,6 +179,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) +fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) } + fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) { MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl) } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index e84bdc2d30..24ac310653 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -58,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 25L + val schemaVersion = 27L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -89,5 +90,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 24) MigrateSessionTo024(realm).perform() if (oldVersion < 25) MigrateSessionTo025(realm).perform() if (oldVersion < 26) MigrateSessionTo026(realm).perform() + if (oldVersion < 27) MigrateSessionTo027(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 007017510c..d2e3e99b75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -118,7 +118,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, return timelineEventEntity } -private fun computeIsUnique( +fun computeIsUnique( realm: Realm, roomId: String, isLastForward: Boolean, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt index 724f307e3b..9ad2708b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt @@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) { if (!chunks.contains(chunkEntity)) { chunks.add(chunkEntity) } } + +internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) { + if (!threadSummaries.contains(threadSummary)) { + threadSummaries.add(threadSummary) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 7f6b64da75..ee3008d40b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId -private typealias ThreadSummary = Pair? +private typealias Summary = Pair? /** * Finds the root thread event and update it with the latest message summary along with the number @@ -93,7 +93,7 @@ internal fun EventEntity.markEventAsRoot( * @param rootThreadEventId The root eventId that will find the number of threads * @return A ThreadSummary containing the counted threads and the latest event message */ -internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary { +internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { // Number of messages val messages = TimelineEventEntity .whereRoomId(realm, roomId = roomId) @@ -124,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: result ?: return null - return ThreadSummary(messages, result) + return Summary(messages, result) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt new file mode 100644 index 0000000000..d19056adfa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.helper + +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.Sort +import io.realm.kotlin.createObject +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.EventType +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import timber.log.Timber +import java.util.UUID + +internal fun ThreadSummaryEntity.updateThreadSummary( + rootThreadEventEntity: EventEntity, + numberOfThreads: Int?, + latestThreadEventEntity: EventEntity?, + isUserParticipating: Boolean, + roomMemberContentsByUser: HashMap) { + updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser) + updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser) + + // Update latest event +// latestThreadEventEntity?.toTimelineEventEntity(roomMemberContentsByUser)?.let { +// Timber.i("###THREADS FetchThreadSummariesTask ThreadSummaryEntity updated latest event:${it.eventId} !") +// this.eventEntity?.threadSummaryLatestMessage = it +// } + + // Update number of threads + this.isUserParticipating = isUserParticipating + numberOfThreads?.let { + // Update only when there is an actual value + this.numberOfThreads = it + } +} + +/** + * Updates the root thread event properties + */ +internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent( + rootThreadEventEntity: EventEntity, + roomMemberContentsByUser: HashMap +) { + val roomId = rootThreadEventEntity.roomId + val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""] + this.rootThreadEventEntity = rootThreadEventEntity + this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl + this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName + this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser) + } else { + true + } +} + +/** + * Updates the latest thread event properties + */ +internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent( + latestThreadEventEntity: EventEntity?, + roomMemberContentsByUser: HashMap +) { + val roomId = latestThreadEventEntity?.roomId ?: return + val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""] + this.latestThreadEventEntity = latestThreadEventEntity + this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl + this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName + this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser) + } else { + true + } +} + +private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap): TimelineEventEntity { + val roomId = roomId + val eventId = eventId + val localId = TimelineEventEntity.nextId(realm) + val senderId = sender ?: "" + + val timelineEventEntity = realm.createObject().apply { + this.localId = localId + this.root = this@toTimelineEventEntity + this.eventId = eventId + this.roomId = roomId + this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() + ?.also { it.cleanUp(sender) } + this.ownedByThreadChunk = true // To skip it from the original event flow + val roomMemberContent = roomMemberContentsByUser[senderId] + this.senderAvatar = roomMemberContent?.avatarUrl + this.senderName = roomMemberContent?.displayName + isUniqueDisplayName = if (roomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser) + } else { + true + } + } + return timelineEventEntity +} + +internal fun ThreadSummaryEntity.Companion.createOrUpdate( + threadSummaryType: ThreadSummaryUpdateType, + realm: Realm, + roomId: String, + threadEventEntity: EventEntity? = null, + rootThreadEvent: Event? = null, + roomMemberContentsByUser: HashMap, + roomEntity: RoomEntity, + userId: String, + cryptoService: CryptoService? = null +) { + when (threadSummaryType) { + ThreadSummaryUpdateType.REPLACE -> { + rootThreadEvent?.eventId ?: return + rootThreadEvent.senderId ?: return + + val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return + + // Something is wrong with the server return + if (numberOfThreads <= 0) return + + val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also { + Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") + } + + val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also { + decryptIfNeeded(cryptoService, it, roomId) + } + val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also { + decryptIfNeeded(cryptoService, it, roomId) + } + val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId + roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId) + threadSummary.updateThreadSummary( + rootThreadEventEntity = rootThreadEventEntity, + numberOfThreads = numberOfThreads, + latestThreadEventEntity = latestThreadEventEntity, + isUserParticipating = isUserParticipating, + roomMemberContentsByUser = roomMemberContentsByUser + ) + + roomEntity.addIfNecessary(threadSummary) + } + ThreadSummaryUpdateType.ADD -> { + val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return + Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") + + val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) + if (threadSummary != null) { + // ThreadSummary exists so lets add the latest event + Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.") + threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser) + threadSummary.numberOfThreads++ + if (threadEventEntity.sender == userId) { + threadSummary.isUserParticipating = true + } + } else { + // ThreadSummary do not exists lets try to create one + Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one") + threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> + // Root thread event entity exists so lets create a new record + ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let { + it.updateThreadSummary( + rootThreadEventEntity = rootThreadEventEntity, + numberOfThreads = 1, + latestThreadEventEntity = threadEventEntity, + isUserParticipating = threadEventEntity.sender == userId, + roomMemberContentsByUser = roomMemberContentsByUser + ) + roomEntity.addIfNecessary(it) + } + } + } + } + } +} + +private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) { + cryptoService ?: return + val event = eventEntity.asDomain() + if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) { + try { + Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}") + // Event from sync does not have roomId, so add it to the event first + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + // Save decryption result, to not decrypt every time we enter the thread list + eventEntity.setDecryptionResult(result) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } +} + +/** + * Request decryption + */ +private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) { + eventDecryptor ?: return + event ?: return + if (event.isEncrypted() && + event.mxDecryptionResult == null && event.eventId != null) { + Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}") + + eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString())) + } +} + +/** + * If we don't have any new state on this user, get it from db + */ +private fun HashMap.addSenderState(realm: Realm, roomId: String, senderId: String) { + getOrPut(senderId) { + CurrentStateEventEntity + .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER) + ?.root?.asDomain() + ?.getFixedRoomMemberContent() + } +} + +/** + * Create an EventEntity for the root thread event or get an existing one + */ +private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) +} + +/** + * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member + * state + */ +private fun createLatestEventEntity(roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap, realm: Realm): EventEntity? { + return getLatestEvent(rootThreadEvent)?.let { + it.senderId?.let { senderId -> + roomMemberContentsByUser.addSenderState(realm, roomId, senderId) + } + createEventEntity(roomId, it, realm) + } +} + +/** + * Returned the latest event message, if any + */ +private fun getLatestEvent(rootThreadEvent: Event): Event? { + return rootThreadEvent.unsignedData?.relations?.latestThread?.event +} + +/** + * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server + * note: Sorting cannot be provided by server, so we have to use that unstable property + * @param roomId The id of the room + */ +internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery = + ThreadSummaryEntity + .where(realm, roomId = roomId) + .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING) + +/** + * Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement + */ +internal fun List.enhanceWithEditions(realm: Realm, roomId: String): List = + this.map { + it.addEditionIfNeeded(realm, roomId, true) + it.addEditionIfNeeded(realm, roomId, false) + it + } + +private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) { + val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return + EventAnnotationsSummaryEntity + .where(realm, roomId, eventId) + .findFirst() + ?.editSummary + ?.editions + ?.lastOrNull() + ?.eventId + ?.let { editedEventId -> + TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent -> + if (enhanceRoot) { + threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)" + } else { + threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)" + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 7869506015..8be3455c07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper { maxUploadFileSize = entity.maxUploadFileSize, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, defaultIdentityServerUrl = entity.defaultIdentityServerUrl, - roomVersions = mapRoomVersion(entity.roomVersionsJson) + roomVersions = mapRoomVersion(entity.roomVersionsJson), + canUseThreading = false // entity.canUseThreading ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt new file mode 100644 index 0000000000..54386c5ea8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import javax.inject.Inject + +internal class ThreadSummaryMapper @Inject constructor() { + + fun map(threadSummary: ThreadSummaryEntity): ThreadSummary { + return ThreadSummary( + roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(), + rootEvent = threadSummary.rootThreadEventEntity?.asDomain(), + latestEvent = threadSummary.latestThreadEventEntity?.asDomain(), + rootEventId = threadSummary.rootThreadEventId, + rootThreadSenderInfo = SenderInfo( + userId = threadSummary.rootThreadEventEntity?.sender ?: "", + displayName = threadSummary.rootThreadSenderName, + isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName, + avatarUrl = threadSummary.rootThreadSenderAvatar + ), + latestThreadSenderInfo = SenderInfo( + userId = threadSummary.latestThreadEventEntity?.sender ?: "", + displayName = threadSummary.latestThreadSenderName, + isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName, + avatarUrl = threadSummary.latestThreadSenderAvatar + ), + isUserParticipating = threadSummary.isUserParticipating, + numberOfThreads = threadSummary.numberOfThreads + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt new file mode 100644 index 0000000000..b56b7d325b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import io.realm.FieldAttribute +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +class MigrateSessionTo027(realm: DynamicRealm) : RealmMigrator(realm, 27) { + + override fun doMigrate(realm: DynamicRealm) { + val eventEntity = realm.schema.get("EventEntity") ?: return + val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity") + .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java) + .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java) + .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity) + .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity) + + realm.schema.get("RoomEntity") + ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity) + + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index ca8049fd96..88eb821aa9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -50,13 +50,18 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, companion object } -internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) { +internal fun ChunkEntity.deleteOnCascade( + deleteStateEvents: Boolean, + canDeleteRoot: Boolean) { assertIsManaged() if (deleteStateEvents) { stateEvents.deleteAllFromRealm() } timelineEvents.clearWith { val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents) + if (deleteRoot) { + room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId) + } it.deleteOnCascade(deleteRoot) } deleteFromRealm() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 445181e576..b7158ba9cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var stateKey: String? = null, var originServerTs: Long? = null, @Index var sender: String? = null, - // Can contain a serialized MatrixError + // Can contain a serialized MatrixError var sendStateDetails: String? = null, var age: Long? = 0, var unsignedData: String? = null, var redacts: String? = null, var decryptionResultJson: String? = null, var ageLocalTs: Long? = null, - // Thread related, no need to create a new Entity for performance + // Thread related, no need to create a new Entity for performance @Index var isRootThread: Boolean = false, @Index var rootThreadEventId: String? = null, var numberOfThreads: Int = 0, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 08ecd5995e..47a83f0ed9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity( var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var lastVersionIdentityServerSupported: Boolean = false, var defaultIdentityServerUrl: String? = null, - var lastUpdatedTimestamp: Long = 0L + var lastUpdatedTimestamp: Long = 0L, + var canUseThreading: Boolean = false ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 2997d5d7d8..4a6f6a7bf8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -20,10 +20,14 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.query.findRootOrLatest +import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), var sendingTimelineEvents: RealmList = RealmList(), + var threadSummaries: RealmList = RealmList(), var accountData: RealmList = RealmList() ) : RealmObject() { @@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", } companion object } +internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) { + assertIsManaged() + threadSummaries.findRootOrLatest(eventId)?.let { + threadSummaries.remove(it) + it.deleteFromRealm() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index c090777972..d0d23dd491 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity /** * Realm module for Session @@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit RoomAccountDataEntity::class, SpaceChildSummaryEntity::class, SpaceParentSummaryEntity::class, - UserPresenceEntity::class + UserPresenceEntity::class, + ThreadSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt new file mode 100644 index 0000000000..21a80502e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model.threads + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Index +import io.realm.annotations.LinkingObjects +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity + +internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String = "", + var rootThreadEventEntity: EventEntity? = null, + var latestThreadEventEntity: EventEntity? = null, + var rootThreadSenderName: String? = null, + var latestThreadSenderName: String? = null, + var rootThreadSenderAvatar: String? = null, + var latestThreadSenderAvatar: String? = null, + var rootThreadIsUniqueDisplayName: Boolean = false, + var isUserParticipating: Boolean = false, + var latestThreadIsUniqueDisplayName: Boolean = false, + var numberOfThreads: Int = 0 +) : RealmObject() { + + @LinkingObjects("threadSummaries") + val room: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt new file mode 100644 index 0000000000..517d43d7cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields + +internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId) +} + +internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery { + return where(realm, roomId) + .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) +} + +internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity { + return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject().apply { + this.rootThreadEventId = rootThreadEventId + } +} +internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? { + return where(realm, roomId, rootThreadEventId).findFirst() +} +internal fun RealmList.find(rootThreadEventId: String): ThreadSummaryEntity? { + return this.where() + .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .findFirst() +} + +internal fun RealmList.findRootOrLatest(eventId: String): ThreadSummaryEntity? { + return this.where() + .beginGroup() + .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId) + .or() + .equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId) + .endGroup() + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt index 7415b988a4..2e52354037 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -17,9 +17,21 @@ package org.matrix.android.sdk.internal.session.filter import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import timber.log.Timber internal object FilterFactory { + fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter { + Timber.i("$userId") + return RoomEventFilter( + limit = numberOfEvents, +// senders = listOf(userId), +// relationSenders = userId?.let { listOf(it) }, + relationTypes = listOf(RelationType.IO_THREAD) + ) + } + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { return RoomEventFilter( limit = numberOfEvents, @@ -58,8 +70,8 @@ internal object FilterFactory { private fun createElementTimelineFilter(): RoomEventFilter? { return null // RoomEventFilter().apply { - // TODO Enable this for optimization - // types = listOfSupportedEventTypes.toMutableList() + // TODO Enable this for optimization + // types = listOfSupportedEventTypes.toMutableList() // } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt index f498322967..c93f6a10db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt @@ -52,12 +52,15 @@ data class RoomEventFilter( * A list of relation types which must be exist pointing to the event being filtered. * If this list is absent then no filtering is done on relation types. */ - @Json(name = "relation_types") val relationTypes: List? = null, +// @Json(name = "relation_types") val relationTypes: List? = null, + @Json(name = "io.element.relation_types") val relationTypes: List? = null, // To be replaced with the above line after the release /** * A list of senders of relations which must exist pointing to the event being filtered. * If this list is absent then no filtering is done on relation types. */ - @Json(name = "relation_senders") val relationSenders: List? = null, +// @Json(name = "relation_senders") val relationSenders: List? = null, + @Json(name = "io.element.relation_senders") val relationSenders: List? = null, // To be replaced with the above line after the release + /** * A list of room IDs to include. If this list is absent then all rooms are included. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index 830a58cd12..55526b41db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -65,7 +65,13 @@ internal data class Capabilities( * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. */ @Json(name = "m.room_versions") - val roomVersions: RoomVersions? = null + val roomVersions: RoomVersions? = null, + /** + * Capability to indicate if the server supports MSC3440 Threading + * True if the user can use m.thread relation, false otherwise + */ + @Json(name = "m.thread") + val threads: BooleanCapability? = null ) @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index e822cbdcdb..8c6bb626d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -121,6 +121,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) } + homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue() } if (getMediaConfigResult != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 2d8c3e9c78..34e859e509 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.threads.ThreadsService +import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService @@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String, private val roomSummaryDataSource: RoomSummaryDataSource, private val timelineService: TimelineService, private val threadsService: ThreadsService, + private val threadsLocalService: ThreadsLocalService, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, @@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String, Room, TimelineService by timelineService, ThreadsService by threadsService, + ThreadsLocalService by threadsLocalService, SendService by sendService, DraftService by draftService, StateService by stateService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 2eebb70bdc..d17d16a82d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -197,6 +197,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleReaction(realm, event, roomId, isLocalEcho) } } + // TODO is that ok?? +// else if (event.unsignedData?.relations?.annotations != null) { +// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}") +// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) +// // EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() +// // ?.let { +// // TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() +// // ?.forEach { tet -> tet.annotations = it } +// // } +// } } EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } @@ -244,7 +254,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } // OPT OUT serer aggregation until API mature enough - private val SHOULD_HANDLE_SERVER_AGREGGATION = false + private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e private fun handleReplace(realm: Realm, event: Event, @@ -346,6 +356,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( /** * Check if the edition is on the latest thread event, and update it accordingly + * @param editedEvent The event that will be changed + * @param replaceEvent The new event */ private fun handleThreadSummaryEdition(editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 86929e013f..71838ab5a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -86,7 +86,7 @@ internal interface RoomAPI { suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String, @Query("from") from: String, @Query("dir") dir: String, - @Query("limit") limit: Int, + @Query("limit") limit: Int?, @Query("filter") filter: String? ): PaginationResponse diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 70c1ab4f42..72a3f9ab22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService +import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService @@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val roomSummaryDataSource: RoomSummaryDataSource, private val timelineServiceFactory: DefaultTimelineService.Factory, private val threadsServiceFactory: DefaultThreadsService.Factory, + private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory, private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, @@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomSummaryDataSource = roomSummaryDataSource, timelineService = timelineServiceFactory.create(roomId), threadsService = threadsServiceFactory.create(roomId), + threadsLocalService = threadsLocalServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index f831a77a5d..5e90076b8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask @@ -294,4 +296,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask + + @Binds + abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index d22583e8b7..f21ee4346c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt 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.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.util.fetchCopyMap @@ -51,7 +50,6 @@ internal class DefaultRelationService @AssistedInject constructor( private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, - private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val timelineEventMapper: TimelineEventMapper, @SessionDatabase private val monarchy: Monarchy ) : RelationService { @@ -205,16 +203,6 @@ internal class DefaultRelationService @AssistedInject constructor( return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } - override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean { - fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( - roomId, - rootThreadEventId, - null, - 10 - )) - return true - } - /** * Saves the event in database as a local echo. * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt new file mode 100644 index 0000000000..d316eed691 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation.threads + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.database.helper.createOrUpdate +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterFactory +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import timber.log.Timber +import javax.inject.Inject + +/*** + * This class is responsible to Fetch all the thread in the current room, + * To fetch all threads in a room, the /messages API is used with newly added filtering options. + */ +internal interface FetchThreadSummariesTask : Task { + data class Params( + val roomId: String, + val from: String = "", + val limit: Int = 100, + val isUserParticipating: Boolean = true + ) +} + +internal class DefaultFetchThreadSummariesTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase private val monarchy: Monarchy, + private val cryptoService: DefaultCryptoService, + @UserId private val userId: String, +) : FetchThreadSummariesTask { + + override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { + val filter = FilterFactory.createThreadsFilter( + numberOfEvents = params.limit, + userId = if (params.isUserParticipating) userId else null).toJSONString() + + val response = executeRequest( + globalErrorReceiver, + canRetry = true + ) { + roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter) + } + + Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ") + + return handleResponse(response, params) + } + + private suspend fun handleResponse(response: PaginationResponse, + params: FetchThreadSummariesTask.Params): Result { + val rootThreadList = response.events + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction + + val roomMemberContentsByUser = HashMap() + for (rootThreadEvent in rootThreadList) { + if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { + continue + } + + ThreadSummaryEntity.createOrUpdate( + threadSummaryType = ThreadSummaryUpdateType.REPLACE, + realm = realm, + roomId = params.roomId, + rootThreadEvent = rootThreadEvent, + roomMemberContentsByUser = roomMemberContentsByUser, + roomEntity = roomEntity, + userId = userId, + cryptoService = cryptoService) + } + } + return Result.SUCCESS + } + + enum class Result { + SHOULD_FETCH_MORE, + REACHED_END, + SUCCESS + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index e7b91ebab7..54ebc620c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -58,7 +58,6 @@ import javax.inject.Inject /*** * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API * - * * How it works * * The problem? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 5967ae8d2e..033c1c0ff9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm import org.matrix.android.sdk.api.session.room.threads.ThreadsService -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.threads.ThreadNotificationState -import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId -import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread -import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition +import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask +import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask internal class DefaultThreadsService @AssistedInject constructor( @Assisted private val roomId: String, @UserId private val userId: String, + private val fetchThreadTimelineTask: FetchThreadTimelineTask, + private val fetchThreadSummariesTask: FetchThreadSummariesTask, @SessionDatabase private val monarchy: Monarchy, private val timelineEventMapper: TimelineEventMapper, + private val threadSummaryMapper: ThreadSummaryMapper ) : ThreadsService { @AssistedFactory @@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getMarkedThreadNotificationsLive(): LiveData> { + override fun getAllThreadSummariesLive(): LiveData> { return monarchy.findAllMappedWithChanges( - { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } + { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { + threadSummaryMapper.map(it) + } ) } - override fun getMarkedThreadNotifications(): List { + override fun getAllThreadSummaries(): List { return monarchy.fetchAllMappedSync( - { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } + { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { threadSummaryMapper.map(it) } ) } - override fun getAllThreadsLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun getAllThreads(): List { - return monarchy.fetchAllMappedSync( - { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean { + override fun enhanceWithEditions(threads: List): List { return Realm.getInstance(monarchy.realmConfiguration).use { - TimelineEventEntity.isUserParticipatingInThread( - realm = it, - roomId = roomId, - rootThreadEventId = rootThreadEventId, - senderId = userId) + threads.enhanceWithEditions(it, roomId) } } - override fun mapEventsWithEdition(threads: List): List { - return Realm.getInstance(monarchy.realmConfiguration).use { - threads.mapEventsWithEdition(it, roomId) - } + override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) { + fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( + roomId = roomId, + rootThreadEventId = rootThreadEventId, + from = from, + limit = limit + )) } - override suspend fun markThreadAsRead(rootThreadEventId: String) { - monarchy.awaitTransaction { - EventEntity.where( - realm = it, - eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE - } + override suspend fun fetchThreadSummaries() { + fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params( + roomId = roomId + )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt new file mode 100644 index 0000000000..3bc36fb2a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.threads.local + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.Realm +import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId +import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId +import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread +import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.EventEntity +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.UserId +import org.matrix.android.sdk.internal.util.awaitTransaction + +internal class DefaultThreadsLocalService @AssistedInject constructor( + @Assisted private val roomId: String, + @UserId private val userId: String, + @SessionDatabase private val monarchy: Monarchy, + private val timelineEventMapper: TimelineEventMapper, +) : ThreadsLocalService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultThreadsLocalService + } + + override fun getMarkedThreadNotificationsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getMarkedThreadNotifications(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getAllThreadsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getAllThreads(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { + TimelineEventEntity.isUserParticipatingInThread( + realm = it, + roomId = roomId, + rootThreadEventId = rootThreadEventId, + senderId = userId) + } + } + + override fun mapEventsWithEdition(threads: List): List { + return Realm.getInstance(monarchy.realmConfiguration).use { + threads.mapEventsWithEdition(it, roomId) + } + } + + override suspend fun markThreadAsRead(rootThreadEventId: String) { + monarchy.awaitTransaction { + EventEntity.where( + realm = it, + eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 573af7c696..63857d611b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -23,10 +23,12 @@ 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.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.RoomSync @@ -36,6 +38,7 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -48,6 +51,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom @@ -60,6 +64,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent +import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.reportSubtask @@ -86,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, private val lightweightSettingsStorage: LightweightSettingsStorage, private val timelineInput: TimelineInput, private val liveEventService: Lazy) { @@ -345,7 +351,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return roomEntity } - val customList = arrayListOf() private fun handleTimelineEvents(realm: Realm, roomId: String, roomEntity: RoomEntity, @@ -420,6 +425,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle optimizedThreadSummaryMap[it] = eventEntity // Add the same thread timeline event to Thread Chunk addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity) + // Add thread list if needed + if(homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) { + ThreadSummaryEntity.createOrUpdate( + threadSummaryType = ThreadSummaryUpdateType.ADD, + realm = realm, + roomId = roomId, + threadEventEntity = eventEntity, + roomMemberContentsByUser = roomMemberContentsByUser, + userId = userId, + roomEntity = roomEntity) + } } ?: run { // This is a normal event or a root thread one optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index ca18060c51..fc76535c4c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -32,7 +32,6 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.views.ThreadListFragment -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @AndroidEntryPoint @@ -92,14 +91,7 @@ class ThreadsActivity : VectorBaseActivity() { * This function is used to navigate to the selected thread timeline. * One usage of that is from the Threads Activity */ - fun navigateToThreadTimeline( - timelineEvent: TimelineEvent) { - val roomThreadDetailArgs = ThreadTimelineArgs( - roomId = timelineEvent.roomId, - displayName = timelineEvent.senderInfo.displayName, - avatarUrl = timelineEvent.senderInfo.avatarUrl, - roomEncryptionTrustLevel = null, - rootThreadEventId = timelineEvent.eventId) + fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) { val commonOption: (FragmentTransaction) -> Unit = { it.setCustomAnimations( R.anim.animation_slide_in_right, @@ -111,8 +103,8 @@ class ThreadsActivity : VectorBaseActivity() { container = views.threadsActivityFragmentContainer, fragmentClass = TimelineFragment::class.java, params = TimelineArgs( - roomId = timelineEvent.roomId, - threadTimelineArgs = roomThreadDetailArgs + roomId = threadTimelineArgs.roomId, + threadTimelineArgs = threadTimelineArgs ), option = commonOption ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 32cb006810..d3a5497d63 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -23,15 +23,19 @@ import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.threads.list.model.threadListItem +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toMatrixItemOrNull import javax.inject.Inject class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, - private val dateFormatter: VectorDateFormatter + private val dateFormatter: VectorDateFormatter, + private val session: Session ) : EpoxyController() { var listener: Listener? = null @@ -43,10 +47,59 @@ class ThreadListController @Inject constructor( requestModelBuild() } - override fun buildModels() { + override fun buildModels() = + when (session.getHomeServerCapabilities().canUseThreading) { + true -> buildThreadSummaries() + false -> buildThreadList() + } + + /** + * Building thread summaries when homeserver + * supports threading + */ + private fun buildThreadSummaries() { val safeViewState = viewState ?: return val host = this + safeViewState.threadSummaryList.invoke() + ?.filter { + if (safeViewState.shouldFilterThreads) { + it.isUserParticipating + } else { + true + } + } + ?.forEach { threadSummary -> + val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) + val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) + val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition + val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition + threadListItem { + id(threadSummary.rootEvent?.eventId) + avatarRenderer(host.avatarRenderer) + matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem()) + title(threadSummary.rootThreadSenderInfo.displayName.orEmpty()) + date(date) + rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) + // TODO refactor notifications that with the new thread summary + threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) + rootMessage(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) + lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) + lastMessageCounter(threadSummary.numberOfThreads.toString()) + lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) + itemClickListener { + host.listener?.onThreadSummaryClicked(threadSummary) + } + } + } + } + /** + * Building local thread list when homeserver do not + * support threading + */ + private fun buildThreadList() { + val safeViewState = viewState ?: return + val host = this safeViewState.rootThreadEventList.invoke() ?.filter { if (safeViewState.shouldFilterThreads) { @@ -74,13 +127,14 @@ class ThreadListController @Inject constructor( lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) itemClickListener { - host.listener?.onThreadClicked(timelineEvent) + host.listener?.onThreadListClicked(timelineEvent) } } } } interface Listener { - fun onThreadClicked(timelineEvent: TimelineEvent) + fun onThreadSummaryClicked(threadSummary: ThreadSummary) + fun onThreadListClicked(timelineEvent: TimelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index d82b5d6ccf..290b71a504 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow @@ -53,11 +54,41 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState } init { - observeThreadsList() + observeThreads() + fetchThreadList() } override fun handle(action: EmptyAction) {} + /** + * Observing thread list with respect to homeserver + * capabilities + */ + private fun observeThreads() { + when (session.getHomeServerCapabilities().canUseThreading) { + true -> observeThreadSummaries() + false -> observeThreadsList() + } + } + + /** + * Observing thread summaries when homeserver support + * threading + */ + private fun observeThreadSummaries() { + room?.flow() + ?.liveThreadSummaries() + ?.map { room.enhanceWithEditions(it) } + ?.flowOn(room.coroutineDispatchers.io) + ?.execute { asyncThreads -> + copy(threadSummaryList = asyncThreads) + } + } + + /** + * Observing thread list when homeserver do not support + * threading + */ private fun observeThreadsList() { room?.flow() ?.liveThreadList() @@ -74,6 +105,14 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState } } + private fun fetchThreadList() { + viewModelScope.launch { + room?.fetchThreadSummaries() + } + } + + fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading + fun applyFiltering(shouldFilterThreads: Boolean) { setState { copy(shouldFilterThreads = shouldFilterThreads) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt index 2a70a5be1e..e08f70030b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt @@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent data class ThreadListViewState( + val threadSummaryList: Async> = Uninitialized, val rootThreadEventList: Async> = Uninitialized, val shouldFilterThreads: Boolean = false, val roomId: String ) : MavericksState { - constructor(args: ThreadListArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 180e6226d0..949778629b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -34,9 +34,11 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -111,12 +113,30 @@ class ThreadListFragment @Inject constructor( views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName } - override fun onThreadClicked(timelineEvent: TimelineEvent) { - (activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent) + override fun onThreadSummaryClicked(threadSummary: ThreadSummary) { + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = threadSummary.roomId, + displayName = threadSummary.rootThreadSenderInfo.displayName, + avatarUrl = threadSummary.rootThreadSenderInfo.avatarUrl, + roomEncryptionTrustLevel = null, + rootThreadEventId = threadSummary.rootEventId) + (activity as? ThreadsActivity)?.navigateToThreadTimeline(roomThreadDetailArgs) + } + + override fun onThreadListClicked(timelineEvent: TimelineEvent) { + val threadTimelineArgs = ThreadTimelineArgs( + roomId = timelineEvent.roomId, + displayName = timelineEvent.senderInfo.displayName, + avatarUrl = timelineEvent.senderInfo.avatarUrl, + roomEncryptionTrustLevel = null, + rootThreadEventId = timelineEvent.eventId) + (activity as? ThreadsActivity)?.navigateToThreadTimeline(threadTimelineArgs) } private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { - val show = state.rootThreadEventList.invoke().isNullOrEmpty() - views.threadListEmptyConstraintLayout.isVisible = show + when (threadListViewModel.canHomeserverUseThreading()) { + true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() + false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() + } } } From f4f48b919e13367413f5211b77bd5e5652d85db2 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 21 Feb 2022 12:14:51 +0200 Subject: [PATCH 007/405] Improve home server capabilities for threads --- .../internal/database/mapper/HomeServerCapabilitiesMapper.kt | 2 +- .../session/homeserver/GetHomeServerCapabilitiesTask.kt | 3 ++- .../internal/session/sync/handler/room/RoomSyncHandler.kt | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 8be3455c07..2e33988a22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -42,7 +42,7 @@ internal object HomeServerCapabilitiesMapper { lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, defaultIdentityServerUrl = entity.defaultIdentityServerUrl, roomVersions = mapRoomVersion(entity.roomVersionsJson), - canUseThreading = false // entity.canUseThreading + canUseThreading = entity.canUseThreading ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 8c6bb626d1..ae8e31c8a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions @@ -121,7 +122,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) } - homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue() + homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() } if (getMediaConfigResult != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 63857d611b..591b3c2ed5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -64,7 +64,6 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent -import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.reportSubtask @@ -425,8 +424,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle optimizedThreadSummaryMap[it] = eventEntity // Add the same thread timeline event to Thread Chunk addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity) - // Add thread list if needed - if(homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) { + if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) { + // Update thread summaries only if homeserver supports threading ThreadSummaryEntity.createOrUpdate( threadSummaryType = ThreadSummaryUpdateType.ADD, realm = realm, From 2b740a1ab662965c24909f53d6c52c0f80284836 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 21 Feb 2022 17:23:17 +0200 Subject: [PATCH 008/405] Implement permalink support for /relations live thread timeline --- .../session/room/timeline/DefaultTimeline.kt | 8 ++++++- .../session/room/timeline/TimelineChunk.kt | 3 ++- .../home/room/detail/TimelineViewModel.kt | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 5662986663..8c2b4d2bbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -301,7 +301,13 @@ internal class DefaultTimeline(private val roomId: String, Timber.v("Post snapshot of ${snapshot.size} events") withContext(coroutineDispatchers.main) { listeners.forEach { - tryOrNull { it.onTimelineUpdated(snapshot) } + if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) { + // We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found + tryOrNull { it.onTimelineUpdated(arrayListOf()) } + } else { + // In all the other cases update timeline as expected + tryOrNull { it.onTimelineUpdated(snapshot) } + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 864b3f0dd9..34aa83f9c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -171,11 +171,12 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, * always fetch results, while we want our data to be up to dated. */ suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult { + val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE return if (direction == Timeline.Direction.BACKWARDS) { try { fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( roomId, - timelineSettings.rootThreadEventId!!, + rootThreadEventId, chunkEntity.prevToken, count )).toLoadMoreResult() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index a404f9136b..b882990e30 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -156,6 +156,9 @@ class TimelineViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { const val PAGINATION_COUNT = 50 + // The larger the number the faster the results, COUNT=200 for 500 thread messages its x4 faster than COUNT=50 + const val PAGINATION_COUNT_THREADS_PERMALINK = 200 + } init { @@ -1174,10 +1177,30 @@ class TimelineViewModel @AssistedInject constructor( } } + /** + * Navigates to the appropriate event (by paginating the thread timeline until the event is found + * in the snapshot. The main reason for this function is to support the /relations api + */ + private var threadPermalinkHandled = false + private fun navigateToThreadEventIfNeeded(snapshot: List) { + if (eventId != null && initialState.rootThreadEventId != null) { + // When we have a permalink and we are in a thread timeline + if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) { + // Permalink event found lets navigate there + handleNavigateToEvent(RoomDetailAction.NavigateToEvent(eventId, true)) + threadPermalinkHandled = true + } else { + // Permalink event not found yet continue paginating + timeline.paginate(Timeline.Direction.BACKWARDS, PAGINATION_COUNT_THREADS_PERMALINK) + } + } + } + override fun onTimelineUpdated(snapshot: List) { viewModelScope.launch { // tryEmit doesn't work with SharedFlow without cache timelineEvents.emit(snapshot) + navigateToThreadEventIfNeeded(snapshot) } } From 0f721d971c1fe2e58406598c8d91cf3decb7e814 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 21 Feb 2022 17:24:34 +0200 Subject: [PATCH 009/405] Ktlint format --- .../vector/app/features/home/room/detail/TimelineViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index b882990e30..a831332407 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -156,9 +156,9 @@ class TimelineViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { const val PAGINATION_COUNT = 50 + // The larger the number the faster the results, COUNT=200 for 500 thread messages its x4 faster than COUNT=50 const val PAGINATION_COUNT_THREADS_PERMALINK = 200 - } init { From deb86d2e874d00c8e895b4aa7de34266cae8dd69 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 22 Feb 2022 13:18:09 +0200 Subject: [PATCH 010/405] Resolve real migration conflicts --- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/migration/MigrateSessionTo026.kt | 28 +++++++++++ .../database/migration/MigrateSessionTo027.kt | 49 ------------------- 3 files changed, 29 insertions(+), 52 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 24ac310653..a57397dad5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -44,7 +44,6 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 -import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -59,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 27L + val schemaVersion = 26L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -90,6 +89,5 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 24) MigrateSessionTo024(realm).perform() if (oldVersion < 25) MigrateSessionTo025(realm).perform() if (oldVersion < 26) MigrateSessionTo026(realm).perform() - if (oldVersion < 27) MigrateSessionTo027(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt index 597d6d1cbe..04b64c2893 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt @@ -19,9 +19,17 @@ package org.matrix.android.sdk.internal.database.migration import io.realm.DynamicRealm import io.realm.FieldAttribute import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator +/** + * Migrating to: + * Live thread list: using enhanced /messages api MSC3440 + * Live thread timeline: using /relations api + */ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { override fun doMigrate(realm: DynamicRealm) { @@ -31,5 +39,25 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { realm.schema.get("TimelineEventEntity") ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java) + + val eventEntity = realm.schema.get("EventEntity") ?: return + val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity") + .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java) + .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java) + .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity) + .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity) + + realm.schema.get("RoomEntity") + ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity) + + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt deleted file mode 100644 index b56b7d325b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.database.migration - -import io.realm.DynamicRealm -import io.realm.FieldAttribute -import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields -import org.matrix.android.sdk.internal.database.model.RoomEntityFields -import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields -import org.matrix.android.sdk.internal.util.database.RealmMigrator - -class MigrateSessionTo027(realm: DynamicRealm) : RealmMigrator(realm, 27) { - - override fun doMigrate(realm: DynamicRealm) { - val eventEntity = realm.schema.get("EventEntity") ?: return - val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity") - .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) - .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java) - .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java) - .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) - .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java) - .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java) - .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) - .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java) - .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java) - .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity) - .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity) - - realm.schema.get("RoomEntity") - ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity) - - realm.schema.get("HomeServerCapabilitiesEntity") - ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) - } -} From 9953d0d0ed095f6c144e80e9ac0fbd724462c2a6 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 22 Feb 2022 13:57:43 +0200 Subject: [PATCH 011/405] Resolve realm migration conflicts --- .../sdk/internal/database/mapper/ThreadSummaryMapper.kt | 2 +- .../sdk/internal/database/migration/MigrateSessionTo026.kt | 6 +++--- .../internal/database/model/threads/ThreadSummaryEntity.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt index 54386c5ea8..cedb9e3d45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt @@ -28,7 +28,7 @@ internal class ThreadSummaryMapper @Inject constructor() { roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(), rootEvent = threadSummary.rootThreadEventEntity?.asDomain(), latestEvent = threadSummary.latestThreadEventEntity?.asDomain(), - rootEventId = threadSummary.rootThreadEventId, + rootEventId = threadSummary.rootThreadEventId.orEmpty(), rootThreadSenderInfo = SenderInfo( userId = threadSummary.rootThreadEventEntity?.sender ?: "", displayName = threadSummary.rootThreadSenderName, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt index 04b64c2893..ac097c916b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt @@ -45,10 +45,10 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java) .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java) - .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java) .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java) .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java) - .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java) + .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java) .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java) .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java) .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity) @@ -58,6 +58,6 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity) realm.schema.get("HomeServerCapabilitiesEntity") - ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) + ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt index 21a80502e7..ab9d66548e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt @@ -23,7 +23,7 @@ import io.realm.annotations.LinkingObjects import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.RoomEntity -internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String = "", +internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "", var rootThreadEventEntity: EventEntity? = null, var latestThreadEventEntity: EventEntity? = null, var rootThreadSenderName: String? = null, From 2054c577f3ad4e74362ad4ceb8bfe50f1ac04c57 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 22 Feb 2022 17:41:54 +0200 Subject: [PATCH 012/405] Fix quality check errors --- .../sdk/api/session/room/threads/model/ThreadEditions.kt | 4 ++-- .../sdk/internal/database/helper/ThreadSummaryHelper.kt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt index db92e800e4..05d6fd52cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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 + * 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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index d19056adfa..2d1b1cccf4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -275,7 +275,11 @@ private fun createEventEntity(roomId: String, event: Event, realm: Realm): Event * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member * state */ -private fun createLatestEventEntity(roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap, realm: Realm): EventEntity? { +private fun createLatestEventEntity( + roomId: String, + rootThreadEvent: Event, + roomMemberContentsByUser: HashMap, + realm: Realm): EventEntity? { return getLatestEvent(rootThreadEvent)?.let { it.senderId?.let { senderId -> roomMemberContentsByUser.addSenderState(realm, roomId, senderId) From f7f363ce257c9cbc66f9cca81b89f974ba860741 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 22 Feb 2022 20:52:01 +0200 Subject: [PATCH 013/405] Fix wrong copyrights --- .../sdk/api/session/room/threads/model/ThreadSummary.kt | 4 ++-- .../api/session/room/threads/model/ThreadSummaryUpdateType.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt index f26be85e85..017afba1ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt index 744265cb94..95697f987f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, From 79c97ac5129ea530f56bf8c870bfe10555fa6833 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 22 Feb 2022 20:59:22 +0200 Subject: [PATCH 014/405] Formating code --- .../api/session/room/threads/model/ThreadEditions.kt | 2 +- .../internal/database/helper/ThreadSummaryHelper.kt | 10 +--------- .../room/EventRelationsAggregationProcessor.kt | 12 ++++++------ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt index 05d6fd52cd..c8353cf0de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 2d1b1cccf4..229e433a89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -56,17 +56,9 @@ internal fun ThreadSummaryEntity.updateThreadSummary( roomMemberContentsByUser: HashMap) { updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser) updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser) - - // Update latest event -// latestThreadEventEntity?.toTimelineEventEntity(roomMemberContentsByUser)?.let { -// Timber.i("###THREADS FetchThreadSummariesTask ThreadSummaryEntity updated latest event:${it.eventId} !") -// this.eventEntity?.threadSummaryLatestMessage = it -// } - - // Update number of threads this.isUserParticipating = isUserParticipating numberOfThreads?.let { - // Update only when there is an actual value + // Update number of threads only when there is an actual value this.numberOfThreads = it } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index d17d16a82d..573aba1aca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -197,15 +197,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleReaction(realm, event, roomId, isLocalEcho) } } - // TODO is that ok?? + // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations // else if (event.unsignedData?.relations?.annotations != null) { // Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}") // handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) -// // EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() -// // ?.let { -// // TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() -// // ?.forEach { tet -> tet.annotations = it } -// // } +// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() +// ?.let { +// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() +// ?.forEach { tet -> tet.annotations = it } +// } // } } EventType.REDACTION -> { From a9b3882bf6b93cd5a30521eaa01217350b8d62b0 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 23 Feb 2022 12:06:35 +0200 Subject: [PATCH 015/405] Add changelogs --- changelog.d/5230.feature | 1 + changelog.d/5232.feature | 1 + changelog.d/5271.sdk | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/5230.feature create mode 100644 changelog.d/5232.feature create mode 100644 changelog.d/5271.sdk diff --git a/changelog.d/5230.feature b/changelog.d/5230.feature new file mode 100644 index 0000000000..b333a3f2c7 --- /dev/null +++ b/changelog.d/5230.feature @@ -0,0 +1 @@ +Thread timeline is now live and much faster especially for large or old threads \ No newline at end of file diff --git a/changelog.d/5232.feature b/changelog.d/5232.feature new file mode 100644 index 0000000000..8f3bec97bd --- /dev/null +++ b/changelog.d/5232.feature @@ -0,0 +1 @@ +View all threads per room screen is now live when the home server supports threads \ No newline at end of file diff --git a/changelog.d/5271.sdk b/changelog.d/5271.sdk new file mode 100644 index 0000000000..b73d97ee4f --- /dev/null +++ b/changelog.d/5271.sdk @@ -0,0 +1 @@ +Adds support for MSC3440, additional threads homeserver capabilities \ No newline at end of file From 8788fb974d081f3e67fac56c26eb2427c7562c7f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 23 Feb 2022 18:39:02 +0200 Subject: [PATCH 016/405] Add new use case about threads in the allScreensTest --- .../vector/app/ui/UiAllScreensSanityTest.kt | 24 ++++++++++++++ .../im/vector/app/ui/robot/ElementRobot.kt | 27 ++++++++++++++-- .../vector/app/ui/robot/MessageMenuRobot.kt | 9 ++++++ .../im/vector/app/ui/robot/RoomDetailRobot.kt | 31 ++++++++++++++++++- .../app/ui/robot/settings/SettingsRobot.kt | 10 ++++-- .../app/ui/robot/settings/labs/LabFeature.kt | 26 ++++++++++++++++ 6 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 417d28d625..5a03d5890a 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -27,6 +27,7 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule import im.vector.app.features.MainActivity import im.vector.app.getString import im.vector.app.ui.robot.ElementRobot +import im.vector.app.ui.robot.settings.labs.LabFeature import im.vector.app.ui.robot.withDeveloperMode import org.junit.Rule import org.junit.Test @@ -97,6 +98,8 @@ class UiAllScreensSanityTest { } } + testThreadScreens() + elementRobot.space { createSpace { crawl() @@ -148,4 +151,25 @@ class UiAllScreensSanityTest { // TODO Deactivate account instead of logout? elementRobot.signout(expectSignOutWarning = false) } + + /** + * Testing multiple threads screens + */ + private fun testThreadScreens() { + elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES) + elementRobot.newRoom { + createNewRoom { + crawl() + createRoom { + val message = "Hello This message will be a thread!" + postMessage(message) + replyToThread(message) + viewInRoom(message) + openThreadSummaries() + selectThreadSummariesFilter() + } + } + } + elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES) + } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index f0ce23b7db..3c5de8b221 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -17,9 +17,15 @@ package im.vector.app.ui.robot import android.view.View +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton @@ -35,6 +41,7 @@ import im.vector.app.features.home.HomeActivity import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.initialSyncIdlingResource import im.vector.app.ui.robot.settings.SettingsRobot +import im.vector.app.ui.robot.settings.labs.LabFeature import im.vector.app.ui.robot.space.SpaceRobot import im.vector.app.withIdlingResource import timber.log.Timber @@ -70,11 +77,11 @@ class ElementRobot { } } - fun settings(block: SettingsRobot.() -> Unit) { + fun settings(shouldGoBack: Boolean = true, block: SettingsRobot.() -> Unit) { openDrawer() clickOn(R.id.homeDrawerHeaderSettingsView) block(SettingsRobot()) - pressBack() + if (shouldGoBack) pressBack() waitUntilViewVisible(withId(R.id.bottomNavigationView)) } @@ -103,6 +110,22 @@ class ElementRobot { waitUntilViewVisible(withId(R.id.bottomNavigationView)) } + fun toggleLabFeature(labFeature: LabFeature) { + when (labFeature) { + LabFeature.THREAD_MESSAGES -> { + settings(shouldGoBack = false) { + labs(shouldGoBack = false) { + onView(withText(R.string.labs_enable_thread_messages)) + .check(ViewAssertions.matches(isDisplayed())) + .perform(ViewActions.closeSoftKeyboard(), click()) + } + } + } + else -> { + } + } + } + fun signout(expectSignOutWarning: Boolean) { clickOn(R.id.groupToolbarAvatarImageView) clickOn(R.id.homeDrawerHeaderSignoutView) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt index 5973dc3473..5c9ecfdef5 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt @@ -70,4 +70,13 @@ class MessageMenuRobot( clickOn(R.string.edit) autoClosed = true } + + fun replyInThread() { + clickOn(R.string.reply_in_thread) + autoClosed = true + } + fun viewInRoom() { + clickOn(R.string.view_in_room) + autoClosed = true + } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 6cf6ad3551..91409582d9 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -62,6 +62,23 @@ class RoomDetailRobot { pressBack() } + fun replyToThread(message: String) { + openMessageMenu(message) { + replyInThread() + } + val threadMessage = "Hello universe - long message to avoid espresso tapping edited!" + writeTo(R.id.composerEditText, threadMessage) + waitUntilViewVisible(withId(R.id.sendButton)) + clickOn(R.id.sendButton) + } + + fun viewInRoom(message: String) { + openMessageMenu(message) { + viewInRoom() + } + waitUntilViewVisible(withId(R.id.composerEditText)) + } + fun crawlMessage(message: String) { // Test quick reaction val quickReaction = EmojiDataSource.quickEmojis[0] // 👍 @@ -110,7 +127,7 @@ class RoomDetailRobot { onView(withId(R.id.timelineRecyclerView)) .perform( RecyclerViewActions.actionOnItem( - ViewMatchers.hasDescendant(ViewMatchers.withText(message)), + ViewMatchers.hasDescendant(withText(message)), ViewActions.longClick() ) ) @@ -130,4 +147,16 @@ class RoomDetailRobot { block(RoomSettingsRobot()) pressBack() } + + fun openThreadSummaries() { + clickMenu(R.id.menu_timeline_thread_list) + waitUntilViewVisible(withId(R.id.threadListRecyclerView)) + } + + fun selectThreadSummariesFilter() { + clickMenu(R.id.menu_thread_list_filter) + sleep(1000) + clickOn(R.id.threadListModalMyThreads) + pressBack() + } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt index 561f14c6f2..97aee7ac4a 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt @@ -16,6 +16,7 @@ package im.vector.app.ui.robot.settings +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.clickOnAndGoBack @@ -51,8 +52,13 @@ class SettingsRobot { clickOnAndGoBack(R.string.settings_security_and_privacy) { block(SettingsSecurityRobot()) } } - fun labs(block: () -> Unit = {}) { - clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() } + fun labs(shouldGoBack: Boolean = true, block: () -> Unit = {}) { + if (shouldGoBack) { + clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() } + } else { + clickOn(R.string.room_settings_labs_pref_title) + block() + } } fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) { diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt new file mode 100644 index 0000000000..656201d812 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.ui.robot.settings.labs + +enum class LabFeature { + SWIPE_TO_REPLY, + TAB_UNREAD_NOTIFICATIONS, + LATEX_MATHEMATICS, + THREAD_MESSAGES, + AUTO_REPORT_ERRORS, + RENDER_USER_LOCATION +} From 985007a1c18241efb96b956a5b9e0f56ad70eeba Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Sat, 26 Feb 2022 10:46:51 +0000 Subject: [PATCH 017/405] Translated using Weblate (Czech) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- vector/src/main/res/values-cs/strings.xml | 43 ++--------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 9d0220f3a6..cf022172c2 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -38,7 +38,6 @@ Telefonní číslo Pozvání do místnosti %1$s a %2$s - Prázdná místnost %s povýšili tuto místnost. %1$s zrušili pozvánku do místnosti pro %2$s @@ -260,7 +259,6 @@ Trvalý odkaz Zobrazit dešifrovaný zdroj Nahlásit obsah - Odhlásit Hlasový hovor Video hovor @@ -281,7 +279,6 @@ Pouze kontakty Matrix Žádné výsledky Místnosti - Komunity Odeslat záznamy Odeslat záznamy zřícení @@ -334,8 +331,6 @@ \nPřejete si nějaké přidat nyní\? Omlouváme se, ale nebyla nalezena žádná externí aplikace pro dokončení této akce. Šifrovaná zpráva - - Prosím, přečtěte si a souhlaste s pravidly tohoto serveru: Neobsahuje platný JSON Znovu požádat o šifrovací klíče z vašich ostatních relací. @@ -360,10 +355,7 @@ Hovor probíhá… Protější strana hovor nepřijala. Informace - - ${app_name} potřebuje oprávnění pro přístup k Vašemu mikrofonu pro uskutečnění hlasových hovorů. - ANO NE Pokračovat @@ -373,16 +365,11 @@ Odmítnout Zobrazit členy Přejít na nepřečtené - %d člen %d členové %d členů - - - - Opustit místnost Opravdu chcete opustit tuto místnost\? PŘÍMÉ KONVERZACE @@ -431,8 +418,6 @@ ${app_name} potřebuje oprávnění pro přístup k Vaší kameře a mikrofonu pro uskutečnění video hovoru. \n \nProsím, povolte přístup na následující hlášce abyste mohli uskutečnit hovor. - - Tuto změnu nelze zvrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy. \nOpravdu to chcete udělat\? Toto by mohlo znamenat, že někdo škodlivě zachytává Vaši komunikaci nebo že Váš telefon nedůvěřuje certifikátu poskytnutému vzdáleným serverem. @@ -440,12 +425,9 @@ Certifikát se změnil z toho, kterému Váš telefon důvěřoval. Toto je VELMI NEOBVYKLÉ. Je doporučeno, abyste NEPŘIJALI tento nový certifikát. Certifikát se změnil z původně důvěryhodného na nyní nedůvěryhodný. Server patrně obnovil svůj certifikát. Kontaktujte administrátora kvůli očekávanému otisku. Přijměte certifikát pouze pokud administrátor serveru publikoval otisk, který odpovídá tomu uvedenému výše. - Vyhledat Filtrovat členy místnosti Žádné výsledky - - Všechny zprávy Přidat na domovskou obrazovku Obrázek profilu @@ -462,7 +444,6 @@ Označit za přečtené Žádný Zrušit - Přihlásit se se single sign-on To není platná adresa Matrix serveru Domovský server není dostupný na této adrese, zkontrolujte ji prosím @@ -553,7 +534,6 @@ Neobdržíte oznámení o příchozích zprávách, je-li aplikace na pozadí. Start při zavádění Čas požadavku na sync vypršel - Prodleva mezi jednotlivými syncy Verze Verze olm @@ -603,7 +583,6 @@ Deaktivovat můj účet Objevování Správa Vašich nastavení pro objevování. - Analýza Odeslat analytická data ${app_name} sbírá anonymní analytická data pro vylepšení aplikace. @@ -612,7 +591,6 @@ Aktualizovat veřejné jméno Viděn naposledy %1$s @ %2$s - Ověření Přihlášen jako Domovský server @@ -661,7 +639,6 @@ Toto jsou experimentální funkce, které mohou selhat neočekávanými způsoby. Použijte obezřetně. Nastavit jako hlavní adresu Odebrat jako hlavní adresu - Motiv vzhledu Chyba dešifrování Veřejné jméno @@ -672,7 +649,6 @@ Export klíčů do místního souboru Export Prosím, vytvořte frázi k zašifrování exportovaných klíčů. Pro import klíčů budete muset zadat stejnou přístupovou frázi. - Obnovení zašifrovaných zpráv Správa zálohy klíčů Import E2E klíčů místností @@ -721,7 +697,6 @@ Importovat e2e klíče ze souboru \"%1$s\". Potvrďte porovnáním následujícího s nastavením uživatele ve svých dalších relacích: Pokud se neshodují, zabezpečení Vaší komunikace může být ohroženo. - Vybrat adresář místností Název serveru Všechny místnosti na serveru %s @@ -731,7 +706,6 @@ %d nepřečtené oznámené zprávy %d nepřečtených oznámených zpráv - %d místnost %d místnosti @@ -834,8 +808,6 @@ Úvod Místnosti Pozvaní - - %2$s Vás vykopnul z %1$s %2$s Vám zakázal %1$s Důvod: %1$s @@ -899,7 +871,6 @@ Uložit klíč obnovy Sdílet Uložit jako soubor - Záloha již existuje na Vašem domovském serveru Vypadá to, že jste již nastavili zálohu klíče z jiné relace. Chcete ji nahradit zálohou, již právě provádíte\? Nahradit @@ -942,7 +913,6 @@ Kontroluji stav zálohy Smazat zálohu Smazat Vaše zálohované šifrovací klíče ze serveru\? Ke čtení šifrované historie zpráv již nebude moci použít klíč obnovy. - Nikdy neztraťte šifrované zprávy Použíjte zálohu klíče Nový bezpečný klíč zpráv @@ -950,11 +920,8 @@ Verze Algoritmus Podpis - Ověřeno! Rozumím - - Žádost na ověření %s chce ověřit Vaši relaci Neznámá chyba @@ -1107,7 +1074,6 @@ Obsah byl nahlášen jako nepatřičný. \n \nPokud si dále nepřejete vidět obsah tohoto uživatele, můžete jej ignorovat a tím skrýt jejich zprávy. - Ignorovat uživatele Všechny zprávy (hlučné) Všechny zprávy @@ -1295,7 +1261,6 @@ Ověřit %s Ověřeno %s Čekám na %s… - Zprávy v této místnosti nejsou koncově šifrovány. Zprávy v této místnosti jsou koncově šifrovány. \n @@ -1443,7 +1408,6 @@ Vytiskněte a uložte na bezpečném místě Uložte je na USB nebo zálohový disk Nahrajte do svého osobního úložiště v cloudu - Šifrování zapnuto Zprávy v této místnosti jsou koncově šifrovány. Zjistěte více a ověřte uživatele v jejich profilech. Šifrování není zapnuto @@ -1855,7 +1819,6 @@ Skrýt pokročilé Ukázat pokročilé %1$d z %2$d - Udělit souhlas Zrušit můj souhlas Udělili jste souhlas pro odeslání emailových adres a telefonních čísel na tento server pro identity za účelem nalezení dalších uživatelů podle svých kontaktů. @@ -1948,8 +1911,6 @@ Při přepojování hovoru došlo k chybě Připojit Nejprve se poraďte - - Probíhající hovor (%1$s) Při vyhledávání telefonního čísla došlo k chybě Číselník @@ -2149,7 +2110,6 @@ Zadejte název nového serveru, který chcete prozkoumat. Přidat nový server Váš server - Omlouváme se, došlo k chybě během pokusu o přistoupení: %s Adresa prostoru Prohlédnout a spravovat adresy tohoto prostoru. @@ -2354,7 +2314,6 @@ Otázka nebo téma hlasování Vytvořit hlasování Hlasování - Odeslat e-maily a telefonní čísla na %s Vaše kontakty jsou soukromé. Pro zjištění uživatelů z vašich kontaktů, potřebujeme vaše svolení k odeslání informací o kontaktech na váš server identit. Relace byla odhlášena! @@ -2505,4 +2464,6 @@ %1$d dalších Zobrazit méně + %1$s, %2$s a další + %1$s a %2$s \ No newline at end of file From f507c6c4a9a4e07a9a0760abbd7f32398d8c29b7 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 26 Feb 2022 10:36:50 +0000 Subject: [PATCH 018/405] Translated using Weblate (Hungarian) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/ --- vector/src/main/res/values-hu/strings.xml | 41 ++--------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 40121e2d1a..000da846b8 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -39,7 +39,6 @@ Meghívó egy szobába %1$s és %2$s Üres szoba - Induló szinkronizáció: \nFiók betöltése… Induló szinkronizáció: @@ -179,7 +178,6 @@ Törlés Átnevezés Tartalom Bejelentése - vagy Meghívás Kijelentkezés @@ -202,7 +200,6 @@ Csak Matrix névjegyek Nincs találat Szobák - Naplófájlok küldése Összeomlásnaplók küldése Képernyőkép küldése @@ -230,11 +227,9 @@ Ez nem tűnik érvényes e-mail címnek Ez az e-mail cím már használatban van. Elfelejtetted a jelszavad? - A Matrix-kiszolgáló szeretné ellenőrizni, hogy nem vagy robot Meg kell adnod a fiókodhoz tartozó e-mail-címet. Az e-mail-címed ellenőrzés sikertelen: győződj meg róla, hogy rákattintottál az e-mailben található hivatkozásra - Adj meg egy érvényes URL-t Hibás JSON Nem tartalmazott érvényes JSON-t @@ -250,14 +245,10 @@ Hívás folyamatban… A hívott fél nem vette fel. Információ - - A ${app_name}nek engedélyre van szüksége a mikrofon eléréséhez, hogy hanghívás tudjon indítani. - A ${app_name}nek engedélyre van szüksége a mikrofonod és kamerád eléréséhez, hogy videohívást tudj indítani. \n \nEngedélyezd a hozzáférést a következő felugró ablakon, hogy hívást tudj indítani. - IGEN NEM Folytatás @@ -265,7 +256,6 @@ Csatlakozás Elutasítás Ugrás az olvasatlanra - Szoba elhagyása Biztos el akarod hagyni a szobát? KÖZVETLEN CSEVEGÉSEK @@ -292,7 +282,6 @@ A tanúsítvány eltér attól, amit a telefonoddal megbízhatónak jelöltél. Ez RENDKÍVÜL SZOKATLAN. Javasoljuk, hogy NE FOGADD EL ezt az új tanúsítványt. Egy korábban megbízhatónak jelölt tanúsítvány megváltozott. Lehet, hogy a szerver frissítette a tanúsítványát. Lépj kapcsolatba a szerver adminisztrátorával és egyeztesd az ujjlenyomatot. Csak akkor fogadd el a tanúsítványt, ha a szerver adminisztrátortól kapott ujjlenyomat megegyezik a fentivel. - Keresés Szobatagok szűrése Nincs találat @@ -337,7 +326,6 @@ Nyilvános Név frissítése Legutóbb láttuk %1$s @ %2$s - Azonosítás Bejelentkezve mint Matrix szerver @@ -366,7 +354,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< Ezek kísérleti funkciók, ezek elromolhatnak nem számított módokon. Használd elővigyázatossággal. Fő címnek állítás Kiszedés fő címek közül - Visszafejtés hiba Nyilvános név Munkamenet-azonosító @@ -377,7 +364,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< Exportálás Írj be jelmondatot Ellenőrizd a jelmondatot - E2E szoba kulcsok importálása Szoba kulcsok importálása Kulcsok importálás helyi fájlból @@ -389,7 +375,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< Hitelesítés Hogy ellenőrizni lehessen, hogy ez a munkamenet megbízható, kérlek használj más kommunikáció módot a tulajdonossal (pl.: személyesen vagy telefonon keresztül) és kérdezd meg hogy a kulcs amit lát a Felhasználói Beállítások alatt megegyezik-e az alábbi kulccsal: Ha nem egyeznek, akkor a kommunikáció biztonsága kompromittálva lehet. A jövőben ez a hitelesítési mód kényelmesebbé lesz téve. - Válassz egy szoba könyvtárat Szerver neve Összes szoba a %s szerveren @@ -473,7 +458,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< %d tagság változás Tagok listázása - %d tag %d tag @@ -482,13 +466,10 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe."< %d új üzenet %d új üzenet - - %d olvasatlan üzenet %d olvasatlan üzenet - %d szoba %d szoba @@ -544,16 +525,10 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés A beszélgetés itt folytatódik Ez a szoba egy másik beszélgetés folytatása Régebbi üzenetek megjelenítéséhez kattints ide - - - - %d kiválasztva %d kiválasztva - - Rendszerriasztások vedd fel a kapcsolatot a szolgáltatás adminisztrátorával Ez a Matrix szerver túllépte valamely erőforrás korlátot így néhány felhasználó nem tud majd bejelentkezni. @@ -680,7 +655,6 @@ Helyezd biztonságba a kulcsokat, hogy ne vesszenek el. Kész Visszaállítási Kulcs mentése Mentés fájlba - Kérlek, készíts egy másolatot! Visszaállítási Kulcs megosztása… Visszaállítási Kulcs készítése jelmondatból, ez néhány másodpercet igénybe vehet. @@ -774,7 +748,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Üzenet küldése Enter billentyűvel Az Enter billentyű a virtuális billentyűzeten elküldi az üzenetet és nem új sort szúr be A jelszó nem érvényes - Média Alapértelmezett tömörítés Válassz @@ -811,8 +784,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Mellőz Ellenőrizve! Értem - - Ellenőrzési kérés %s szeretné ellenőrizni a munkamenetedet Ismeretlen Hiba @@ -909,7 +880,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Nincs Visszavonás Bontás - Nem érhető el Matrix-kiszolgáló ezen a címen, ellenőrizd Háttér Szinkronizálási Mód Optimalizált akkumulátor használat @@ -920,7 +890,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze \nEz befolyásolja a rádió és az akkumulátor használatot, és folyamatosan egy értesítés fog megjelenni arról, hogy a ${app_name} figyel a neki küldött eseményekre. Nincs szinkroniziálás a háttérben Nem leszel értesítve az érkező üzenetekről, ha az alkalmazás csak a háttérben fut. - Felderítés Felderítési beállítások megváltoztatása. Nem használsz Azonosítási Szervert @@ -990,7 +959,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Ez a tartalom nem idevalónak lett bejelentve. \n \n Ha nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra. - Integrációk Botok, hidak, kisalkalmazások és matrica csomagok kezeléséhez használj Integrációs Menedzsert. \n @@ -1207,7 +1175,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Ellenőrzés: %s Ellenőrizve: %s Várakozás %s felhasználóra… - Az üzenetek a szobában nincsenek végponttól végpontig titkosítva. A szobában az üzenetek végponttól végpontig titkosítva vannak. \n @@ -1353,7 +1320,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Nyomtasd ki és tárold valahol biztonságos helyen Mentsd el egy USB kulcsra vagy mentő eszközre Másold fel a személyes felhő tárhelyedre - Titkosítás bekapcsolva Ebben a szobában az üzenetek végpontok között titkosítottak. További információkért és ellenőrzéshez nyisd meg a felhasználók profiljait! Titkosítás nincs engedélyezve @@ -1674,7 +1640,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze QR kód Meghívás QR kóddal A QR kód beolvasásához szükség van kamera hozzáférésre. - Elutasítás Fogadás Nincsen jogosultságod konferenciahívás indításához @@ -1939,8 +1904,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Átadás Kapcsolódás Először tájékozódj - - Aktív hívás (%1$s) A telefonszám megkeresésekor hiba történt Tárcsázó számlap @@ -2110,7 +2073,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Add meg a felfedezni kívánt új szerver nevét. Új szerver hozzáadása Matrix szervered - Bocsánat, hiba történt a csatlakozáskor ide: %s Tér cím Tér címek megjelenítése és kezelése. @@ -2298,7 +2260,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Matrix szerver kiválasztása A matrix szervert nem sikerül elérni ezen az URL-en: %s. Ellenőrizd a kapcsolatodat vagy add meg a matrix szervert kézzel. Értesítések figyelése - A névjegyeid személyes adatok. Ahhoz, hogy a névjegyzéked alapján megtalálhass felhasználókat, szükségünk van az engedélyedre, hogy a névjegy adatokat elküldhessük az azonosítási szolgáltatásnak. Legalább %1$s válasz szükséges @@ -2456,4 +2417,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze %d szerver jogosultság változott %d szerver jogosultság változott + %1$s, %2$s és még mások + %1$s és %2$s \ No newline at end of file From ff08ed322fa892a024c877c0f73c0c160f94fde0 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 26 Feb 2022 00:00:43 +0000 Subject: [PATCH 019/405] Translated using Weblate (Indonesian) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- vector/src/main/res/values-in/strings.xml | 45 +---------------------- 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index 097efda7a6..d7856089e8 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -3,7 +3,6 @@ Undangan Ruangan %1$s dan %2$s Ruangan kosong - Pengaturan OK Batal @@ -85,9 +84,7 @@ Nama server Pilih direktori ruang Terverifikasi - Gandakan ke clipboard - Kirim tampilan layar Mohon uraikan kutu tersebut. Apa yang Anda lakukan\? Apa yang Anda harapkan terjadi\? Apa yang sebenarnya terjadi\? Catat dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk catat dan tangkapan layar, tidak akan terlihat secara umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silakan hapus centang: @@ -96,20 +93,15 @@ Kemajuan (%s%%) Nama Pengguna Nama pengguna dan/atau kata sandi salah - Anda perlu memasukkan alamat email yang tertaut pada akun. Verifikasi alamat email gagal: pastikan tautan yang termuat di email telah diklik - JSON amburadul Tidak berisi JSON yang sah Pengajuan yang dikirimkan terlalu banyak Panggilan Video Masuk Panggilan Suara Masuk Panggilan Sedang Berlangsung… - - ${app_name} membutuhkan permisi atas akses mikrofon Anda untuk melakukan panggilan audio. - ${app_name} membutuhkan izin untuk mengakses kamera dan mikrofon Anda untuk melakukan panggilan video. \n \nHarap berikan akses pada halaman berikut ini untuk melakukan panggilan. @@ -142,19 +134,11 @@ %d perubahan keanggotaan Panggilan - - Daftar Anggota Arahkan ke pesan yang belum dibaca - - %d anggota - - - - Tinggalkan ruang Apa benar Anda ingin meninggalkan ruangan ini\? PERCAKAPAN LANGSUNG @@ -189,12 +173,9 @@ %d terpilih - Cari Saring anggota ruang Tidak ada hasil - - Semua pesan Tambahkan ke Layar Utama Gambar Profil @@ -263,8 +244,6 @@ Beranda Ruangan Telah Diundang - - Anda telah dikeluarkan dari %1$s oleh %2$s Anda telah dicekal dari %1$s oleh %2$s Alasan: %1$s @@ -303,13 +282,11 @@ Apabila cocok, tekan tombol verifikasi berikut. Apabila tidak, seseorang sedang menyadap perangkat ini dan mungkin perlu diblokir. Di masa mendatang proses verifikasi ini akan dimutakhirkan. - Semua ruangan dalam server %s Semua ruangan bawaan %s %d pesan pemberitahuan yang belum dibaca - Singkapan Riwayat Ruangan Siapa yang dapat membaca riwayat\? Siapapun @@ -323,7 +300,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Ini adalah fitur uji coba dan mungkin rusak tanpa terduga. Hati-hati menggunakannya. Tentukan sebagai alamat utama Tidak tentukan sebagai alamat utama - Tema Kesalahan dekripsi Nama perangkat @@ -335,7 +311,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Ekspor Masukkan kata sandi Tegaskan kata sandi - Mendengarkan peristiwa Pemberitahuan pihak ketiga Hak cipta @@ -371,7 +346,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Nama Perangkat Terakhir terlihat %1$s @ %2$s - Otentikasi Masuk sebagai Homeserver @@ -663,7 +637,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Gagal membuat koneksi real-time. \nSilakan minta administrator homeserver Anda untuk mengkonfigurasi server TURN agar panggilan untuk bekerja dengan andal. ${app_name} Panggilan Gagal - URL API homeserver Kirim riwayat permintaan pemberian kunci Space @@ -1011,7 +984,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Integrasi dinonaktifkan Pengelola integrasi Izinkan integrasi - Ini akan menggantikan Kunci atau Frasa Anda saat ini. Buat Kunci Keamanan baru atau atur Frasa Keamanan baru untuk cadangan yang ada. Lindungi dari kehilangan akses ke pesan & data terenkripsi dengan mencadangkan kunci enkripsi di server Anda. @@ -1032,7 +1004,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. %d detik - Anda tidak akan diberitahu tentang pesan masuk saat aplikasi berada di latar belakang. Tidak ada sinkronisasi latar belakang ${app_name} akan disinkronkan di latar belakang secara berkala pada waktu yang tepat (dapat dikonfigurasi). @@ -1076,7 +1047,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Memutuskan sambungan dari server identitas Anda akan membuat Anda tidak dapat ditemukan oleh pengguna lain dan Anda tidak akan dapat mengundang orang lain melalui email atau nomor telepon. Kirim email dan nomor telepon Anda telah memberikan persetujuan untuk mengirim email dan nomor telepon ke server identitas ini untuk menemukan pengguna lain dari kontak Anda. - Anda sedang berbagi email atau nomor telepon di server identitas %1$s. Anda harus menyambungkan kembali ke %2$s untuk berhenti membagikannya. Setujui Persyaratan Layanan server identitas (%s) agar Anda dapat ditemukan melalui email atau nomor telepon. Kami mengirimi Anda email konfirmasi ke %s, periksa email Anda dan klik tautan konfirmasi @@ -1147,7 +1117,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Semua pesan Semua pesan (brisik) Abaikan pengguna - Konten ini telah dilaporkan sebagai tidak pantas. \n \nJika Anda tidak ingin melihat konten dari pengguna ini, Anda dapat mengabaikan pengguna itu untuk menyembunyikan pesan dari pengguna. @@ -1295,11 +1264,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kesalahan Tidak Diketahui %s ingin memverifikasi sesi Anda Permintaan Verifikasi - - Saya mengerti Terverifikasi! - Tanda Tangan Algoritma Versi @@ -1316,7 +1282,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Jangan kehilangan pesan terenkripsi Lindungi dari kehilangan akses ke pesan & data terenkripsi Cadangan Aman - Hapus kunci enkripsi yang sudah dicadangkan dari server\? Anda akan tidak dapat menggunakan kunci pemulihan untuk membaca riwayat pesan terenkripsi. Hapus Cadangan Memeriksa status cadangan @@ -1364,7 +1329,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Sepertinya Anda telah menyiapkan cadangan kunci dari sesi lain. Apakah Anda ingin menggantinya dengan yang Anda buat\? Cadangan sudah ada di homeserver Anda Kunci pemulihan telah disimpan. - Simpan sebagai File Bagikan Simpan Kunci Pemulihan @@ -1543,7 +1507,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. \nPesan Anda diamankan dengan kunci dan hanya Anda dan penerima memiliki kunci unik untuk mengakses mereka. Pesan ini tidak terenkripsi secara ujung-ke-ujung. Pesan di ruangan ini tidak terenkripsi secara ujung-ke-ujung. - Menunggu untuk %s… Diverifikasi %s Verifikasi %s @@ -1733,7 +1696,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tingkatkan Mohon sabar, ini mungkin membutuhkan waktu yang lama. Bergabung ke ruangan yang diganti - Ruangan Tanpa Nama Beberapa ruangan mungkin disembunyikan karena mereka privat dan Anda membutuhkan undangan. Beberapa ruangan mungkin disembunyikan karena mereka privat dan Anda membutuhkan undangan. @@ -2086,7 +2048,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Jika Anda batalkan, Anda mungkin kehilangan pesan terenkripsi dan data Anda jika Anda kehilangan akses ke login Anda. \n \nAnda juga dapat mengatur Cadangan Aman dan kelola kunci Anda di Pengaturan. - Salin ke penyimpanan awan pribadi Anda Simpan di flashdisk atau penyimpanan cadangan Cetak dan simpan di tempat yang aman @@ -2179,8 +2140,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. %1$d panggilan aktif · - - Panggilan aktif (%1$s) Ada sebuah kesalahan saat mencari nomor telepon Tombol penyetel @@ -2275,7 +2234,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Pertanyaan atau topik poll Buat Poll Poll - Kirim email dan nomor telepon ke %s Kontak Anda privat. Untuk menemukan pengguna dari kontak Anda, kami membutuhkan izin untuk mengirim info kontak ke server identitas Anda. Sesinya telah dikeluarkan! @@ -2321,7 +2279,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. di sini Bantu kami mengidentifikasi masalah-masalah dan membuat ${app_name} lebih baik dengan membagikan data penggunaan anonim. Untuk memahami bagaimana orang-orang menggunakan beberapa perangkat-perangkat, kami akan membuat pengenal acak, yang dibagikan oleh perangkat Anda. \n -\n \nAnda dapat membaca semua kebijakan kami %s. Bantu buat ${app_name} lebih baik Aktifkan @@ -2415,4 +2372,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. %d perubahan ACL server + %1$s, %2$s dan lainnya + %1$s dan %2$s \ No newline at end of file From 91418493a6501b115decb219cf80276662a44807 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 27 Feb 2022 07:09:54 +0000 Subject: [PATCH 020/405] Translated using Weblate (Japanese) Currently translated at 97.4% (2103 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 138 +++++++++++++++------- 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index d249aeb745..0d35cc8c99 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -23,7 +23,6 @@ ルームへの招待 %1$sと%2$s 空のルーム - %1$sが今後のルーム履歴を%2$sに見えるように設定しました。 ルームのメンバー全員(招待された時点から) ルームのメンバー全員(参加した時点から) @@ -86,7 +85,6 @@ 招待中 低優先度 会話 - 不具合を報告 不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか? ここに不具合の内容を記述 @@ -173,7 +171,6 @@ 最後のオンライン日時 %1$s @ %2$s 認証 - ログイン中のアカウント 言語を選択 言語 @@ -220,11 +217,9 @@ ルーム サインアウト 送信 - このホームサーバーは、あなたがロボットではないことの確認を求めています アカウントに登録されたメールアドレスの入力が必要です。 メールアドレスの確認に失敗しました:電子メールのリンクをクリックしたことを確認してください - 不正な形式のJSON 有効なJSONを含んでいませんでした ログイン要求が多すぎます @@ -256,7 +251,6 @@ %sの全てのメッセージを表示しますか? \n \nこの操作はアプリを再起動するため、時間がかかる場合があります。 - 外観 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート @@ -272,14 +266,10 @@ Matrixの連絡先のみ 通信先が通話の受取に失敗しました。 情報 - - ${app_name}は、音声通話を実行するためにマイクへアクセスするための許可を必要としています。 - ${app_name}はビデオ通話を行うためにカメラとマイクにアクセスする許可を必要としています。 \n \n通話をするためには、次のポップアップでアクセスを許可してください。 - 発言を通報 写真を撮影 動画を撮影 @@ -289,7 +279,6 @@ リクエストの送信に失敗しました。 ウィジェットを作成できません。 ウィジェットをこのルームから削除してもよろしいですか? - 一致していない場合は、あなたのコミュニケーションの安全性が損なわれている可能性があります。 このセッションでは、未検証のセッションに対して暗号化されたメッセージを送信しない。 認証済のセッションに対してのみ暗号化 @@ -306,7 +295,6 @@ 通知あり(音量大) 通知あり(サイレント) 不具合の報告 - このユーザーにあなたと同じ権限を与えます。この変更は取り消せません。 \nよろしいですか? 信用する @@ -318,7 +306,6 @@ 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 サーバーの管理者が上のフィンガープリントと一致するものを発行した場合に限り、証明書を承認してください。 - 検索 このアプリの情報をシステム設定で表示。 アプリの情報 @@ -355,7 +342,6 @@ ホーム画面にショートカットを作成 インラインURLプレビュー コミュニティーのアバター - 暗号鍵を要求している新しいセッション \'%s\' を追加しました。 未認証のセッション \'%s\' が暗号鍵を要求しています。 作成 @@ -370,15 +356,12 @@ %d個のメンバーシップの変更 メンバーを表示 - %d名のメンバー %d件の新しいメッセージ - - アバター スタンプを送る ダウンロード @@ -392,10 +375,6 @@ 申し訳ありません、この操作を完了するための外部アプリが見つかりません。 あなたの他のセッションに暗号鍵を再要求する。 鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で${app_name}を起動してください。 - - - - %d個選択済 @@ -408,7 +387,6 @@ %d件の通知された未読メッセージ - %d個のルーム @@ -430,8 +408,6 @@ 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには - - %1$sのホームサーバーの使用を継続するには、利用規約を確認し、同意する必要があります。 エラー 今すぐ確認 @@ -472,13 +448,11 @@ 会話から追放 鍵のバックアップ 鍵のバックアップを使用 - 詳細な通知設定 バックグラウンド同期モード バッテリーを考慮して最適化 リアルタイム性を重視して最適化 バックグラウンド同期を行わない - 入力中通知を送信 文字入力中であることを他のメンバーに伝えます。 開封確認メッセージを表示 @@ -732,7 +706,7 @@ QRコード QRコードによる追加 コードを共有 - ${app_name} で会話しましょう:%s + ${app_name}で話しましょう:%s 友達を招待 既知のユーザー 無効なQRコード(無効なURI)! @@ -812,7 +786,6 @@ ビデオ通話が行われています… 有効な認証情報がないため、権限がありません ${app_name} 呼び出し失敗 - ルームディレクトリの全てのルームを表示(露骨なコンテンツのあるルームを含む)する。 露骨なコンテンツのあるルームを表示 ルームディレクトリ @@ -970,7 +943,6 @@ %1$sがルームのアバターを削除しました ルームの説明を削除しました ルーム名を削除しました - ディスカバリー設定を管理します。 ディスカバリー(発見) これにより、現在のキーまたはフレーズが置き換えられます。 @@ -1101,7 +1073,6 @@ 鍵のバックアップで管理 鍵のバックアップを使用 暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう - バックアップされた暗号鍵をサーバーから削除しますか?今後、現在のリカバリーキーを使って、暗号化されたメッセージの履歴を読むことができなくなります。 バックアップを削除 バックアップの状態を確認しています @@ -1151,7 +1122,6 @@ 別のセッションで鍵のバックアップを既に設定しているようです。上書きしますか? ホームサーバーにバックアップが存在しています リカバリーキーが保存されました。 - リカバリーキーを保存 コピーをしました リカバリーキーはパスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください @@ -1363,11 +1333,8 @@ 不明なエラー このルームを含む参加済のスペース このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 - - 了解 完了しました! - メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ @@ -1418,7 +1385,6 @@ この操作を実行するための権限がありません。システム設定から権限を付与してください。 IDサーバーに接続できませんでした IDサーバーのURLを入力 - 同意する 同意を撤回 あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。 @@ -1642,7 +1608,7 @@ あなたの非公開スペース あなたの公開スペース 自分のみ - スレッドでディスカッションを整理して管理 + スレッドで議論を整理して管理 %sを待機しています… この端末でスキャン 認証を送信済 @@ -1744,7 +1710,6 @@ このファイルは大きすぎてアップロードできません。 この情報の送信に同意しますか? 連絡先を発見するには、連絡先のデータ(電話番号や電子メール)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 - メールアドレスと電話番号を%sに送信 このIDサーバーはポリシーを提供していません IDサーバーのポリシーを隠す @@ -1886,7 +1851,6 @@ 自分の会話は、自分のもの。 %1$sがこのルームを「招待者のみ参加可能」に設定しました。 選択したメッセージをネタバレとして送信 - %1$sはこのルームを「リンクを知っている人が参加可能」に設定しました。 初めに設定画面でIDサーバーの利用規約を承認してください。 初めにIDサーバーを設定してください。 @@ -1910,7 +1874,7 @@ \nアップグレードは通常、ルームがサーバー上で処理される仕方にだけ影響します。 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 %sして、このルームを皆に紹介しましょう。 - このコードを皆と共有し、スキャンして追加してもらい、会話を始めましょう。 + このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 正当な参加者が%sにアクセスできることを確認してください。 参加者を追加 @@ -2183,7 +2147,6 @@ 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを検証し、本人確認を行う必要があります。 詳しく知る セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを検証しましょう。 - 暗号化の設定が正しくありません。 暗号化を復元 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 @@ -2228,7 +2191,7 @@ %1$sの権限レベルを変更しました。 誰と使いますか? どんなスペースを作りますか? - あなたとチームメイトの非公開のスペース + 自分と仲間の非公開のスペース ルームを整理するためのプライベートスペース ここが会話のスタート地点です。 ここが%sのスタート地点です。 @@ -2266,4 +2229,97 @@ 全てリセット 連絡先 検証がキャンセルされました。再び検証を開始することができます。 + 押し続けて録音し、離すと送信 + セキュリティー向上のため、PINコードを選択してください + + %d個のサーバーアクセス制御リストの変更 + + 置き換えられたルームに参加 + このルームが発見できません。存在することを確認してください。 + 指紋や顔画像など、端末に固有の生体認証を有効にしてください。 + 絵文字で検証 + テキストで検証 + すべてのセッションを検証し、アカウントとメッセージが安全であることを確認してください + ログインしている場所を確認 + 復旧用の手段を全て無くしてしまいましたか?全てリセットする + 、あるいはクロス署名に対応した他のMatrixのクライアント + どのような議論を%sで行いたいですか? + クロス署名の設定に失敗しました + 履歴とメッセージが消去され、信頼済の端末、信頼済のユーザーが取り消されます + 全てをリセットすると + 最新の${app_name}を他の端末で、${app_name} ウェブ版、${app_name} デスクトップ版、${app_name} iOS、${app_name} Android、あるいはクロス署名に対応した他のMatrixのクライアントでご使用ください + スライドして通話を終了 + 電話番号を検索する際にエラーが発生しました + 着信を拒否しました + それぞれにルームを作りましょう。後から追加することもできます(既にあるルームも追加できます)。 + 分かるように特徴を記入してください。これはいつでも変更できます。 + 目立つように特徴を記入してください。これはいつでも変更できます。 + 未読のメッセージ数のみを通知に表示。 + 2分間${app_name}を使用しないと、PINコードが要求されます。 + 🔐️ ${app_name}で話しましょう + 個人情報保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 + アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。 + ステートイベントを送信 + ステートイベント + カスタムのステートイベントを送信 + ステートイベントを送信しました! + 続行するには名前を付けてください。 + どんな作業に取り組みますか? + + あと%1$d件 + + Matrix上の連絡先を検索 + 通話の転送中にエラーが発生しました + 2分後にPINコードを要求 + ルーム名やメッセージの内容などの詳細を表示。 + エラーが多すぎます。ログアウトしました + 警告!もう一度誤ったコードを入力すると、ログアウトします! + + コードが誤っています。残りの試行回数は%d回です + + プッシュ通知を有効にするには、設定を確認してください + リンク %1$s は別のサイトに移動します:%2$s +\n +\n続行してよろしいですか? + このリンクを再確認してください + ログインを検証してください:%1$s + 機密ストレージのアクセスに失敗しました + この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 + このアカウントは無効化されています。 + 個人のクラウドストレージにコピーしましょう + 印刷して安全な場所に保管しましょう + %2$sと%1$sが設定されました。 +\n +\n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 + 作成したアイデンティティーキーを公開しています + アプリケーションのスクリーンショットを防ぐ + 続行するには%1$sか%2$sを使用してください。 + エラーのためメッセージが送信されませんでした + %sにいない人を探していますか? + 直接${app_name}で招待を受け取るには、設定画面から%sしてください。 + PINコードを入力しなければ${app_name}のロックを解除することはできません。 + ${app_name}を開く際にはPINコードの入力が必要です。 + あなたがブロックされているルームを開くことはできません。 + PINコードの検証に失敗しました。新しいコードを入力してください。 + 端末の連絡先がありません + 暗号化の履歴を待機しています + 送信者があなたのセッションを信頼していないため、このメッセージにアクセスすることができません + 送信者によりブロックされているため、このメッセージにアクセスすることができません + このメッセージを待機しています。時間がかかる可能性があります + ルームの設定の変更に成功しました + 確認のため、セキュリティーフレーズを再入力してください。 + ホームサーバー(%1$s)が、IDサーバーに%2$sを設定するよう提案しています + IDサーバー %s から切断しますか? + ダイレクトメッセージを作成できませんでした。招待したユーザーを確認し、もう一度やり直してください。 + セキュリティーフレーズ + 自分と仲間 + メッセージの種類がありません + 絵文字の一覧を閉じる + 絵文字の一覧を開く + 認証に失敗しました + 復旧を設定しています。 + このセッションは、他のセッションと検証を共有することができません。 +\n検証は端末に保存され、新しいバージョンのアプリで共有されます。 + %1$s、%2$s他 + %1$sと%2$s \ No newline at end of file From a4d9b4d5a80046d35ee7e71cb24aee3e14059b34 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Sat, 26 Feb 2022 00:11:55 +0000 Subject: [PATCH 021/405] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- vector/src/main/res/values-pt-rBR/strings.xml | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index b603364d4e..b9e2fa3791 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -39,7 +39,6 @@ Convite de Sala %1$s e %2$s Sala vazia - Seu convite %1$s criou a sala Você criou a sala @@ -244,7 +243,6 @@ Deletar Renomear Reportar Conteúdo - ou Convidar Fazer signout @@ -267,7 +265,6 @@ Contatos de Matrix somente Nenhum resultado Salas - Enviar logs Enviar crash logs Enviar screenshot @@ -295,11 +292,9 @@ Isto não parece com um endereço de email válido Este endereço de email já está definido. Esqueceu senha\? - Este servidorcasa gostaria de assegurar que você não é um robô O endereço de email linkado a sua conta deve ser entrado. Falha para verificar endereço de email: assegure-se que clicou no link no email - Por favor entre um URL válido JSON malformado Não continha JSON válido @@ -315,14 +310,10 @@ Chamada Em Progresso… O lado remoto falhou para atender. Informação - - ${app_name} precisa de permissão para acessar seu microfone para performar chamadas de áudio. - ${app_name} precisa de permissão para acessar sua câmera e seu microfone para performar chamadas de vídeo. \n \nPor favor permita acesso no próximo pop-up para ser capaz de fazer a chamada. - SIM NÃO Continuar @@ -330,7 +321,6 @@ Juntar-se Rejeitar Pular para não-lida(s) - Sair de sala Você tem certeza que você quer sair da sala\? Mensagens Diretas @@ -357,7 +347,6 @@ O certificado tem mudado de um que era confiado por seu telefone. Isto é ALTAMENTE INCOMUM. É recomendado que você NÃO ACEITE este novo certificado. O certificado tem mudado de um previamente confiado para um que não é confiado. O servidor pode ter renovado seu certificado. Contacte o/a administrador(a) de servidor para a impressão digital esperada. Somente aceite o certificado se o/a administrador(a) de servidor tem publicado uma impressão digital que corresponde com a acima. - Pesquisar Filtrar membros de sala Nenhum resultado @@ -402,7 +391,6 @@ Atualizar Nome Público Visto por último %1$s @ %2$s - Autenticação Feito login como Servidorcasa @@ -433,7 +421,6 @@ Estes são recursos experimentais que podem quebrar de maneiras inesperadas. Use com cuidado. Definir como endereço principal Des-definir como endereço principal - Erro de decriptação Nome público ID de sessão @@ -444,7 +431,6 @@ Exportar Entrar frasepasse Confirmar frasepasse - Importar chaves de sala E2E Importar chaves de sala Importar as chaves de um arquivo local @@ -456,7 +442,6 @@ Verificar Confirme ao comparar o seguinte com as Configurações de Usuária(o) em sua outra sessão: Se não correspondem, a segurança de sua comunicação pode estar comprometida. - Selecionar um diretório de salas Nome de servidor Todas as salas em servidor %s @@ -532,7 +517,6 @@ Você foi expulsa(o) de %1$s por %2$s Você foi banida(o) de %1$s por %2$s Razão: %1$s - %d membro %d membros @@ -541,8 +525,6 @@ %d nova mensagem %d novas mensagens - - %d mudança de filiação %d mudanças de filiação @@ -552,7 +534,6 @@ %d mensagem notificada não-lida %d mensagens notificadas não-lidas - %d sala %d salas @@ -576,10 +557,6 @@ Desculpe, nenhum aplicativo externo tem sido encontrado para completar esta ação. Re-requisitar chaves de encriptação de suas outras sessões. Por favor lance ${app_name} num outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão. - - - - %d selecionada %d selecionadas @@ -603,8 +580,6 @@ Muda seu apelido de exibição Ativar/Desativar markdown Para consertar gerenciamento de Apps Matrix - - Para continuar usando o servidorcasa %1$s você deve revisar e aceitar os termos e condições. Revisar agora Desativar Conta @@ -690,7 +665,6 @@ Convites, remoções e bans são desafetados. Mostrar eventos de conta Inclui mudanças de avatar e nome de exibição. - Restrições de background estão desabilitadas para ${app_name}. Este teste devia ser rodado usando dados móveis (sem Wi-Fi). \n%1$s Restrições de background estão habilitadas para ${app_name}. @@ -757,7 +731,6 @@ Copiar Sucesso Notificações - Chamada ${app_name} Falhou Falha para estabelecer conexão em tempo real. \nPor favor peça ao/à administrador(a) de seu servidorcasa para configurar um servidor TURN a fim que chamadas funcionem confiavelmente. @@ -805,7 +778,6 @@ \nIsto vai impactar uso de rádio e bateria, vai ter uma notificação permanente exibida declarando que ${app_name} está à escuta por eventos. Sem sinc em background Você não vai ser notificada(o) sobre mensagens entrantes quando o app está em background. - Integrações Use um gerenciador de integrações para gerenciar bots, bridges, widgets e pacotes de stickers. \nGerenciadores de integrações recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome. @@ -921,7 +893,6 @@ Salvar Chave de Recuperação Compartilhar Salvar como Arquivo - A chave de recuperação tem sido salva. Um backup já existe em seu servidorcasa Parece que você já tem configurado backup de chave de uma outra sessão. Você quer substituí-lo pelo que você está criando\? @@ -975,7 +946,6 @@ Checando estado de backup Deletar Backup Deletar suas chaves de encriptação, das quais foi feito backup, do servidor\? Você não vai ser mais capaz de usar sua chave de recuperação para ler histórico de mensagens encriptadas. - Backup Seguro Salvaguardar-se contra perda de acesso a mensagens & dados encriptados Nunca perca mensagens encriptadas @@ -991,11 +961,8 @@ Versão Algoritmo - Verificada(o)! Entendido - - Requisição de Verificação %s quer verificar sua sessão Erro Desconhecido @@ -1159,7 +1126,6 @@ Este conteúdo foi reportado como inapropriado. \n \nSe você não quer ver mais nada de conteúdo desta(e) usuária(o), você pode ignorá-la(o) para esconder mensagens dela(e). - Ignorar usuária(o) Todas as mensagens (barulhento) Todas as mensagens @@ -1363,7 +1329,6 @@ Verificar %s Verificou %s Esperando por %s… - Mensagens nesta sala não são encriptadas ponta-a-ponta. Mensagens nesta sala são encriptadas ponta-a-ponta. \n @@ -1521,7 +1486,6 @@ Mensagem… Usar Arquivo Checando Chave de backup - Se você cancelar agora, você pode perder mensagens & dados encriptados se você perder acesso a seus logins. \n \nVocê também pode configurar Backup Seguro & gerenciar suas chaves em Configurações. @@ -1823,7 +1787,6 @@ Você poderia habilitar isto se a sala vai somente ser usada para colaborar com times internos em seu servidorcasa. Isto não poder ser mudado mais tarde. Bloquear qualquer pessoa que não é parte de %s de jamais se juntar a esta sala %1$d de %2$d - Dar consentimento Revogar meu consentimento Você tem dado seu consentimento para enviar emails e números de telefone para este servidor de identidade para descobrir outras(os) usuárias(os) de seus contatos. @@ -1913,8 +1876,6 @@ Transferir Conectar Consultar primeiro - - Chamada ativa (%1$s) Houve um erro ao procurar o número de telefone Pad de disco @@ -2112,7 +2073,6 @@ Enviar vídeo com o tamanho original Enviar vídeos com o tamanho original - Desculpe, um erro ocorreu enquanto tentando se juntar: %s Endereço de espaço Ver e gerenciar endereços deste espaço. @@ -2313,7 +2273,6 @@ Sondar pergunta ou tópico Criar Sondagem Sondagem - Enviar emails e números de telefone para %s Seus contatos são privados. Para descobrir usuárias(os) de seus contatos, você precisa de permissão para enviar info de contato a seu servidor de identidade. O signout desta sessão tem sido feito! @@ -2458,4 +2417,6 @@ %d mudança de ACLs de servidor %d mudanças de ACLs de servidor + %1$s, %2$s e outras(os) + %1$s e %2$s \ No newline at end of file From 8597d1144239ea11ed55fb3f8bb40f02274e8b20 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 25 Feb 2022 19:15:18 +0000 Subject: [PATCH 022/405] Translated using Weblate (Slovak) Currently translated at 98.8% (2132 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- vector/src/main/res/values-sk/strings.xml | 43 ++--------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index d9d9c7b1cd..18af56a598 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -39,7 +39,6 @@ Pozvanie do miestnosti %1$s a %2$s Prázdna miestnosť - %s aktualizoval/a túto miestnosť. Úvodná synchronizácia: \nPrebieha import účtu… @@ -197,7 +196,6 @@ Vymazať Premenovať Nahlásiť obsah - alebo Pozvať Odhlásiť sa @@ -220,7 +218,6 @@ Len Matrix kontakty Žiadne výsledky Miestnosti - Odoslať záznamy Odoslať záznamy o zlyhaní Odoslať snímku obrazovky @@ -248,11 +245,9 @@ Zdá sa, že toto nie je platná emailová adresa Táto emailová adresa sa už používa. Zabudli ste heslo? - Tento domovský server by sa rád uistil, že nieste robot Musíte zadať emailovú adresu prepojenú s vašim účtom. Nepodarilo sa overiť emailovú adresu: Uistite sa, že ste správne klikli na odkaz v emailovej správe - Zadajte platnú adresu URL Chybné údaje vo formáte JSON Neplatné údaje vo formáte JSON @@ -269,14 +264,10 @@ Prebiehajúci hovor… Vzdialenej strane sa nepodarilo prijať hovor. Informácia - - Aby ste mohli uskutočňovať audio hovory, ${app_name} potrebuje prístup k mikrofónu vašeho zariadenia. - ${app_name} potrebuje povolenie na prístup k vašej kamere a mikrofónu na uskutočňovanie videohovorov. \n \nPovoľte prístup v ďalších vyskakovacích oknách, aby ste mohli uskutočniť hovor. - ÁNO NIE Pokračovať @@ -284,7 +275,6 @@ Vstúpiť Odmietnuť Preskočiť na neprečítanú správu - Opustiť miestnosť Ste si istí, že chcete opustiť miestnosť? PRIAME KONVERZÁCIE @@ -311,7 +301,6 @@ Certifikát sa zmenil na iný, ktorému tento telefón dôveroval. Toto je VEĽMI NEZVYČAJNÉ. Odporúča sa, aby ste tento nový certifikát NEPRIJALI. Certifikát sa zmenil z predtým dôveryhodného na nedôveryhodný. Server mohol obnoviť svoj certifikát. Obráťte sa na správcu servera, aby vám poskytol očakávaný odtlačok. Dôverujte certifikátu len v prípade, že správca servera zverejnil odtlačok zhodný s odtlačkom zobrazeným vyššie. - Hľadať Filtrovať členov v miestnosti Žiadne výsledky @@ -364,7 +353,6 @@ Aktualizovať verejné meno Naposledy videné %1$s @ %2$s - Overenie Prihlásený ako Domovský server @@ -402,7 +390,6 @@ Tieto funkcie sú experimentálne a môžu sa nečakane pokaziť. Pri používaní buďte opatrní. Nastaviť ako hlavnú adresu Zrušiť nastavenie ako hlavnej adresy - Vzhľad Chyba dešifrovania Verejné meno @@ -414,7 +401,6 @@ Exportovať Zadajte prístupovú frázu Potvrďte prístupovú frázu - Importovať šifrovacie kľúče miestnosti Importovať kľúče miestnosti Importovať kľúče z lokálneho súboru @@ -426,7 +412,6 @@ Overiť Ak chcete overiť, či táto relácia je skutočne dôveryhodná, kontaktujte jeho vlastníka iným spôsobom (napr. osobne alebo cez telefón) a opýtajte sa ho, či kľúč, ktorý má zobrazený v Nastaveniach, sa zhoduje s kľúčom zobrazeným nižšie: Ak sa kľúče zhodujú, stlačte tlačidlo Overiť nižšie. Ak sa nezhodujú, niekto ďalší odpočúva toto zariadenie a mali by ste ho pridať na čiernu listinu. - Vyberte adresár miestností Názov servera Všetky miestnosti na serveri %s @@ -488,7 +473,6 @@ %d zmien členstva Zobraziť členov - 1 člen %d členovia @@ -499,14 +483,11 @@ %d nové správy %d nových správ - - 1 neprečítaná správa %d neprečítané správy %d neprečítaných správ - 1 miestnosť %d miestnosti @@ -546,10 +527,6 @@ Nebola nájdená žiadna vhodná aplikácia na dokončenie tejto akcie. Prosím, vložte svoje heslo. Ak je to možné, prosím popis napíšte v angličtine. - - - - 1 vybratý %d vybratí @@ -569,8 +546,6 @@ Mení vaše zobrazované meno / prezývku Zapnutie/vypnutie formátovanie textu markdown Užitočné na opravu spravovania Matrix aplikácií - - Táto miestnosť bola nahradená inou a nie je viac aktívna. Konverzácia pokračuje tu Táto miestnosť je pokračovaním predchádzajúcej konverzácii @@ -662,7 +637,6 @@ Pozvania, odstránenia a zákazy nie sú ovplyvnené. Zobrazovať udalosti účtu Zahŕňa zmeny zobrazovaného mena a obrázka v profile. - Heslo Spustiť predvolený fotoaparát v systéme namiesto zobrazenia vlastnej vstavanej obrazovky. Príkaz \"%s\" vyžaduje viac argumentov, alebo nie sú všetky zadané správne. @@ -735,7 +709,6 @@ Zrušiť Odpojiť sa Odmietnuť - Toto nie je platná adresa Matrix serveru Domovský server je nedostupný na tejto URL adrese, preverte to prosím Relácie @@ -799,7 +772,6 @@ \nBude to mať vplyv na používanie rádia a batérie, bude sa zobrazovať trvalé oznámenie, že ${app_name} počúva udalosti. Žiadna synchronizácia na pozadí Nebudete dostávať oznámenia o prichádzajúcich správach, keď aplikácia pracuje na pozadí. - Integrácie Použite správcu integrácií na nastavenie botov, premostení, widgetov a balíčkov s nálepkami. \nSprávcovia integrácie dostávajú konfiguračné údaje a môžu vo vašom mene upravovať widgety, posielať pozvánky do miestnosti a nastavovať úrovne oprávnení. @@ -897,7 +869,6 @@ Uložiť kľúč obnovenia Zdieľať Uložiť ako súbor - Kľúč obnovenia bol uložený. Záloha už existuje na vašom domovskom serveri Zdá sa, že ste si už zálohovanie kľúčov nastavili z inej relácie. Chcete ho nahradiť zálohou, ktorú vytvárate teraz\? @@ -953,7 +924,6 @@ Zisťovanie stavu zálohovania Vymazať zálohu Vymazať vaše zálohované šifrovacie kľúče z domovského servera\? Na čítanie histórie zašifrovaných správ už nebudete môcť použiť kľúč na obnovenie. - Bezpečné zálohovanie Zabezpečte sa proti strate šifrovaných správ a údajov Nikdy neprídete o šifrované správy @@ -971,11 +941,8 @@ Verzia Algoritmus Podpis - Overené! Rozumiem - - Žiadosť o overenie %s chce overiť vašu reláciu Neznáma chyba @@ -1381,7 +1348,6 @@ Nastavenia miestnosti Verzia miestnosti Nová hodnota - Číselník Zavolať späť Vyčistiť históriu @@ -1680,7 +1646,6 @@ Otvoriť ponuku vytvorenia miestnosti Zdá sa, že serveru trvá príliš dlho, kým odpovie, čo môže byť spôsobené buď zlým pripojením, alebo chybou servera. Skúste to o chvíľu znova. Súhlasíte so zaslaním týchto informácií\? - Preskočiť na potvrdenie o prečítaní Vlastné (%1$d) v %2$s Predvolené v %1$s @@ -1783,7 +1748,6 @@ Žiadna odpoveď Podržali ste hovor Automaticky aktualizovať nadradený priestor - Niektoré miestnosti môžu byť skryté, pretože sú súkromné a potrebujete pozvánku. Tento priestor nemá žiadne miestnosti Pre viac informácií sa obráťte na správcu domovského servera @@ -1906,7 +1870,6 @@ Zmienky a kľúčové slová Iba zmienky a kľúčové slová Medzinárodné telefónne čísla musia začínať znakom \"+\" - Ak chcete zistiť existujúce kontakty, potrebujete odoslať kontaktné informácie (e-maily a telefónne čísla) na server totožností. Pred odoslaním vaše údaje zahašujeme kvôli ochrane osobných údajov. Odoslať e-maily a telefónne čísla na %s Dali ste súhlas na odosielanie e-mailov a telefónnych čísel na tento server totožností na objavenie ďalších používateľov z vašich kontaktov. @@ -1965,7 +1928,6 @@ \n \nZastaviť proces zmeny hesla\? Zabudli ste alebo ste stratili všetky možnosti obnovy\? Obnovte všetko - Použite prístupovú frázu na obnovenie alebo kľúč Použite najnovšiu aplikáciu ${app_name} na svojich ostatných zariadeniach: Použite najnovšiu aplikáciu ${app_name} na svojich ostatných zariadeniach, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} pre Android alebo iného klienta Matrix podporujúceho krížové podpisovanie @@ -1994,7 +1956,6 @@ %1$d aktívne hovory · %1$d aktívnych hovorov · - Prebiehajúci hovor (%1$s) Pri vyhľadávaní telefónneho čísla došlo k chybe Žiadna odpoveď @@ -2164,7 +2125,6 @@ Táto relácia nemôže zdieľať toto overenie s vašimi ostatnými reláciami. \nOverenie bude uložené lokálne a zdieľané v budúcej verzii aplikácie. Akcie správcu - Zobrazujú sa len prvé výsledky, zadajte ďalšie písmená… Zatraste telefónom a otestujte prah detekcie Prahová hodnota detekcie @@ -2184,7 +2144,6 @@ Zadajte kľúčové slová pre vyhľadanie reakcie. Odobrať z nízkej priority Pridať k nízkej priorite - Na pokračovanie použite %1$s alebo %2$s. Ak chcete pokračovať, zadajte prístupovú frázu pre zálohovanie kľúčov. Generovanie kľúča SSSS z prístupovej frázy %s @@ -2476,4 +2435,6 @@ %d zmeny ACL servera %d zmien ACL servera + %1$s, %2$s a ďalší + %1$s a %2$s \ No newline at end of file From 8c6b15a1ede171c2438f37b56f0d674914e07454 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Fri, 25 Feb 2022 20:26:59 +0000 Subject: [PATCH 023/405] Translated using Weblate (Swedish) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/ --- vector/src/main/res/values-sv/strings.xml | 57 ++++++----------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 38f7a60928..69c25ebba3 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -88,7 +88,6 @@ Telefonnummer Rumsinbjudan %1$s och %2$s - Tomt rum Inledande synk: \nImporterar konto… @@ -264,7 +263,6 @@ Återkalla Koppla ifrån Rapportera innehåll - eller Bjud in Godkänn @@ -296,7 +294,6 @@ Bara Matrix-kontakter Inga resultat Rum - Gemenskaper Skicka loggar Skicka kraschloggar @@ -322,7 +319,6 @@ Skicka röst Är du säker på att du vill starta ett röstsamtal\? Är du säker på att du vill skapa ett videosamtal\? - Skicka filer Skicka dekal Ta foto eller video @@ -339,11 +335,9 @@ Det här ser inte ut som en giltig e-postadress Den här e-postadressen är redan definierad. Glömt lösenordet\? - Denna hemserver skulle vilja verifiera att du inte är en robot Du måste skriva in e-postadressen länkad till ditt konto. Misslyckades att verifiera e-postadressen: se till att du klickade på länken i e-brevet - Vänligen granska och acceptera villkoren för denna hemserver: Vänligen skriv in en giltig URL Det här är inte en giltig Matrixserveradress @@ -374,14 +368,10 @@ Videosamtal pågår… Den andra parten svarade inte. Information - - ${app_name} behöver tillstånd att komma åt din mikrofon för hålla röstsamtal. - ${app_name} behöver tillstånd att komma åt din kamera och mikrofon för att kunna utföra videosamtal. \n \nVänligen ge tillstånd i nästa popup för att kunna utföra samtalet. - JA NEJ Fortsätt @@ -390,16 +380,10 @@ Avslå Lista medlemmar Hoppa till oläst - - %d medlem %d medlemmar - - - - Lämna rum Är du säker på att du vill lämna rummet\? DIREKTCHATT @@ -409,8 +393,6 @@ Kicka Ignorera Filtrera rumsmedlemmar - - Fäst rum med missade aviseringar Fäst rum med olästa meddelanden Alla rum på %s-servern @@ -420,7 +402,6 @@ %d rum Rum - Detta kommer att göra ditt konto permanent oanvändbart. Du kommer inte kunna logga in, och ingen kommer kunna registrera sig med samma användar-ID. Detta kommer att få ditt konto att lämna alla rum det är med i, och det kommer att ta bort din kontoinformation från din identitetsserver. Den här handlingen går inte att ångra. \n \nAtt inaktivera ditt konto får oss normalt inte att glömma meddelanden du har skickat. Om du skulle vilja att vi glömmer dina meddelanden, markera rutan nedan. @@ -456,7 +437,6 @@ Meddelanden som innehåller mitt visningsnamn Användarinställningar Innehåller byten av avatar eller visningsnamn. - Lösenord Byt lösenord Nuvarande lösenord @@ -473,7 +453,6 @@ Din återställningsnyckel är ett skyddsnät - du kan använda den för att återfå åtkomst till dina krypterade meddelanden om du skulle glömma din lösenfras. \nLagra din återställningsnyckel på något säkert ställe, t.ex. en lösenordshanterare (eller ett kassaskåp) Lagra din återställningsnyckel på något säkert ställe, t.ex. en lösenordshanterare (eller ett kassaskåp) - Byt nätverk Alla gemenskaper Allmänt @@ -483,7 +462,6 @@ Meddelanden i det här rummet är totalsträckskrypterade. Lär dig mer och verifiera användare i deras profiler. Avignorera Kunde inte verifiera den externa serverns identitet. - Alla meddelanden Lägg till e-postadress Lägg till telefonnummer @@ -541,7 +519,6 @@ Inaktivera mitt konto Upptäckbarhet Hantera dina upptäckbarhetsinställningar. - Inloggad som Hemserver Identitetsserver @@ -714,7 +691,6 @@ Använd den här sessionen för att verifiera din nya och ge den tillgång till krypterade meddelanden. Om du avbryter så kommer du inte kunna läsa krypterade meddelanden på den här enheten, och andra användare kommer inte att lita på den Om du avbryter så kommer du inte kunna läsa krypterade meddelanden på din nya enhet, och andra användare kommer inte att lita på den - När jag bjuds in till ett rum Samtalsinbjudningar Meddelanden skickade av en bott @@ -755,15 +731,12 @@ Integrationer är avstängda Aktivera \'Tillåt integrationer\' I inställningarna för att göra detta. Exportera nycklarna till en lokal fil - Importera nycklarna från en lokal fil - Välj en rumskatalog %d oläst aviserat meddelande %d olästa aviserade meddelanden - %1$s: %2$d meddelande %1$s: %2$d meddelanden @@ -783,7 +756,6 @@ Krypterat meddelande kontakta din tjänstadministratör Spara som fil - Radera dina säkerhetskopierade krypteringsnycklar från servern\? Du kommer inte längre kunna använda din återställningsnyckel för att läsa krypterad meddelandehistorik. Skydda dig mot att tappa åtkomst till krypterade meddelanden och data Nya säkra meddelandenycklar @@ -1013,7 +985,6 @@ Sätt upp säker säkerhetskopiering Alla nycklar säkerhetskopierade Algoritm - %s vill verifiera din session Visa borttagna meddelanden Visa en platshållare för borttagna meddelanden @@ -1029,7 +1000,6 @@ %1$s, %2$s och %3$d annan har läst %1$s, %2$s och %3$d andra har läst - Du ignorerar inga användare Annan Om du har skapat ett konto på en hemserver så kan du använda ditt Matrix-ID (t.ex. @användare:domän.com) och lösenord nedan. @@ -1124,7 +1094,6 @@ Du kommer inte att bli aviserad om inkommande meddelanden när appen är i bakgrunden. Starta vid boot Timeout för synkbegäran - Fördröjning mellan varje synkronisering Lokala kontakter Kontaktbehörighet @@ -1172,7 +1141,6 @@ Rummets interna ID Sätt som huvudadress Avsätt som huvudadress - Avkrypteringsfel Publikt namn Nycklar framgångsrikt importerade @@ -1253,7 +1221,6 @@ Skapa Hem Bjöd in - Du har blivit utsparkad från %1$s av %2$s Du har blivit bannad från %1$s av %2$s Orsak: %1$s @@ -1319,10 +1286,8 @@ Säkerhetskopierar %d nycklar… Signatur - Verifierad! Jag förstår - Verifieringsbegäran Okänt fel Det verkar som att du försöker ansluta till en annan hemserver. Vill du logga ut\? @@ -1525,7 +1490,6 @@ Verifiera %s Verifierade %s Väntar på %s… - Säkerhet Adminhandlingar Lämnar rummet… @@ -1830,7 +1794,6 @@ Dölj avancerat Visa avancerat %1$d av %2$d - Ge samtycke Återkalla mitt samtycke Du har gett samtycke att skicka e-postadresser och telefonnummer till den här identitetsservern för att upptäcka andra användare baserat på dina kontakter. @@ -1914,8 +1877,6 @@ Flytta Anslut Rådfråga först - - Aktivt samtal (%1$s) Ett fel inträffade när telefonnumret slogs upp Knappsats @@ -2003,7 +1964,7 @@ Jag och mina teamkamrater Att privat utrymme för att organisera dina rum Bara jag - Det till att rätt personer har åtkomst till %s. Du kan ändra detta senare. + Det till att rätt personer har åtkomst till %s. Vem jobbar du med\? För att gå med i ett existerande utrymme så behöver du en inbjudan. Detta kan ändras senare @@ -2112,7 +2073,6 @@ Ange namnet för en ny server du vill utforska. Lägg till en ny server Din server - För att utföra detta, vänligen ge kameraåtkomst från systeminställningarna. Vissa behörigheter saknas för att utföra detta, vänligen ge behörighet från systeminställningarna. Observera att uppgradering kommer att göra en ny version av rummet. Alla nuvarande meddelanden kommer att vara kvar i det här arkiverade rummet. @@ -2313,7 +2273,6 @@ Omröstningens fråga eller ämne Skapa omröstning Omröstning - Skicka e-postadresser och telefonnummer till %s Dina kontakter är privata. För att upptäcka användare från dina kontakter så behöver vi ditt tillstånd att skicka kontaktinfo till din identitetsserver. Sessionen har loggats ut! @@ -2446,4 +2405,18 @@ Kopiera länk till tråd Visa i rum Visa trådar + Rumsaviseringar + Användare + Avisera hela rummet + + %1$d till + %1$d till + + Visa mindre + %1$s, %2$s och fler + %1$s och %2$s + + %d server-ACL-ändring + %d server-ACL-ändringar + \ No newline at end of file From a4f04b704fbae4796c1592c678a9b3f19bae88c8 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 25 Feb 2022 22:42:34 +0000 Subject: [PATCH 024/405] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- vector/src/main/res/values-uk/strings.xml | 35 ++--------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index 066a40fdf4..2f34fc66fb 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -39,7 +39,6 @@ Помилка Matrix Адреса електронної пошти Номер телефону - %s оновлює цю кімнату. Початкове налаштування: \nІмпортування даних облікового запису @@ -164,7 +163,6 @@ Видалити Перейменувати Поскаржитись на вміст - або Запрошення Вийти з облікового запису @@ -187,7 +185,6 @@ Лише Matrix-контакти Немає результатів Кімнати - Надіслати журнали Надіслати журнали помилок Надіслати знімок екрана @@ -235,7 +232,6 @@ Інформація Для здійснення аудіодзвінків потрібен доступ до мікрофону. Для здійснення відеодзвінків потрібен доступ до камери та мікрофону.\n\nБудь ласка, надайте його у наступних виринаючих вікнах, щоб мати змогу їх здійснити. - ТАК НІ Продовжити @@ -269,7 +265,6 @@ Сертифікат почав відрізнятися від того, якому довіряв ваш телефон. Це ДУЖЕ НЕЗВИЧНО. Наполегливо радимо НЕ ПРИЙМАТИ цей новий сертифікат. Сертифікат змінився з довіреного на недовірений. Сервер міг оновити свій сертифікат. Зв\'яжіться з адміністратором сервера, щоб отримати дійсний відбиток. Приймайте сертифікат лише у випадку збігу відбитку вище з відбитком, оприлюдненим адміністратором сервера. - Пошук Фільтр переліку користувачів Тут порожньо @@ -370,7 +365,6 @@ Експорт Введіть парольну фразу Підтвердіть парольну фразу - Імпортувати E2E ключі кімнати Імпортувати ключі кімнати Імпортувати ключі з локального файлу @@ -382,7 +376,6 @@ Звірити Підтвердьте, порівнявши вказане за допомогою налаштувань користувача в іншому сеансі: Якщо вони відрізняються, безпека вашого зв\'язку може бути під загрозою. - Вибір каталогу кімнат Ім\'я сервера Всі кімнати на сервері %s @@ -439,7 +432,6 @@ У вас поки що не має наліпок. \n \nДодати зараз\? - %d учасник %d учасники @@ -466,18 +458,12 @@ Помилка Системні сповіщення Якщо можливо, будь ласка, напишіть опис англійською. - - - - %d вибрано %d вибрано %d вибрано %d вибрано - - Попередній перегляд посилань Попередній перегляд медіа перед надсиланням ${app_name} збирає анонімну аналітику, щоб ми могли вдосконалювати цей додаток. @@ -490,7 +476,6 @@ %d непрочитаних сповіщень - %d кімната %d кімнати @@ -522,8 +507,6 @@ Домівка Кімнати Запрошено - - %2$s вилучає вас із %1$s %2$s блокує вас у %1$s Причина: %1$s @@ -753,7 +736,6 @@ Не вдалося встановити зв’язок у режимі реального часу. \nПопросіть адміністратора вашого домашнього сервера налаштувати сервер TURN для надійної роботи викликів. ${app_name} не вдалося здійснити виклик - Більше немає результатів Відкликати публікування Додати @@ -784,7 +766,6 @@ Схоже у вас вже є резервна копія ключа налаштування з іншого сеансу. Хочете замінити його тим, який ви створюєте\? Резервна копія вже існує на вашому homeserver Ключ відновлення збережено. - Зберегти як файл Поділитися Зберегти ключ відновлення @@ -932,7 +913,6 @@ Інтеграцію вимкнено Керування інтеграцією Дозволити інтеграції - Це замінить ваш поточний ключ або фразу. Створіть новий ключ безпеки або встановіть нову фразу безпеки для наявної резервної копії. Захистіться від втрати доступу до зашифрованих повідомлень і даних створенням резервної копії ключів шифрування на своєму сервері. @@ -950,7 +930,6 @@ %d секунд %d секунд - Ви не отримуватимете сповіщення про вхідні повідомлення, коли програма перебуває у фоновому режимі. Немає фонової синхронізації ${app_name} періодично синхронізуватиметься у фоновому режимі в певний час (налаштовується). @@ -1273,9 +1252,7 @@ Ви утримали виклик %s утримали виклик Утримати - Активний виклик (%1$s) - Змінити мережу Змінити Push-сповіщення вимкнено @@ -1372,7 +1349,6 @@ Зазначте адресу сервера ідентифікації Неможливо під\'єднатись до сервера ідентифікації Зазначте адресу сервера ідентифікації - Повторити Від\'єднання від вашого сервера ідентифікації означатиме, що ви не будете виявними для інших користувачів та не зможете запрошувати інших через електронну пошту або номер телефону. Ви наразі не використовуєте жодного сервера ідентифікації. Для того, щоб виявляти інших та бути виявним для знайомих вам наявних контактів, налаштуйте такий сервер нижче. @@ -1786,8 +1762,6 @@ %s хоче звірити ваш сеанс Запит перевірки Запит перевірки - - Зрозуміло Резервні копії всіх ключів створено Резервне копіювання ключів. Це може тривати кілька хвилин… @@ -1823,14 +1797,12 @@ Перегляд реакцій Тут буде показано ваші кімнати. Натисніть + унизу праворуч, щоб знайти наявні або створити власні. Схоже, ви намагаєтесь під\'єднатися до іншого домашнього сервера. Бажаєте вийти\? - Резервне копіювання %d ключа… Резервне копіювання %d ключів… Резервне копіювання %d ключів… Резервне копіювання %d ключів… - Додати наявну кімнату до простору Створити простір Лише я @@ -1844,7 +1816,6 @@ Сталася помилка пошуку номера телефона Ви відхилили цей виклик Ваша книга контактів порожня - Закрити нагадування про резервне копіювання ключів Схоже, що відповідь сервера надто тривала, це може бути спричинено або поганим з’єднанням, або помилкою сервера. Повторіть спробу через деякий час. Повторіть спробу, коли погодитесь з умовами свого домашнього сервера. @@ -2285,7 +2256,6 @@ \n \nБажаєте зайти через вебклієнт\? Щоб знайти наявні контакти, надішліть дані контактів (е-пошти й номери телефонів) серверу ідентифікації. Ми хешуємо ваші дані перед надсиланням для приватності. - Ваші контакти приватні. Щоб дізнаватись про користувачів, відповідних вашим контактам, дозвольте нам надсилати дані ваших контактів серверу ідентифікації. Надіслати електронні адреси та номери телефонів %s Сеанс завершено! @@ -2337,7 +2307,6 @@ Кімната — версії %s, яку домашній сервер позначив нестабільною. Поліпшення кімнати — серйозна операція. Її зазвичай радять, коли кімната нестабільна через вади, брак функціоналу чи вразливості безпеки. \nЗазвичай це впливає лише на деталі опрацювання кімнати сервером. - Деяких кімнат може бути не видно, бо вони закриті й потребують запрошення. Деяких кімнат може бути не видно, бо вони закриті й потребують запрошення. \nУ вас нема дозволу додавати кімнати. @@ -2382,14 +2351,12 @@ Якщо скасуєте це й загубите пристрій, то втратите зашифровані повідомлення й дані. \n \nВвімкнути захищене резервне копіювання й керувати своїми ключами можна в налаштуваннях. - Скасування залишить %1$s (%2$s) без звірки. У їхньому користувацькому профілі можна почати заново. Звірте цим сеансом свій новий. Це надасть йому доступ до зашифрованих повідомлень. Надіслані цьому сеансу й цим сеансом повідомлення позначатимуться застереженнями, поки цей користувач йому не довірить. Або ви можете власноруч звірити сеанс. Якщо ви увімкнете шифрування для кімнати, його неможливо буде вимкнути. Надіслані у зашифровану кімнату повідомлення будуть прочитними тільки для учасників кімнати, натомість для сервера вони будуть непрочитними. Увімкнення шифрування може унеможливити роботу ботів та мостів. Не вдалося поширити звірку цього сеансу з вашими іншими. \nЗвірка збережеться локально, її поширить майбутня версія застосунку. - Можете ввімкнути це, якщо в кімнаті співпрацюватимуть лише внутрішні команди на вашому домашньому сервері. Цього більше не можна буде змінити. Цей сеанс — користувача %1$s, а ви надаєте облікові дані користувача %2$s. Це не підтримується в ${app_name}. \nБудь ласка, спершу очистіть дані, а тоді ввійдіть в інший обліковий запис. @@ -2542,4 +2509,6 @@ І ще %1$d Згорнути + %1$s, %2$s та інші + %1$s і %2$s \ No newline at end of file From 1f6275762ecf8507a7413acbf9f37cddb7d1c1f0 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Fri, 25 Feb 2022 20:29:56 +0000 Subject: [PATCH 025/405] Translated using Weblate (Swedish) Currently translated at 100.0% (51 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sv/ --- fastlane/metadata/android/sv-SE/changelogs/40104000.txt | 2 ++ fastlane/metadata/android/sv-SE/changelogs/40104020.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40104000.txt create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40104020.txt diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104000.txt b/fastlane/metadata/android/sv-SE/changelogs/40104000.txt new file mode 100644 index 0000000000..6bce52ba36 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Initial implementation av trådmeddelanden. Meddelandebubblor. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104020.txt b/fastlane/metadata/android/sv-SE/changelogs/40104020.txt new file mode 100644 index 0000000000..e3b5d4cd1c --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: lägg till stöd för @room och slutna omröstningar, och många andra små ändringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.2 From 329ce7736ca0d3572ff9f0a64929e5cf57511236 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sat, 26 Feb 2022 08:39:52 +0000 Subject: [PATCH 026/405] Translated using Weblate (Japanese) Currently translated at 54.9% (28 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/ja/ --- fastlane/metadata/android/ja-JP/changelogs/40101040.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40101100.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40103070.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40103080.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40103100.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40103110.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40103120.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40104000.txt | 2 ++ fastlane/metadata/android/ja-JP/changelogs/40104020.txt | 2 ++ 9 files changed, 18 insertions(+) create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40101040.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40101100.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103070.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103080.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103100.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103110.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103120.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40104000.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40104020.txt diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101040.txt b/fastlane/metadata/android/ja-JP/changelogs/40101040.txt new file mode 100644 index 0000000000..2dc1cdb781 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101040.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:パフォーマンスの向上と不具合の修正 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101100.txt b/fastlane/metadata/android/ja-JP/changelogs/40101100.txt new file mode 100644 index 0000000000..2f720498ec --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101100.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:テーマ、スタイルの更新と、スペースに関する新機能。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103070.txt b/fastlane/metadata/android/ja-JP/changelogs/40103070.txt new file mode 100644 index 0000000000..09c44e990d --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103070.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:主に通知に関する不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103080.txt b/fastlane/metadata/android/ja-JP/changelogs/40103080.txt new file mode 100644 index 0000000000..7c37f5a756 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103080.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103100.txt b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt new file mode 100644 index 0000000000..76c28cdd90 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:投票機能のサポート(実験的)。URL プレビューの新規デザイン。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103110.txt b/fastlane/metadata/android/ja-JP/changelogs/40103110.txt new file mode 100644 index 0000000000..5295af5833 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103110.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103120.txt b/fastlane/metadata/android/ja-JP/changelogs/40103120.txt new file mode 100644 index 0000000000..3859bee8d5 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103120.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:不具合の修正 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104000.txt b/fastlane/metadata/android/ja-JP/changelogs/40104000.txt new file mode 100644 index 0000000000..22a205dc37 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104000.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:スレッド機能の実装、吹き出しメッセージ。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104020.txt b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt new file mode 100644 index 0000000000..e792008faf --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:@roomの対応、非公開の投票など。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.2 From eda723c23082382b9f013493b8223195036a2d7d Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 28 Feb 2022 12:35:27 +0200 Subject: [PATCH 027/405] Remove fetching thread summaries when homeserver do not support MSC3440 --- .../session/homeserver/GetCapabilitiesResult.kt | 3 ++- .../room/threads/list/viewmodel/ThreadListViewModel.kt | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index 55526b41db..3a016bc3e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -70,7 +70,8 @@ internal data class Capabilities( * Capability to indicate if the server supports MSC3440 Threading * True if the user can use m.thread relation, false otherwise */ - @Json(name = "m.thread") +// @Json(name = "m.thread") + @Json(name = "io.element.thread") val threads: BooleanCapability? = null ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 290b71a504..d68e0a3248 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -54,8 +54,7 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState } init { - observeThreads() - fetchThreadList() + fetchAndObserveThreads() } override fun handle(action: EmptyAction) {} @@ -64,9 +63,12 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState * Observing thread list with respect to homeserver * capabilities */ - private fun observeThreads() { + private fun fetchAndObserveThreads() { when (session.getHomeServerCapabilities().canUseThreading) { - true -> observeThreadSummaries() + true -> { + fetchThreadList() + observeThreadSummaries() + } false -> observeThreadsList() } } From e59f2bba0a05b6962c8ebc40261af993331a2283 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 28 Feb 2022 17:13:06 +0200 Subject: [PATCH 028/405] Add analytics to threads --- .../session/room/timeline/TimelineEvent.kt | 8 ++ .../analytics/extensions/ComposerExt.kt | 28 ++++++ .../analytics/extensions/InteractionExt.kt | 24 +++++ .../app/features/analytics/plan/Composer.kt | 5 + .../features/analytics/plan/Interaction.kt | 95 +++++++++++++++++-- .../features/analytics/plan/MobileScreen.kt | 5 + .../home/room/detail/TimelineFragment.kt | 13 +-- .../composer/MessageComposerViewModel.kt | 4 + .../composer/MessageComposerViewState.kt | 3 + .../timeline/action/EventSharedAction.kt | 2 +- .../action/MessageActionsViewModel.kt | 3 +- .../home/room/threads/ThreadsActivity.kt | 3 + .../threads/arguments/ThreadTimelineArgs.kt | 3 +- .../list/viewmodel/ThreadListViewModel.kt | 7 +- .../threads/list/views/ThreadListFragment.kt | 2 + 15 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index c03d0fd17b..d7796c8808 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room.timeline import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.orFalse 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.RelationType @@ -159,6 +160,13 @@ fun TimelineEvent.isSticker(): Boolean { return root.isSticker() } +/** + * Returns whether or not the event is a root thread event + */ +fun TimelineEvent.isRootThread(): Boolean { + return root.threadDetails?.isRootThread.orFalse() +} + /** * Get the latest message body, after a possible edition, stripping the reply prefix if necessary */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt new file mode 100644 index 0000000000..80675ac57c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.extensions + +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.home.room.detail.composer.MessageComposerViewState +import im.vector.app.features.home.room.detail.composer.SendMode + +fun MessageComposerViewState.toAnalyticsComposer(): Composer = + Composer( + inThread = isInThreadTimeline(), + isEditing = sendMode is SendMode.Edit, + isReply = sendMode is SendMode.Reply, + startsThread = startsThread) diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt new file mode 100644 index 0000000000..c46230cdd1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.extensions + +import im.vector.app.features.analytics.plan.Interaction + +fun Interaction.Name.toAnalyticsInteraction(interactionType: Interaction.InteractionType = Interaction.InteractionType.Touch) = + Interaction( + name = this, + interactionType = interactionType) diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Composer.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Composer.kt index a3b847a1bd..79be8aae2b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Composer.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Composer.kt @@ -39,6 +39,10 @@ data class Composer( * sent event. */ val isReply: Boolean, + /** + * Whether this message begins a new thread or not. + */ + val startsThread: Boolean? = null, ) : VectorAnalyticsEvent { override fun getName() = "Composer" @@ -48,6 +52,7 @@ data class Composer( put("inThread", inThread) put("isEditing", isEditing) put("isReply", isReply) + startsThread?.let { put("startsThread", it) } }.takeIf { it.isNotEmpty() } } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt index 7bdc7740e1..2007f75fbc 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Interaction.kt @@ -40,6 +40,36 @@ data class Interaction( ) : VectorAnalyticsEvent { enum class Name { + /** + * User tapped on Add to Home button on Room Details screen. + */ + MobileRoomAddHome, + + /** + * User tapped on Leave Room button on Room Details screen. + */ + MobileRoomLeave, + + /** + * User tapped on Threads button on Room screen. + */ + MobileRoomThreadListButton, + + /** + * User tapped on a thread summary item on Room screen. + */ + MobileRoomThreadSummaryItem, + + /** + * User tapped on the filter button on ThreadList screen. + */ + MobileThreadListFilterItem, + + /** + * User selected a thread on ThreadList screen. + */ + MobileThreadListThreadItem, + /** * User tapped the already selected space from the space list. */ @@ -52,8 +82,8 @@ data class Interaction( SpacePanelSwitchSpace, /** - * User clicked the create room button in the + context menu of the room - * list header in Element Web/Desktop. + * User clicked the create room button in the add existing room to space + * dialog in Element Web/Desktop. */ WebAddExistingToSpaceDialogCreateRoomButton, @@ -105,12 +135,24 @@ data class Interaction( */ WebRightPanelRoomUserInfoInviteButton, + /** + * User clicked the threads 'show' filter dropdown in the threads panel + * in Element Web/Desktop. + */ + WebRightPanelThreadPanelFilterDropdown, + /** * User clicked the create room button in the room directory of Element * Web/Desktop. */ WebRoomDirectoryCreateRoomButton, + /** + * User clicked the Threads button in the top right of a room in Element + * Web/Desktop. + */ + WebRoomHeaderButtonsThreadsButton, + /** * User adjusted their favourites using the context menu on the header * of a room in Element Web/Desktop. @@ -153,6 +195,12 @@ data class Interaction( */ WebRoomListHeaderPlusMenuCreateRoomItem, + /** + * User clicked the explore rooms button in the + context menu of the + * room list header in Element Web/Desktop. + */ + WebRoomListHeaderPlusMenuExploreRoomsItem, + /** * User adjusted their favourites using the context menu on a room tile * in the room list in Element Web/Desktop. @@ -189,6 +237,12 @@ data class Interaction( */ WebRoomListRoomsSublistPlusMenuCreateRoomItem, + /** + * User clicked the explore rooms button in the + context menu of the + * rooms sublist in Element Web/Desktop. + */ + WebRoomListRoomsSublistPlusMenuExploreRoomsItem, + /** * User interacted with leave action in the general tab of the room * settings dialog in Element Web/Desktop. @@ -201,6 +255,12 @@ data class Interaction( */ WebRoomSettingsSecurityTabCreateNewRoomButton, + /** + * User clicked a thread summary in the timeline of a room in Element + * Web/Desktop. + */ + WebRoomTimelineThreadSummaryButton, + /** * User interacted with the theme radio selector in the Appearance tab * of Settings in Element Web/Desktop. @@ -214,17 +274,40 @@ data class Interaction( WebSettingsSidebarTabSpacesCheckbox, /** - * User clicked the create room button in the + context menu of the room - * list header in Element Web/Desktop. + * User clicked the explore rooms button in the context menu of a space + * in Element Web/Desktop. + */ + WebSpaceContextMenuExploreRoomsItem, + + /** + * User clicked the home button in the context menu of a space in + * Element Web/Desktop. + */ + WebSpaceContextMenuHomeItem, + + /** + * User clicked the new room button in the context menu of a space in + * Element Web/Desktop. */ WebSpaceContextMenuNewRoomItem, /** - * User clicked the create room button in the + context menu of the room - * list header in Element Web/Desktop. + * User clicked the new room button in the context menu on the space + * home in Element Web/Desktop. */ WebSpaceHomeCreateRoomButton, + /** + * User clicked the back button on a Thread view going back to the + * Threads Panel of Element Web/Desktop. + */ + WebThreadViewBackButton, + + /** + * User selected a thread in the Threads panel in Element Web/Desktop. + */ + WebThreadsPanelThreadItem, + /** * User clicked the theme toggle button in the user menu of Element * Web/Desktop. diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt index 758a0540bf..33976cb4cc 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt @@ -225,6 +225,11 @@ data class MobileScreen( */ SwitchDirectory, + /** + * Screen that displays list of threads for a room + */ + ThreadList, + /** * A screen that shows information about a room member. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 1a40018526..67fb595378 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -119,7 +119,8 @@ import im.vector.app.core.utils.startInstallFromSourceIntent import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.FragmentTimelineBinding -import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentsHelper @@ -1505,9 +1506,6 @@ class TimelineFragment @Inject constructor( return } if (text.isNotBlank()) { - withState(messageComposerViewModel) { state -> - analyticsTracker.capture(Composer(isThreadTimeLine(), isEditing = state.sendMode is SendMode.Edit, isReply = state.sendMode is SendMode.Reply)) - } // We collapse ASAP, if not there will be a slight annoying delay views.composerLayout.collapse(true) lockSendButton = true @@ -2204,7 +2202,7 @@ class TimelineFragment @Inject constructor( } is EventSharedAction.ReplyInThread -> { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { - navigateToThreadTimeline(action.eventId) + navigateToThreadTimeline(action.eventId, action.startsThread) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } @@ -2363,9 +2361,11 @@ class TimelineFragment @Inject constructor( * using the ThreadsActivity */ - private fun navigateToThreadTimeline(rootThreadEventId: String) { + private fun navigateToThreadTimeline(rootThreadEventId: String, startsThread: Boolean = false) { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( + startsThread = startsThread, roomId = timelineArgs.roomId, displayName = timelineViewModel.getRoomSummary()?.displayName, avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, @@ -2381,6 +2381,7 @@ class TimelineFragment @Inject constructor( */ private fun navigateToThreadList() { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( roomId = timelineArgs.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 6adf248af9..a07d01fed5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -27,6 +27,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsComposer import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.command.CommandParser @@ -188,6 +189,9 @@ class MessageComposerViewModel @AssistedInject constructor( private fun handleSendMessage(action: MessageComposerAction.SendMessage) { withState { state -> + analyticsTracker.capture(state.toAnalyticsComposer()).also { + setState { copy(startsThread = false) } + } when (state.sendMode) { is SendMode.Regular -> { when (val slashCommandResult = commandParser.parseSlashCommand( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index f90f3975c6..95553eb1cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MavericksState import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -62,6 +63,7 @@ data class MessageComposerViewState( val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonVisible: Boolean = false, val rootThreadEventId: String? = null, + val startsThread: Boolean = false, val sendMode: SendMode = SendMode.Regular("", false), val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle ) : MavericksState { @@ -80,6 +82,7 @@ data class MessageComposerViewState( constructor(args: TimelineArgs) : this( roomId = args.roomId, + startsThread = args.threadTimelineArgs?.startsThread.orFalse(), rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) fun isInThreadTimeline(): Boolean = rootThreadEventId != null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 048a4754f5..5f12c2f174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -48,7 +48,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Reply(val eventId: String) : EventSharedAction(R.string.reply, R.drawable.ic_reply) - data class ReplyInThread(val eventId: String) : + data class ReplyInThread(val eventId: String, val startsThread: Boolean) : EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) object ViewInRoom : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 745cb0c731..20cf0b46fd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -61,6 +61,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited import org.matrix.android.sdk.api.session.room.timeline.isPoll +import org.matrix.android.sdk.api.session.room.timeline.isRootThread import org.matrix.android.sdk.api.session.room.timeline.isSticker import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -328,7 +329,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.ReplyInThread(eventId)) + add(EventSharedAction.ReplyInThread(eventId, !timelineEvent.isRootThread())) } if (canViewInRoom(timelineEvent, messageContent, actionPermissions)) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index fc76535c4c..726138ed93 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -26,6 +26,8 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.arguments.TimelineArgs @@ -92,6 +94,7 @@ class ThreadsActivity : VectorBaseActivity() { * One usage of that is from the Threads Activity */ fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) { + analyticsTracker.capture(Interaction.Name.MobileThreadListThreadItem.toAnalyticsInteraction()) val commonOption: (FragmentTransaction) -> Unit = { it.setCustomAnimations( R.anim.animation_slide_in_right, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt index aadad3d97c..d3a80811ea 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt @@ -26,5 +26,6 @@ data class ThreadTimelineArgs( val displayName: String?, val avatarUrl: String?, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, - val rootThreadEventId: String? = null + val rootThreadEventId: String? = null, + val startsThread: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index d68e0a3248..8da9d83e10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -25,6 +25,9 @@ import dagger.assisted.AssistedInject import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -34,6 +37,7 @@ import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState, + private val analyticsTracker: AnalyticsTracker, private val session: Session) : VectorViewModel(initialState) { @@ -113,9 +117,10 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState } } - fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading + fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading fun applyFiltering(shouldFilterThreads: Boolean) { + analyticsTracker.capture(Interaction.Name.MobileThreadListFilterItem.toAnalyticsInteraction()) setState { copy(shouldFilterThreads = shouldFilterThreads) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 949778629b..d5659efa49 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentThreadListBinding +import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.threads.ThreadsActivity @@ -62,6 +63,7 @@ class ThreadListFragment @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = MobileScreen.ScreenName.ThreadList } override fun onOptionsItemSelected(item: MenuItem): Boolean { From f0f98ce019e87eb47532fdb869c9612298de0958 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 28 Feb 2022 17:44:53 +0200 Subject: [PATCH 029/405] Add changelog file --- changelog.d/5378.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5378.misc diff --git a/changelog.d/5378.misc b/changelog.d/5378.misc new file mode 100644 index 0000000000..1cf6da5e59 --- /dev/null +++ b/changelog.d/5378.misc @@ -0,0 +1 @@ +Add analytics support for threads \ No newline at end of file From 12ea262ebc3d9e960a0dd8ee97468bedd3629c0b Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 28 Feb 2022 20:45:22 +0000 Subject: [PATCH 030/405] Translated using Weblate (Japanese) Currently translated at 98.0% (2116 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 0d35cc8c99..2c17a90df8 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -2322,4 +2322,5 @@ \n検証は端末に保存され、新しいバージョンのアプリで共有されます。 %1$s、%2$s他 %1$sと%2$s + 自分と相手を認証してチャットを安全に保ちます \ No newline at end of file From 2673f6715a317fef69cc115119e288a3ce7530db Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 28 Feb 2022 20:44:45 +0000 Subject: [PATCH 031/405] Translated using Weblate (Japanese) Currently translated at 98.0% (2116 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 68 +++++++++++++---------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 2c17a90df8..fe125881f4 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -79,7 +79,7 @@ クリップボードへコピー 警告 お気に入り - 知人 + メンバー ルーム ルーム名で絞り込む 招待中 @@ -713,7 +713,7 @@ これを行うには設定から「インテグレーションを許可」を有効にしてください。 インテグレーションが無効になっています インテグレーションマネージャー - インテグレーション(統合)を許可 + インテグレーションを許可 FCMトークンが正常に取得されました: \n%1$s Firebaseトークン @@ -957,8 +957,8 @@ /confettiコマンドを使用するか、❄️または🎉を含むメッセージを送信 チャットでエフェクトを表示 ホームサーバーがこの機能をサポートしている場合は、チャット内のリンクをプレビューします。 - ボット、ブリッジ、ウィジェット、ステッカーパックの管理をします。 -\nインテグレーションマネージャーは、構成データを受信し、ユーザーに代わってウィジェットの変更、ルーム招待の送信、権限の設定などを行うことができます。 + ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。 +\nインテグレーションマネージャーは、構成データを受信し、ユーザーに代わってウィジェットの変更や、ルーム招待の送信、権限の設定などを行うことができます。 インテグレーション(統合) アプリがバックグラウンドにある場合、着信メッセージは通知されません。 ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 @@ -1296,7 +1296,7 @@ プッシュ通知に関するルール あなたは既にこのルームを見ています! その他のサードパーティーの使用に関する掲示 - Matrix SDKバージョン + Matrix SDKのバージョン ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポートします。 鍵のバックアップデータの取得中にエラーが発生しました 信頼情報の取得中にエラーが発生しました @@ -1314,7 +1314,7 @@ お待ち下さい… ネットワークがありません。インターネット接続を確認してください。 不正な形式のイベントです。表示できません - ルーム管理者によってモデレートされたイベント + ルームの管理者によってモデレートされたイベント リアクション リアクションを見る リアクションを追加 @@ -1463,7 +1463,7 @@ コンテンツが報告されました ヘルプとサポート ヘルプ - ${app_name}のポリシー + ${app_name}の運営方針 ここ キーワードを追加 自分のスレッド @@ -1564,7 +1564,7 @@ 応答がありません スレッドへのリンクをコピー 有効にする - あなたのIDサーバーのポリシー + あなたのIDサーバーの運営方針 新しいルームを作成 認証コードが正しくありません。 IDサーバーのURLを入力してください @@ -1602,7 +1602,7 @@ 添付ファイルの取得中にエラーが発生しました。 鍵のバックアップのバナーを閉じる キーワードに「%s」を含めることはできません - %s へのメール通知を有効にする + %sへのメール通知を有効にする ヒント:メッセージを長押しして「%s」を選択。 スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 あなたの非公開スペース @@ -1646,7 +1646,7 @@ 音声メッセージ(%1$s) 推奨のルームバージョンへとアップグレード 音声メッセージを録音 - あなたのホームサーバーのポリシー + あなたのホームサーバーの運営方針 一番下に移動 %sが読みました %1$sと%2$sが読みました @@ -1698,7 +1698,7 @@ %d人のユーザーが読みました スペースへのアクセス - このサーバーはポリシーを提供していません。 + このサーバーは運営方針を提供していません。 数秒かかるかもしれません。少々お待ちください。 利用可能な言語を読み込んでいます… ユーザーを招待 @@ -1706,14 +1706,14 @@ パスワードを選択してください。 メンバーを追加 ログインを検証 - メッセージ… + メッセージを送る… このファイルは大きすぎてアップロードできません。 この情報の送信に同意しますか? 連絡先を発見するには、連絡先のデータ(電話番号や電子メール)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 メールアドレスと電話番号を%sに送信 - このIDサーバーはポリシーを提供していません - IDサーバーのポリシーを隠す - IDサーバーのポリシーを表示 + このIDサーバーは運営方針を提供していません + IDサーバーの運営方針を隠す + IDサーバーの運営方針を表示 アカウントの新しいパスワードを設定… シェイクを検出しました! 電話を振って、しきい値を試してください @@ -1888,7 +1888,7 @@ スペースは、ルームや連絡先をグループ化する新しい方法です。 招待されています 新しいスペースを、あなたが管理するスペースに追加。 - 注意:アプリケーションは再起動します + 注意:アプリケーションが再起動します ホームサーバーの管理者にお問い合わせください あなたが参加している全てのルームがホームに表示されます。 親のスペースを自動的に更新 @@ -1932,7 +1932,7 @@ メッセージを送信できませんでした ウィジェットを開く %1$sに転送 - 通話は終了しました + 通話が終了しました 自分自身にダイレクトメッセージを送信することはできません! 電話番号(任意) 電話番号を確認 @@ -2038,7 +2038,7 @@ 位置情報を共有しました %sでリアクションしました 検証終了 - 次のいずれかのセキュリティが破られている可能性があります。 + 次のいずれかのセキュリティーが破られている可能性があります。 \n \n - あなたのホームサーバー \n - 検証している相手のホームサーバー @@ -2091,7 +2091,7 @@ ユーザーを招待できませんでした。招待したいユーザーを確認して、もう一度試してください。 %sの利用規約を開く ユーザーによる同意は与えられていません。 - 代わりに、他のIDサーバーのURLを入力できます + または、他のIDサーバーのURLを入力できます サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。 \n @@ -2200,7 +2200,7 @@ %sを待機しています… このユーザーがこのセッションを検証するまで、送受信されるメッセージには警告マークが付きます。手動で検証することも可能です。 セッションの取得に失敗しました - チームメイトは誰ですか? + 誰がチームの仲間ですか? %sを探索できるようになります 私のスペース %1$s %2$s に参加してください スキップ @@ -2228,21 +2228,21 @@ 再認証が必要です 全てリセット 連絡先 - 検証がキャンセルされました。再び検証を開始することができます。 + 検証をキャンセルしました。あらためて開始してください。 押し続けて録音し、離すと送信 - セキュリティー向上のため、PINコードを選択してください + PINコードを設定してください %d個のサーバーアクセス制御リストの変更 置き換えられたルームに参加 このルームが発見できません。存在することを確認してください。 - 指紋や顔画像など、端末に固有の生体認証を有効にしてください。 + 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で検証 テキストで検証 すべてのセッションを検証し、アカウントとメッセージが安全であることを確認してください ログインしている場所を確認 復旧用の手段を全て無くしてしまいましたか?全てリセットする - 、あるいはクロス署名に対応した他のMatrixのクライアント + クロス署名に対応した他のMatrixのクライアントでも使用できます。 どのような議論を%sで行いたいですか? クロス署名の設定に失敗しました 履歴とメッセージが消去され、信頼済の端末、信頼済のユーザーが取り消されます @@ -2252,7 +2252,7 @@ 電話番号を検索する際にエラーが発生しました 着信を拒否しました それぞれにルームを作りましょう。後から追加することもできます(既にあるルームも追加できます)。 - 分かるように特徴を記入してください。これはいつでも変更できます。 + このスペースを特定できるような特徴を記入してください。これはいつでも変更できます。 目立つように特徴を記入してください。これはいつでも変更できます。 未読のメッセージ数のみを通知に表示。 2分間${app_name}を使用しないと、PINコードが要求されます。 @@ -2297,8 +2297,8 @@ エラーのためメッセージが送信されませんでした %sにいない人を探していますか? 直接${app_name}で招待を受け取るには、設定画面から%sしてください。 - PINコードを入力しなければ${app_name}のロックを解除することはできません。 - ${app_name}を開く際にはPINコードの入力が必要です。 + PINコードでしか${app_name}のロックを解除することはできません。 + ${app_name}を開く際には、毎回PINコードの入力が必要です。 あなたがブロックされているルームを開くことはできません。 PINコードの検証に失敗しました。新しいコードを入力してください。 端末の連絡先がありません @@ -2308,7 +2308,7 @@ このメッセージを待機しています。時間がかかる可能性があります ルームの設定の変更に成功しました 確認のため、セキュリティーフレーズを再入力してください。 - ホームサーバー(%1$s)が、IDサーバーに%2$sを設定するよう提案しています + ホームサーバー(%1$s)は、IDサーバーに%2$sを設定するように提案しています IDサーバー %s から切断しますか? ダイレクトメッセージを作成できませんでした。招待したユーザーを確認し、もう一度やり直してください。 セキュリティーフレーズ @@ -2323,4 +2323,16 @@ %1$s、%2$s他 %1$sと%2$s 自分と相手を認証してチャットを安全に保ちます + あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。 + 監査結果をエクスポート + ストレージから機密情報を発見できません + 操作を実行できません。ホームサーバーは最新のバージョンではありません。 + ビデオ通話が拒否されました + 音声通話が拒否されました + %1$sは通話を拒否しました + このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 + このセッションを信頼済として検証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: + アカウントのセキュリティーが破られている可能性があります + 選択したスペースに追加 + 最新の${app_name}は他のデバイスでも使用できます: \ No newline at end of file From 214e0efcd990a6b4c9d491819e010dff9cc13069 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 2 Mar 2022 13:47:08 +0200 Subject: [PATCH 032/405] Add Markdown support to thread summaries and thread list --- .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../internal/database/mapper/EventMapper.kt | 2 +- .../detail/search/SearchResultController.kt | 3 + .../room/detail/search/SearchResultItem.kt | 4 +- .../format/DisplayableEventFormatter.kt | 100 +++++++++++++++++- .../helper/MessageItemAttributesFactory.kt | 3 + .../detail/timeline/item/AbsMessageItem.kt | 3 +- .../list/viewmodel/ThreadListController.kt | 38 +++++-- 8 files changed, 139 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index fafe17b2c0..d6937d5b26 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.threads +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.sender.SenderInfo /** @@ -26,7 +27,7 @@ data class ThreadDetails( val isRootThread: Boolean = false, val numberOfThreads: Int = 0, val threadSummarySenderInfo: SenderInfo? = null, - val threadSummaryLatestTextMessage: String? = null, + val threadSummaryLatestEvent: Event? = null, val lastMessageTimestamp: Long? = null, var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE, val isThread: Boolean = false, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 9c420e81fd..c3302f5ccb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -114,7 +114,7 @@ internal object EventMapper { ) }, threadNotificationState = eventEntity.threadNotificationState, - threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(), + threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(), lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 2cdc1a0d90..5b1f17cfe2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -32,6 +32,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Content @@ -45,6 +46,7 @@ class SearchResultController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, private val userPreferencesProvider: UserPreferencesProvider ) : TypedEpoxyController() { @@ -125,6 +127,7 @@ class SearchResultController @Inject constructor( .sender(eventAndSender.sender ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem()) .threadDetails(event.threadDetails) + .threadSummaryFormatted(displayableEventFormatter.formatThreadSummary(event.threadDetails?.threadSummaryLatestEvent).toString()) .areThreadMessagesEnabled(userPreferencesProvider.areThreadMessagesEnabled()) .listener { listener?.onItemClicked(eventAndSender.event) } .let { result.add(it) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 2ec786fab2..3e141ab0e9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -42,6 +42,7 @@ abstract class SearchResultItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var spannable: EpoxyCharSequence @EpoxyAttribute var sender: MatrixItem? = null @EpoxyAttribute var threadDetails: ThreadDetails? = null + @EpoxyAttribute var threadSummaryFormatted: String? = null @EpoxyAttribute var areThreadMessagesEnabled: Boolean = false @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null @@ -60,8 +61,7 @@ abstract class SearchResultItem : VectorEpoxyModel() { if (it.isRootThread) { showThreadSummary(holder) holder.threadSummaryCounterTextView.text = it.numberOfThreads.toString() - holder.threadSummaryInfoTextView.text = it.threadSummaryLatestTextMessage.orEmpty() - + holder.threadSummaryInfoTextView.text = threadSummaryFormatted.orEmpty() val userId = it.threadSummarySenderInfo?.userId ?: return@let val displayName = it.threadSummarySenderInfo?.displayName val avatarUrl = it.threadSummarySenderInfo?.avatarUrl diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index d5f3a74e4e..d4a6f2ee87 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -24,9 +24,11 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.html.EventHtmlRenderer import me.gujun.android.span.span import org.commonmark.node.Document +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.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -120,14 +122,14 @@ class DisplayableEventFormatter @Inject constructor( EventType.CALL_CANDIDATES -> { span { } } - EventType.POLL_START -> { + EventType.POLL_START -> { timelineEvent.root.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question ?: stringProvider.getString(R.string.sent_a_poll) } - EventType.POLL_RESPONSE -> { + EventType.POLL_RESPONSE -> { stringProvider.getString(R.string.poll_response_room_list_preview) } - EventType.POLL_END -> { + EventType.POLL_END -> { stringProvider.getString(R.string.poll_end_room_list_preview) } else -> { @@ -139,6 +141,98 @@ class DisplayableEventFormatter @Inject constructor( } } + fun formatThreadSummary( + event: Event?, + latestEdition: String? = null): CharSequence { + event ?: return "" + + // There event have been edited + if (latestEdition != null) { + return run { + val localFormattedBody = htmlRenderer.get().parse(latestEdition) as Document + val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: latestEdition + renderedBody + } + } + + // The event have been redacted + if (event.isRedacted()) { + return noticeEventFormatter.formatRedactedEvent(event) + } + + // The event is encrypted + if (event.isEncrypted() && + event.mxDecryptionResult == null) { + return stringProvider.getString(R.string.encrypted_message) + } + + return when (event.getClearType()) { + EventType.MESSAGE -> { + (event.getClearContent().toModel() as? MessageContent)?.let { messageContent -> + when (messageContent.msgType) { + MessageType.MSGTYPE_TEXT -> { + val body = messageContent.getTextDisplayableContent() + if (messageContent is MessageTextContent && messageContent.matrixFormattedBody.isNullOrBlank().not()) { + val localFormattedBody = htmlRenderer.get().parse(body) as Document + val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: body + renderedBody + } else { + body + } + } + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + stringProvider.getString(R.string.verification_request) + } + MessageType.MSGTYPE_IMAGE -> { + stringProvider.getString(R.string.sent_an_image) + } + MessageType.MSGTYPE_AUDIO -> { + if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) { + stringProvider.getString(R.string.sent_a_voice_message) + } else { + stringProvider.getString(R.string.sent_an_audio_file) + } + } + MessageType.MSGTYPE_VIDEO -> { + stringProvider.getString(R.string.sent_a_video) + } + MessageType.MSGTYPE_FILE -> { + stringProvider.getString(R.string.sent_a_file) + } + MessageType.MSGTYPE_LOCATION -> { + stringProvider.getString(R.string.sent_location) + } + else -> { + messageContent.body + } + } + } ?: span { } + } + EventType.STICKER -> { + stringProvider.getString(R.string.send_a_sticker) + } + EventType.REACTION -> { + event.getClearContent().toModel()?.relatesTo?.let { + emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key)) + } ?: span { } + } + EventType.POLL_START -> { + event.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question + ?: stringProvider.getString(R.string.sent_a_poll) + } + EventType.POLL_RESPONSE -> { + stringProvider.getString(R.string.poll_response_room_list_preview) + } + EventType.POLL_END -> { + stringProvider.getString(R.string.poll_end_room_list_preview) + } + else -> { + span { + } + } + } + } + private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence { return if (appendAuthor) { span { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 845b765101..ef42e32a76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -22,6 +22,7 @@ import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import org.matrix.android.sdk.api.session.threads.ThreadDetails @@ -32,6 +33,7 @@ class MessageItemAttributesFactory @Inject constructor( private val messageColorProvider: MessageColorProvider, private val avatarSizeProvider: AvatarSizeProvider, private val stringProvider: StringProvider, + private val displayableEventFormatter: DisplayableEventFormatter, private val preferencesProvider: UserPreferencesProvider, private val emojiCompatFontProvider: EmojiCompatFontProvider) { @@ -59,6 +61,7 @@ class MessageItemAttributesFactory @Inject constructor( readReceiptsCallback = callback, emojiTypeFace = emojiCompatFontProvider.typeface, decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message), + threadSummaryFormatted = displayableEventFormatter.formatThreadSummary(threadDetails?.threadSummaryLatestEvent).toString(), threadDetails = threadDetails, areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled() ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 9e8f86c26e..bad29bd444 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -115,7 +115,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem attributes.threadDetails?.let { threadDetails -> holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() - holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage ?: attributes.decryptionErrorMessage + holder.threadSummaryInfoTextView.text = attributes.threadSummaryFormatted ?: attributes.decryptionErrorMessage val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let val displayName = threadDetails.threadSummarySenderInfo?.displayName @@ -183,6 +183,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null, val decryptionErrorMessage: String? = null, + val threadSummaryFormatted: String? = null, val threadDetails: ThreadDetails? = null, val areThreadMessagesEnabled: Boolean = false ) : AbsBaseMessageItem.Attributes { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index d3a5497d63..aeef69c6dc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -17,11 +17,11 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController -import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.threads.list.model.threadListItem import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary @@ -35,6 +35,7 @@ class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, private val session: Session ) : EpoxyController() { @@ -70,9 +71,18 @@ class ThreadListController @Inject constructor( } ?.forEach { threadSummary -> val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) - val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) - val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition - val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition + val lastMessageFormatted = threadSummary.let { + displayableEventFormatter.formatThreadSummary( + event = it.latestEvent, + latestEdition = it.threadEditions.latestThreadEdition + ).toString() + } + val rootMessageFormatted = threadSummary.let { + displayableEventFormatter.formatThreadSummary( + event = it.rootEvent, + latestEdition = it.threadEditions.rootThreadEdition + ).toString() + } threadListItem { id(threadSummary.rootEvent?.eventId) avatarRenderer(host.avatarRenderer) @@ -82,8 +92,8 @@ class ThreadListController @Inject constructor( rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) // TODO refactor notifications that with the new thread summary threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) - lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) + rootMessage(rootMessageFormatted) + lastMessage(lastMessageFormatted) lastMessageCounter(threadSummary.numberOfThreads.toString()) lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) itemClickListener { @@ -112,8 +122,18 @@ class ThreadListController @Inject constructor( } ?.forEach { timelineEvent -> val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST) - val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) val lastRootThreadEdition = timelineEvent.root.threadDetails?.lastRootThreadEdition + val lastMessageFormatted = timelineEvent.root.threadDetails?.threadSummaryLatestEvent.let { + displayableEventFormatter.formatThreadSummary( + event = it, + ).toString() + } + val rootMessageFormatted = timelineEvent.root.let { + displayableEventFormatter.formatThreadSummary( + event = it, + latestEdition = lastRootThreadEdition + ).toString() + } threadListItem { id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) @@ -122,8 +142,8 @@ class ThreadListController @Inject constructor( date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(lastRootThreadEdition ?: timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage) - lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage ?: decryptionErrorMessage) + rootMessage(rootMessageFormatted) + lastMessage(lastMessageFormatted) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) itemClickListener { From 4ca2a4e8b14b59cc3db02bf8fa9591ca1ef21aab Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 1 Mar 2022 15:38:06 +0000 Subject: [PATCH 033/405] Translated using Weblate (German) Currently translated at 99.8% (2153 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- vector/src/main/res/values-de/strings.xml | 78 +++++++---------------- 1 file changed, 23 insertions(+), 55 deletions(-) diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index bf72827085..7a8ac9c71b 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -39,7 +39,6 @@ Raumeinladung %1$s und %2$s Leerer Raum - %s hat diesen Raum aufgewertet. Sende eine Nachricht… Erste Synchronisation: @@ -244,7 +243,6 @@ Löschen Umbenennen Inhalt melden - oder Einladen Abmelden @@ -267,7 +265,6 @@ Nur Matrix-Kontakte Keine Ergebnisse Räume - Logdateien übermitteln Absturzberichte übermitteln Screenshot übermitteln @@ -298,7 +295,6 @@ Dieser Homeserver möchte sicherstellen, dass du kein Roboter bist Die E-Mail-Adresse, die mit deinem Account verknüpft ist, muss eingegeben werden. Verifizierung der E-Mail-Adresse ist fehlgeschlagen. Stelle sicher, dass du den Link in der E-Mail geöffnet hast - Bitte eine gültige URL eingeben Fehlerhaftes JSON Enthielt kein gültiges JSON @@ -314,13 +310,10 @@ Anruf aktiv… Die Gegenseite hat den Anruf nicht angenommen. Information - ${app_name} benötigt die Berechtigung, auf dein Mikrofon zugreifen zu können, um (Sprach-)Anrufe tätigen zu können. - ${app_name} benötigt die Berechtigung, auf Kamera und Mikrofon zu zugreifen, um Video-Anrufe durchzuführen. \n \nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen. - Ja Nein Fortsetzen @@ -328,7 +321,6 @@ Betreten Ablehnen Zu ungelesenen Nachrichten springen - Raum verlassen Raum wirklich verlassen\? DIREKT-CHATS @@ -341,8 +333,8 @@ Du wirst diese Änderung nicht rückgängig machen können, da die Person dieselbe Berechtigungsstufe wie du erhalten wird. \nBist du sicher\? %s schreibt… - %1$s & %2$s schreiben… - %1$s, %2$s & andere schreiben… + %1$s und %2$s schreiben… + %1$s, %2$s und andere schreiben… Du bist nicht berechtigt, in diesen Raum zu schreiben. Vertrauen Nicht vertrauen @@ -355,7 +347,6 @@ Das Zertifikat unterscheidet sich von dem Zertifikat, dem dein Gerät ursprünglich vertraut hat. Dies ist SEHR UNGEWÖHNLICH. Es wird empfohlen, dass du dieses neue Zertifikat NICHT AKZEPTIERST. Das Zertifikat hat sich von einem ursprünglich vertrauenswürdigem Zertifikat in ein nicht vertrauenswürdiges Zertifikat geändert. Eventuell wurde das Zertifikat des Servers erneuert. Bitte erkundige dich beim Server-Administrator, welcher Fingerprint als vertrauenswürdig gilt. Akzeptiere das Zertifikat nur dann, wenn der Server-Administrator einen Fingerprint veröffentlicht hat, der mit dem obigen übereinstimmt. - Suchen Raummitglieder filtern Keine Suchergebnisse @@ -400,7 +391,6 @@ Öffentlichen Namen aktualisieren Zuletzt gesehen %1$s @ %2$s - Authentifizierung Angemeldet als Heimserver @@ -441,7 +431,6 @@ Exportieren Passphrase eingeben Passphrase bestätigen - Ende-zu-Ende-Raumschlüssel importieren Raumschlüssel importieren Schlüssel aus lokaler Datei importieren @@ -453,7 +442,6 @@ Bestätigen Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des anderen Nutzers und bestätige: Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert. - Raumverzeichnis auswählen Server-Name Alle Räume auf dem %s-Server @@ -531,7 +519,6 @@ Community-Avatare Schütteln, um einen Fehler zu melden Mitglieder auflisten - %d Mitglied %d Mitglieder @@ -540,13 +527,10 @@ %d neue Nachricht %d neue Nachrichten - - %d ungelesene Nachricht %d ungelesene Nachrichten - %d Raum %d Räume @@ -606,16 +590,10 @@ Die Konversation wird hier fortgesetzt Dieser Raum ist die Fortsetzung einer anderen Konversation Klicke hier um die älteren Nachrichten zu sehen - - - - %d ausgewählt %d ausgewählt - - Systembenachrichtigungen kontaktiere deinen Service-Administrator Dieser Homeserver hat eine seiner Ressourcen-Grenzen erreicht, sodass einige Nutzer sich nicht anmelden können. @@ -707,7 +685,6 @@ Token-Registrierung Wenn ein Benutzer ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Bereitschaftsmodus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. Ignoriere Optimierungen - Keine validen Google-Play-Dienste gefunden. Benachrichtigungen könnten nicht richtig funktionieren. Videogespräch aktiv… Schlüsselsicherung @@ -756,7 +733,6 @@ Nachrichten in verschlüsselten Räumen sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der Empfänger haben die Schlüssel um diese Nachrichten zu lesen. \n \nSichere deine Schlüssel, um sie nicht zu verlieren. - Wiederherstellungsschlüssel aus Passphrase generieren. Dies kann mehrere Sekunden brauchen. Du verlierst möglicherweise den Zugang zu deinen Nachrichten, wenn du dich abmeldest oder das Gerät verlierst. Rufe Backup-Version ab… @@ -817,7 +793,6 @@ Bewahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort auf, wie z.B. einem Passwortmanager (oder Tresor) auf Ich habe eine Kopie angefertigt Teilen - Verliere nie wieder verschlüsselte Nachrichten Benutze Schlüsselsicherung Neue sichere Schlüssel für Nachrichten @@ -839,7 +814,6 @@ Nachricht mit Eingabetaste senden Eingabetaste der Bildschirmtastatur schickt die Nachricht ab, statt einen Zeilenumbruch zu erzeugen Das Passwort ist ungültig - Medien Standard-Komprimierung Wählen @@ -879,7 +853,6 @@ Überprüfe Sicherungsstatus Verifiziert! Verstanden - Verifizierungsanfrage %s möchte deine Sitzung verifizieren Unbekannter Fehler @@ -949,7 +922,6 @@ Link in die Zwischenablage kopiert Raum erstellen… Bearbeitungsverlauf anzeigen - E2E-Schlüssel aus der Datei \"%1$s\" importieren. Vielen Dank, der Vorschlag wurde erfolgreich gesendet Der Vorschlag konnte nicht gesendet werden (%s) @@ -986,7 +958,6 @@ Erkennungsoptionen werden angezeigt, sobald du eine E-Mail hinzugefügt hast. Gib einen neuen Identitätsserver ein Konnte keine Verbindung zum Homeserver herstellen - Dies ist keine Adresse eines Matrixservers Kann Homeserver nicht unter dieser URL erreichen. Bitte überprüfen Hintergrund-Synchronisierungsmodus @@ -994,7 +965,6 @@ \nAbhängig vom Ressourcen-Status deines Geräts kann dein System die Synchronisierung verschieben. ${app_name} wird sich im Hintergrund periodisch zu einem bestimmten Zeitpunkt synchronisieren (konfigurierbar). \nDies wird Funk- und Akkunutzung beeinflussen. Es wird eine permanente Benachrichtigung geben, die sagt, dass ${app_name} auf Ereignisse lauscht. - Integrationen Benutze einen Integrationsmanager um Bots, Brücken, Widgets und Stickerpakete zu verwalten. \nIntegrationsmanager erhalten Rauminformationen und können so Widgets verändern, Einladungen senden und in deinem Namen Berechtigungslevel setzen. @@ -1078,7 +1048,6 @@ Dieser Inhalt wurde als unangebracht gemeldet. \n \nWenn du keine weiteren Inhalte dieses Nutzers sehen möchtest, kannst ihn ignorieren, um jene Nachrichten auszublenden. - Nutzer ignorieren Alle Nachrichten (laut) Alle Nachrichten @@ -1101,13 +1070,13 @@ Premium-Hosting für Organisationen Mehr erfahren Andere - Benutzerdefinierte & erweiterte Einstellungen + Benutzerdefinierte und erweiterte Einstellungen Fortfahren Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzern gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. Du teilst deine E-Mail-Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden zu werden. Zu teilende Daten nicht verarbeitbar - Erweitere & individualisiere dein Benutzererlebnis + Erweitere und individualisiere dein Benutzererlebnis Mit %1$s verbinden Mit Element Matrix Services verbinden Mit einem individuellen Server verbinden @@ -1271,7 +1240,6 @@ %s verifizieren %s verifiziert Warte auf %s… - Nachrichten in diesem Raum sind nicht Ende-zu-Ende-verschlüsselt. Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. \n @@ -1295,7 +1263,7 @@ Nutzer Admin in %1$s Moderation in %1$s - Springen & als gelesen markieren + Springen und als gelesen markieren ${app_name} kann keine Ereignisse vom Typ \'%1$s\' ${app_name} ist beim Verarbeiten des Ereignisinhalts mit der ID \'%1$s\' auf ein Problem gestoßen Nicht ignorieren @@ -1392,7 +1360,7 @@ \n- Dieses Gerät, oder das andere Gerät \n- Die Internetverbindung, die von den Geräten genutzt wird \n -\nWir empfehlen dir dein Passwort & Wiederherstellungsschlüssel in den Einstellungen sofort zu ändern. +\nWir empfehlen dir dein Passwort und den Wiederherstellungsschlüssel in den Einstellungen sofort zu ändern. Verifizierung abgebrochen. Du kannst sie erneut starten. Verifizierung abgebrochen Gib dein %s ein um fortzufahren. @@ -1409,7 +1377,7 @@ Synchronisiere Benutzerschlüssel Synchronisiere selbstsignierenden Schlüssel Richte Schlüsselbackup ein - Deine %2$s & %1$s sind nun eingerichtet. + Deine %2$s und %1$s sind nun eingerichtet. \n \nBewahre sie sicher auf! Du wirst sie benötigen, um verschlüsselte Nachrichten und sichere Informationen freizuschalten, wenn du alle deine aktive Sitzungen verlierst. Speichere ihn auf einem USB-Stick oder auf einem Sicherungslaufwerk @@ -1426,7 +1394,7 @@ Verschlüsselung ist nicht aktiviert Raumupgrades Verschlüsselung aktiviert - Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil. + Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Erfahre mehr und verifiziere Benutzer in deren Profil. Die Verschlüsselung in diesem Raum wird nicht unterstützt Warte auf %s… Fehlerbehebung @@ -1435,12 +1403,11 @@ Fast geschafft! Warte auf Bestätigung… Verschlüsselte Direktnachrichten Nachricht… - Verifiziere dich & andere, um eure Chats zu schützen + Verifiziere dich und andere, um eure Chats zu schützen Gib zum Fortfahren deinen %s ein Datei benutzen Dies ist kein gültiger Wiederherstellungsschlüssel Bitte gib deinen Wiederherstellungsschlüssel ein - Verschlüsselungsupgrade verfügbar Überprüfe Wiederherstellungsschlüssel Überprüfe Sicherungsstatus (%s) @@ -1472,7 +1439,7 @@ Unverschlüsselt Verschlüsselt von einem unbekannten Gerät Überprüfe, wo du angemeldet bist - Verifiziere alle deine Sitzungen, um sicherzustellen, dass dein Konto & deine Nachrichten sicher sind + Verifiziere alle deine Sitzungen, um sicherzustellen, dass dein Konto und deine Nachrichten sicher sind Bestätige neue Anmeldung zu deinem Konto: %1$s Verifiziere manuell mit einem Text Anmeldung verifizieren @@ -1543,7 +1510,7 @@ Backup einrichten Backup zurücksetzen Auf diesem Gerät einrichten - Verlust verschlüsselter Nachrichten & Daten verhindern, indem die Schlüssel für die Entschlüsselung auf dem Server gesichert werden. + Verlust verschlüsselter Nachrichten und Daten verhindern, indem die Schlüssel für die Entschlüsselung auf dem Server gesichert werden. Generiere einen neuen Sicherheitsschlüssel oder setze eine neue Sicherheitspassphrase für dein existierendes Backup. Dieses wird deinen aktuellen Schlüssel oder deine aktuelle Phrase ersetzen. Integrationen sind deaktiviert @@ -1589,9 +1556,9 @@ Dein Serveradministrator hat in privaten Räumen und Direktnachrichten Ende-zu-Ende-Verschlüsselung standardmäßig deaktiviert. Flugzeugmodus ist aktiv Gib eine Sicherheitsphrase ein, die nur du kennst. Diese wird benutzt um deine Daten auf dem Server geheim zu halten. - Wenn du jetzt abbrichst und den Zugriff zu deinen Sitzungen verlierst, kannst du verschlüsselte Nachrichten & Daten verlieren. + Wenn du jetzt abbrichst und den Zugriff zu deinen Sitzungen verlierst, kannst du verschlüsselte Nachrichten und Daten verlieren. \n -\nDu kannst auch ein Backup einrichten & deine Schlüssel in den Einstellungen verwalten. +\nDu kannst auch ein Backup einrichten und deine Schlüssel in den Einstellungen verwalten. Du hast den Raum erstellt und konfiguriert. Dieser Account ist deaktiviert worden. Konnte Mediendatei nicht speichern @@ -1620,7 +1587,7 @@ Stoppe Kamera Starte Kamera Backup - Verlust verschlüsselter Nachrichten & Daten verhindern, indem die Schlüssel für die Entschlüsselung am Server gesichert werden. + Verlust verschlüsselter Nachrichten und Daten verhindern, indem die Schlüssel für die Entschlüsselung am Server gesichert werden. Sicherheitsschlüssel benutzen Generiere einen Sicherheitsschlüssel, welcher z.B. in einem Passwortmanager oder in einem Tresor sicher aufbewahrt werden sollte. Eine Sicherheitsphrase benutzen @@ -1786,7 +1753,7 @@ Raumname Prüfung exportieren Direktnachricht - Geschichte der Anfragen von Schlüsselfreigaben senden + Verlauf der Anfragen von Schlüsselfreigaben senden Keine weiteren Ergebnisse Starte die Diskussion Autorisieren @@ -1841,7 +1808,6 @@ Aktivieren, wenn der Raum nur von Mitgliedern deines Homeservers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden. Begrenze Zugang zu diesem Raum (für immer!) auf Mitglieder von %s %1$d von %2$d - Keine Vorschau für diesen Raum verfügbar. Willst du direkt beitreten\? Der Raum ist gerade nicht zugänglich. \nVersuche es später nochmal, oder bitte einen Raum-Admin um Hilfe. @@ -1910,8 +1876,6 @@ Beim Weiterleiten des Anrufs ist ein Fehler aufgetreten Weiterleiten Verbinden - - Aktiver Anruf (%1$s) Beim Suchen der Telefonnummer ist ein Fehler aufgetreten Wähltastatur @@ -2106,7 +2070,6 @@ Spaces Feedback Dieser Server ist schon in der Liste vorhanden Server oder Raumliste kann nicht gefunden werden - Bei %1$s anfragen Zu %1$s weiterleiten Space-Adressen @@ -2187,7 +2150,7 @@ Alle im übergeordneten Space haben Zugriff auf den Raum - es ist nicht nötig jeden einzeln einzuladen. Du kannst dies in den Raumeinstellungen jederzeit ändern. Andere Spaces oder Räume die du kennst Spaces mit diesem Raum und dir als Mitglied - Nur Erwähnungen & Schlüsselwörter + Nur Erwähnungen und Schlüsselwörter Auflegen… Sprachanruf mit %s Videoanruf mit %s @@ -2353,7 +2316,6 @@ Sichere Nachrichtenübertragung. Besitze deine Konversationen. Um bestehende Kontakte ermitteln zu können, müsst du Kontaktinformationen (E-Mails und Telefonnummern) an Ihren Identitätsserver senden. Wir verschlüsseln deine Daten vor dem Senden, um den Datenschutz zu gewährleisten. - Deine Kontakte sind privat. Um in deinen Kontakten Benutzer erkennen zu können, benötigen wir deine Erlaubnis, Kontaktinformationen an deinen Identitätsserver zu senden. Dieser Server stellt keine Richtlinie bereit. Deine Identitätsserver-Richtlinie @@ -2430,7 +2392,7 @@ Hinweis: App wird neugestartet diese Frage überspringen Noch nicht sicher\? Du kannst %s - Freundschaften und Familie + Freunde und Familie In Thread antworten Aus einem Thread Filter @@ -2450,4 +2412,10 @@ %1$d mehr Weniger anzeigen + %1$s und %2$s + %1$s, %2$s und andere + + %d Server-ACL geändert + %d Server-ACLs geändert + \ No newline at end of file From a71cc5a942ce79ad557c728cb636153c9b999fa5 Mon Sep 17 00:00:00 2001 From: random Date: Mon, 28 Feb 2022 10:07:30 +0000 Subject: [PATCH 034/405] Translated using Weblate (Italian) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/it/ --- vector/src/main/res/values-it/strings.xml | 42 ++--------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 849a4e92a4..492d8af526 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -39,7 +39,6 @@ Invito nella stanza %1$s e %2$s Stanza vuota - Sincronizzazione iniziale: \nImportazione account… Sincronizzazione iniziale: @@ -241,7 +240,6 @@ Elimina Rinomina Segnala contenuto - o Invita Disconnetti @@ -264,7 +262,6 @@ Mostra solo i contatti Matrix Nessun risultato Stanze - Invia i registri Invia i registri di crash Invia schermata @@ -295,11 +292,9 @@ Questo indirizzo email non sembra corretto L\'indirizzo email è già stato impostato. Hai dimenticato la password\? - Questo homeserver vuole assicurarsi che tu non sia un robot Va inserito l\'indirizzo email associato al tuo account. La verifica del tuo indirizzo email è fallita: assicurati di aver cliccato sul link contenuto nella mail - Inserisci un URL valido JSON malformato Non conteneva un JSON valido @@ -315,14 +310,10 @@ Chiamata in corso… Ricezione fallita da parte del destinatario. Informazione - - ${app_name} deve essere autorizzato ad accedere al microfono e poter così fare chiamate audio. - ${app_name} deve essere autorizzato ad accedere a fotocamera e microfono per poter fare chiamate video. \n \nNel prossimo pop-up concedi le autorizzazioni per poter fare la chiamata. - NO Continua @@ -330,7 +321,6 @@ Entra Rifiuta Vai ai non letti - Esci dalla stanza Sei sicuro di voler uscire dalla stanza? CHAT DIRETTE @@ -357,7 +347,6 @@ Il certificato è diverso da quello precedentemente contrassegnato sul tuo telefono come \"affidabile\". Questa cosa è MOLTO INSOLITA. Si raccomanda di NON ACCETTARE questo nuovo certificato. Il certificato del server è cambiato: quello precedente era stato contrassegnato come affidabile ma quello attuale no. Può darsi che il certificato precedente sia scaduto e sia stato semplicemente sostituito con uno nuovo. Contatta l\'amministratore del server per verificare l\'impronta digitale in uso. Contrassegna il certificato come affidabile solo se l\'mpronta digitale comunicata dall\'amministratore del server corrisponde a quella qua sopra. - Cerca Cerca tra i membri della stanza Nessun risultato @@ -443,7 +432,6 @@ Queste sono caratteristiche sperimentali che potrebbero dare risultati inattesi. Usali con cautela. Imposta come indirizzo principale Non usare più come indirizzo principale - Tema Errore di decriptazione Nome pubblico @@ -455,7 +443,6 @@ Esporta Inserisci la Passphrase Conferma la Passphrase - Importa le chiavi di crittografia E2E della stanza Importa le chiavi della stanza Importa le chiavi da un file locale @@ -467,7 +454,6 @@ Conferma Conferma confrontando la seguente con le impostazioni utente della tua altra sessione: Se non corrispondono, la sicurezza delle tue comunicazioni potrebbe essere compromessa. - Scegli un elenco di stanze Nome del server Tutte le stanze sull\'Home Server %s @@ -509,7 +495,6 @@ Sicuro di voler fare una chiamata audio\? Sicuro di voler fare una videochiamata\? Elenco dei membri - %d utente %d utenti @@ -519,8 +504,6 @@ %d nuovo messaggio %d nuovi messaggi - - Tutti i messaggi Aggiungi alla schermata iniziale Anteprima degli URL @@ -530,7 +513,6 @@ %d messaggio notificato non letto %d messaggi notificati non letti - %d stanza %d stanze @@ -608,16 +590,10 @@ La conversazione continua qui Questa stanza contiene una conversazione cominciata altrove Clicca qui per vedere i messaggi precedenti - - - - %d selezionato %d selezionati - - Avvisi di sistema contatta l\'amministratore del servizio L\'Home Server ha superato uno dei limiti delle risorse, pertanto alcuni utenti non potranno accedere. @@ -708,7 +684,6 @@ ${app_name} non è influenzato dall\'ottimizzazione della batteria. Se si lascia un dispositivo scollegato, fermo e con lo schermo spento, dopo un certo tempo questo entra in modalità Doze. Ciò impedisce alle App di accedere alla rete e ritarda le attività, le sincronizzazioni e la ricezione dei normali allarmi. Ignora l\'ottimizzazione - Non è stato trovato nessun APK Google Play Services valido. Le notifiche non funzioneranno correttamente. Chiamata video in corso… Backup delle chiavi @@ -778,7 +753,6 @@ Salva il codice di recupero Condividi Salva come file - Si prega di farne una copia Condividi il codice di recupero con… Generazione del codice di recupero basato sulla Passphrase. Questo processo può durare alcuni secondi. @@ -826,7 +800,6 @@ Eliminazione Backup… Elimina Backup Eliminare il Backup delle chiavi crittografiche dall\'Home Server\? Non potrai più usare il codice di recupero per leggere i messaggi cifrati. - Non perdere mai i messaggi cifrati Usa il Backup delle chiavi Nuove chiavi per messaggi sicuri @@ -840,7 +813,6 @@ Versione Algoritmo Firma - Multimedia Compressione predefinita Scegli @@ -874,8 +846,6 @@ Ignora Verificato! Capito - - Richiesta di verifica %s vuole verificare la tua sessione Errore sconosciuto @@ -972,7 +942,6 @@ Nessuno Revoca Disconnetti - Non è stato trovato alcun Home Server seguendo questo URL. Per favore, controllalo Modalità sync in background Ottimizzato per la batteria @@ -983,7 +952,6 @@ \nCiò avrà un certo impatto sulla quantità di dati e batteria utilizzati. Una notifica sempre accesa comunicherà che ${app_name} è attivo. Nessuna sincronizzazione in background Quando l\'App è in background non ti verranno notificati i messaggi in arrivo. - Farsi trovare Gestisci le impostazioni per farsi trovare. Non stai usando alcun server di identità @@ -1053,7 +1021,6 @@ Questo contenuto è stato segnalato come inappropriato. \n \nSe non vuoi più vedere i contenuti di questo utente puoi ignorarlo. Ciò nasconderà i suoi messaggi dalla tua vista. - Integrazioni Usa un gestore di integrazioni per gestire bot, bridge, widget e pacchetti di sticker. \nI gestori di integrazioni possono ricevere dati di configurazione, modificare widget, mandare inviti alle stanze e modificare permessi a tuo nome. @@ -1264,7 +1231,6 @@ Verifica %s %s verificato In attesa di %s… - I messaggi in questa stanza non sono cifrati E2E. I messaggi in questa stanza sono cifrati E2E. \n @@ -1410,7 +1376,6 @@ Stampala e conservala in un posto sicuro Salvala in una penna USB o disco di backup Copiala sul Cloud - Crittografia attiva I messaggi in questa stanza sono crittografati E2E. Maggiori info e verifica degli utenti nel loro profilo. Crittografia non attiva @@ -1813,7 +1778,6 @@ Nascondi avanzate Mostra avanzate %1$d di %2$d - Dai il consenso Revoca il mio consenso Hai acconsentito ad inviare email e numeri di telefono a questo server d\'identità per poter rintracciare altri utenti tra i tuoi contatti. @@ -1902,8 +1866,6 @@ Trasferisci Connetti Prima consulta - - Chiamata attiva (%1$s) Si è verificato un errore cercando il numero di telefono Tastierino numerico @@ -2102,7 +2064,6 @@ Inserisci il nome di un nuovo server che vuoi esplorare. Aggiungi un nuovo server Il tuo server - Spiacenti, si è verificato un errore tentando di entrare: %s Indirizzo dello spazio Vedi e gestisci gli indirizzi di questo spazio. @@ -2303,7 +2264,6 @@ Domanda o argomento del sondaggio Crea sondaggio Sondaggio - Invia email e numeri di telefono a %s I tuoi contatti sono privati. Per trovare utenti dai tuoi contatti, ci serve l\'autorizzazione per inviare le informazioni dei contatti al tuo server d\'identità. La sessione è stata disconnessa! @@ -2448,4 +2408,6 @@ %d modifica delle ACL del server %d modifiche delle ACL del server + %1$s e %2$s + %1$s, %2$s e altri \ No newline at end of file From 8f1cd11bbbef8c96defac371a4a8a55a57c75699 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 2 Mar 2022 16:21:58 +0000 Subject: [PATCH 035/405] Translated using Weblate (Japanese) Currently translated at 98.3% (2121 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index fe125881f4..fbd6540dd6 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -1974,7 +1974,7 @@ 国際電話番号は「+」から始まる必要があります コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません - パスワードを変更すると、すべてのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションから鍵をエクスポートしておいてください。 + パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 パスワードの再設定を確認するために認証メールを送信します。 このメールアドレスはどのアカウントにも属していません。 このアプリではこのホームサーバーにアカウントを作成できません。 @@ -2322,7 +2322,7 @@ \n検証は端末に保存され、新しいバージョンのアプリで共有されます。 %1$s、%2$s他 %1$sと%2$s - 自分と相手を認証してチャットを安全に保ちます + 自分と相手を認証して、チャットを安全に保ちましょう あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。 監査結果をエクスポート ストレージから機密情報を発見できません @@ -2335,4 +2335,9 @@ アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 最新の${app_name}は他のデバイスでも使用できます: + 音声通話が終了しました・%1$s + ビデオ通話が終了しました・%1$s + メールアドレスで招待したり、連絡先を検索したりできます… + 鍵のバックアップの機密情報をSSSSに保存しています + あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。 \ No newline at end of file From 84a27d23fdbd8bf8ba093eb68ff8c6f34a7eb796 Mon Sep 17 00:00:00 2001 From: Arusekk Date: Wed, 2 Mar 2022 20:59:53 +0000 Subject: [PATCH 036/405] Translated using Weblate (Polish) Currently translated at 93.8% (2025 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pl/ --- vector/src/main/res/values-pl/strings.xml | 75 +++++------------------ 1 file changed, 17 insertions(+), 58 deletions(-) diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 408fc207f5..fe98d6b6a1 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -30,7 +30,6 @@ Zaproszenie do pokoju %1$s i %2$s Pusty pokój - ** Nie można odszyfrować: %s ** %s wykonał(a) rozmowę wideo. %s wykonał(a) połączenie głosowe. @@ -46,14 +45,14 @@ Wstępna synchronizacja: \nImportowanie kryptografii Wstępna synchronizacja: -\nImportowanie pokoi +\nImportowanie pokojów Wstępna synchronizacja: \nImportowanie Twoich konwersacji -\nJeśli dołączyłeś(aś) do wielu pokoi, może to zająć dłuższą chwilę +\nJeśli dołączyłeś(aś) do wielu pokojów, może to zająć dłuższą chwilę Wstępna synchronizacja: -\nImportowanie zaproszonych pokoi +\nImportowanie zaproszonych pokojów Wstępna synchronizacja: -\nImportowanie opuszczonych pokoi +\nImportowanie opuszczonych pokojów Wstępna synchronizacja: \nImportowanie grup Wstępna synchronizacja: @@ -74,7 +73,6 @@ Usuń Zmień nazwę Zgłoś treść - i Zaproś Wyloguj się @@ -97,7 +95,6 @@ Tylko kontakty Matrixa Brak wyników Pokoje - Wyślij dzienniki Wyślij dzienniki awarii Wyślij zrzut ekranu @@ -141,7 +138,6 @@ Przychodzące połączenie głosowe W trakcie połączenia… Informacja - ${app_name} wymaga uprawnienia, aby przeprowadzić połączenie audio. TAK NIE @@ -169,7 +165,6 @@ Ignoruj Odcisk palca (%s): Nie można zweryfikować tożsamości serwera. - Szukaj Filtruj członków pokoju Brak wyników @@ -209,7 +204,6 @@ Zaaktualizuj nazwę publiczną Ostatnio widziany(-a) %1$s @ %2$s - Uwierzytelnianie Zalogowany jako Interfejs użytkownika @@ -240,7 +234,6 @@ Laboratorium Znajdują się tu eksperymentalne funkcje, których należy używać z ostrożnością. Ustaw jako główny adres - Motyw Nazwa publiczna ID sesji @@ -283,8 +276,6 @@ Czy jesteś pewien, że chcesz rozpocząć wideorozmowę? Zrób zdjęcie Nagraj film - - Uszkodzony JSON %d zmiana członkostwa @@ -293,15 +284,10 @@ Rozmówca nie połączył się. - - ${app_name} wymaga dostępu do kamery i mikrofonu, aby przeprowadzać rozmowy wideo. \n \nPrzyznaj dostęp w następnym oknie. - Lista uczestników - - 1 członek kilku członków @@ -323,8 +309,6 @@ Certyfikat różni się od tego zaufanego w twoim telefonie. Serwer mógł odnowić certyfikat. Skontaktuj się z administratorem serwera w celu weryfikacji odcisku palca. Certyfikat zmienił stan z zaufanego na niezaufany. jest to NIEZWYKLE RZADKIE. Rekomendowane jest NIE AKCEPTOWANIE nowego certyfikatu. Akceptuj certyfikat tylko wtedy gdy administrator opublikował odcisk palca pasujący do tego powyżej. - - Wszystkie wiadomości Dodaj do ekranu domowego Pokaż informacje o aplikacji w ustawieniach systemu. @@ -363,7 +347,6 @@ Wysyłaj dane analityczne ${app_name} zbiera anonimowe informacje które pozwolą ulepszyć aplikację. Eksportuj klucze E2E pokoju - Importuj klucze E2E pokoju Importuj klucze pokoju Importuj klucze z lokalnego pliku @@ -372,14 +355,13 @@ Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji w tym pokoju z tej sesji. Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. - Wyślij naklejkę Niestety, nie znaleziono zewnętrznej aplikacji, która ukończy to działanie. 1 pokój %d pokoje - %d pokoi - + %d pokojów + %d pokojów 1 aktywny widżet @@ -399,7 +381,6 @@ %d nieprzeczytanych wiadomości powiadomienia - %1$s w %2$s Czy na pewno chcesz usunąć widżet z tego pokoju? Nie można utworzyć widżetu. @@ -436,16 +417,10 @@ Błąd Opuszcza pokój Włącza/Wyłącza markdown - - Kliknij tutaj, aby zobaczyć starsze wiadomości Przepraszamy, wystąpił błąd Alerty systemowe Jeżeli to możliwe, proszę napisz opis w języku angielskim. - - - - 1 zaznaczone %d zaznaczone @@ -603,7 +578,6 @@ Jeżeli nie pamiętasz swoich danych odzystkiwania, możesz %s. Zgubiłeś (-łaś) swój klucz odzyskiwania\? Możesz ustawić nowy w ustawieniach. Kopia zapasowa posiada poprawną sygnaturę z niezweryfikowanej sesji %s - Jesteś na bieżąco! Unieważnij Rozłącz @@ -652,7 +626,6 @@ Niepowodzenie przy pobieraniu wersji kluczy (%s). Usuwanie kopii zapasowej… Sprawdzanie stanu kopii zapasowej - Reakcje Brak sieci. Sprawdź swoje połączenie z Internetem. Proszę czekać… @@ -682,7 +655,6 @@ IGNORUJ UŻYTKOWNIKA Treść zgłoszona Zgłoszone jako spam - Proszę napisz swoją sugestię poniżej. Opisz swoją sugestię tutaj Utwórz nową rozmowę bezpośrednią @@ -712,7 +684,6 @@ Opuść pokój Włącz szyfrowanie end-to-end… Brak - Nie udało się połączyć z serwerem o podanym adresie URL, upewnij się, że wpisano go poprawnie Niektóre rodzaje wiadomości będą ciche (wygenerują powiadomienie bez dźwięku). Weryfikacja Usług Google @@ -761,9 +732,8 @@ \nWpłynie to na użycie baterii i sieci, na panelu powiadomień pozostanie wyświetlone stałe powiadomiene o nasłuchiwaniu zdarzeń. Brak synchronizacji w tle Nie będziesz otrzymywać powiadomień o przychodzących wiadomościach gdy aplikacja będzie działać w tle. - Użyj menedżera integracji aby zarządzać botami, mostami, widżetami i pakietami naklejek. -\nMenedżerzy integracji odbierają dane konfiguracji, modyfikują widżety, wysyłają zaproszenia do pokoi i ustawiają poziomy uprawnień na Twe żądanie. +\nMenedżerzy integracji odbierają dane konfiguracji, modyfikują widżety, wysyłają zaproszenia do pokojów i ustawiają poziomy uprawnień w Twoim imieniu. Pokaż podgląd linków wewnątrz czatu jeśli twój serwer wspiera tę funkcję. Formatuj wiadomości używając składni Markdown zanim zostaną wysłane. Pozwala to na zaawansowane formatowanie takie jak używanie asterysków do wyświetlania tekstu w kursywie. Nie wpływa to na zaproszenia, wyrzucenia oraz bany. @@ -771,7 +741,6 @@ Przycisk enter na klawiaturze programowej wyśle wiadomość zamiast wprowadzania łamanania linii Znajdź Zarządzaj ustawieniami wyszukiwania. - Media Domyślne źródło mediów Odzyskiwanie zaszyforwanych wiadomości @@ -832,7 +801,6 @@ Zrobiłem kopię Zapisz Klucz Odzyskiwania Zapisz jako plik - Kopia zapasowa istnieje już na Twoim serwerze domowym Wygląda na to, iż kopia zapasowa kluczy została skonfigurowana za pomocą innej sesji. Czy chcesz zastąpić ją tą, którą tworzysz\? Generowanie Klucza Odzyskiwania używając hasła, proces może zająć kilka sekund. @@ -861,7 +829,6 @@ Kopiowanie %d kluczy… - Niektóre powiadomienia są wyłączone w osobistej konfiguracji. Usługi Google Play są aktualne. Rozumiem @@ -930,7 +897,6 @@ Zawartość została zgłoszona jako niewłaściwa. \n \nJeżeli nie chcesz widzieć treści od tego użytkownika, możesz go zablokować aby ukryć jego wiadomości. - Opuść pokój %1$s nie dokona(-ła) zmian Wysyła wiadomość jako spoiler @@ -1109,7 +1075,6 @@ Zweryfikuj %s Zweryfikowano %s Oczekiwanie na %s… - Wiadomości w tym pokoju są zaszyfrowane end-to-end. \n \nTwoje wiadomości są zabezpieczone zamkami i jedynie Ty oraz Twój odbiorca posiadacie klucze, aby je odblokować. @@ -1222,7 +1187,7 @@ Szyfrowanie miniatury… Nie możesz czegoś odnaleźć\? Utwórz nowy pokój - Otwórz katalog pokoi + Otwórz katalog pokojów Nazwa lub ID (#przykład:matrix.org) Serwer toższamości Odłącz od serwera tożsamości @@ -1345,7 +1310,6 @@ Jeżeli teraz przerwiesz, możesz utracić zaszyfrowane wiadomości oraz dane jeżeli utracisz dostęp do zalogowanych sesji. \n \nMożesz także ustawić Secure Backup i zarządzać swoimi kluczami w Ustawieniach. - Wydrukuj go i schowaj w bezpiecznym miejscu Twój %2$s i %1$s są teraz ustawione. \n @@ -1457,7 +1421,6 @@ Dodaj obraz z Niepoprawny kod weryfikacyjny. Kod - Udziel zgody Wycofaj moją zgodę Udzieliłeś zgody na wysłanie adresów e-mail oraz numerów telefonów do tego serwera tożsamości w celu odkrycia innych użytkowników z Twoich kontaktów. @@ -1508,7 +1471,7 @@ %d zablokowanych użytkowników %d zablokowanych użytkowników - Opublikować ten pokój dla wszystkich w katalogu pokoi %1$s\? + Opublikować ten pokój dla wszystkich w katalogu pokojów %1$s\? Cofnij publikację tego adresu Opublikuj ten adres Dodaj adres lokalny @@ -1526,7 +1489,7 @@ To jest główny adres Opublikowane adresy mogą zostać wykorzystane przez kogokolwiek na dowolnym serwerze do dołączenia do Twojego pokoju. Żeby opublikować adres musi być on najpierw ustawiony jako adres lokalny. Opublikowane adresy - Obejrzyj i zarządzaj adresami tego pokoju oraz jego widocznością w katalogu pokoi. + Obejrzyj i zarządzaj adresami tego pokoju oraz jego widocznością w katalogu pokojów. Adres pokoju Dostęp do pokoju Zmiany dotyczące tego kto może czytać historię zostaną zastosowane wyłącznie do przyszłych wiadomości w tym pokoju. Widoczności już istniejącej historii pozostanie niezmieniona. @@ -1557,7 +1520,7 @@ Anuluj zaproszenie Zaprzestanie ignorowania użytkownika spowoduje ponowne wyświetlanie wszystkich jego wiadomości. Przestań ignorować użytkownika - Zignorowanie tego użytkownika usunie wszystkie jego wiadomości z pokoi, które współdzielicie. + Zignorowanie tego użytkownika usunie wszystkie jego wiadomości z pokojów, które współdzielicie. \n \nOperacja ta może zostać cofnięta w dowolnej chwili poprzez ustawienia ogólne. Ignoruj użytkownika @@ -1895,7 +1858,7 @@ Adres URL serwera domowego Wyślij historię żądań udostępnienia klucza Przestrzenie - Katalog pokoi + Katalog pokojów Sugerowane pokoje Nowa wartość Przełącz @@ -1984,7 +1947,7 @@ Niektóre pokoje mogą być ukryte, gdyż są prywatne i wymagają od Ciebie zaproszenia. Niektóre pokoje mogą być ukryte, gdyż są prywatne i wymagają od Ciebie zaproszenia. \nNie masz uprawnień aby dodawać pokoje. - W tej Przestrzeni nie ma żadnych pokoi + W tej Przestrzeni nie ma żadnych pokojów Proszę skontaktować się z administratorem Twojego serwera domowego aby uzyskać więcej informacji Wygląda na to, że Twój serwer domowy jeszcze nie obsługuje Przestrzeni Lubisz eksperymentować\? @@ -2006,7 +1969,7 @@ Dodaj istniejące pokoje i przestrzenie Wybierz aby opuścić Opuść wybrane pokoje i przestrzenie… - Nie opuszczaj żadnych pokoi i przestrzeni + Nie opuszczaj żadnych pokojów i przestrzeni Opuść wszystkie pokoje i przestrzenie Jesteś jedynym administratorem tej przestrzeni. Opuszczenie jej oznacza brak kontroli nad nią. Nie będziesz w stanie ponownie dołączyć, do momentu kiedy nie zostaniesz ponownie zaproszony. @@ -2051,7 +2014,7 @@ Publiczna Prywatna przestrzeń dla Ciebie i Twoich znajomych Ja i moi znajomi - Prywatna przestrzeń do organizacji Twoich pokoi + Prywatna przestrzeń do organizacji Twoich pokojów Tylko ja Upewnij się, że odpowiednie osoby mają dostęp do %s. Możesz zmienić to później. Z kim pracujesz\? @@ -2132,7 +2095,7 @@ Opuścić obecną konferencję i przejść do innej\? Wystąpił błąd podczas próby dołączenia do konferencji Ten serwer znajduje się już na liście - Nie można odnaleźć tego serwera lub jego listy pokoi + Nie można odnaleźć tego serwera lub jego listy pokojów Wprowadź nazwę nowego serwera, który chcesz odkrywać. Dodaj nowy serwer Twój serwer @@ -2149,7 +2112,7 @@ wyproszenie użytkownika zaskutkuje usunięciem go z tej przestrzeni. \n \nAby uniemożliwić mu ponowne dołączenie, należy go zbanować. - Pokaż wszystkie pokoje w katalogu pokoi (również te zawierające treści dla dorosłych). + Pokaż wszystkie pokoje w katalogu pokojów (również te zawierające treści dla dorosłych). Pokaż pokoje z treścią dla dorosłych %1$s zmienił(a) alternatywne adresy dla tego pokoju. Dodano %1$s i usunięto %2$s jako adresy tego pokoju. @@ -2190,7 +2153,6 @@ Ustawienia pokoju Ankieta Plik jest zbyt duży, aby go przesłać. - Wyślij maile i numery telefonów do %s Twoje kontakty są prywatne. Aby odnaleźć użytkowników z Twoich kontaktów, potrzebujemy zgody do wysłania informacji o nich na Twój serwer tożsamości. Bezpieczna kopia @@ -2273,7 +2235,6 @@ Aktualizuj pokój prywatny Aktualizuj pokój publiczny Dołącz do pokoju zastępczego - Dodaj kilka szczegółów, aby ułatwić ludziom identyfikację. Możesz je zmienić w dowolnym momencie. Dodaj kilka szczegółów, aby się wyróżnić. Możesz je zmienić w dowolnym momencie. Twoja przestrzeń prywatna @@ -2336,7 +2297,6 @@ %1$d aktywnych połączeń · %1$d aktywnych połączeń · - Zgadzasz się na wysłanie tych informacji\? Ustawienia systemu Wersje @@ -2371,7 +2331,6 @@ Niedostępny Dostępny Pokaż znaczniki odczytania - Odkrywanie (%s) Dokończ konfigurację odkrywania. Usuń wszystkie nieudane wiadomości From abfd9b8fdcc4ce95aaf196d2d497c46c6586aa20 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 1 Mar 2022 23:41:47 +0000 Subject: [PATCH 037/405] Translated using Weblate (Slovak) Currently translated at 98.9% (2134 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- vector/src/main/res/values-sk/strings.xml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 18af56a598..6ec0827757 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -658,7 +658,7 @@ Ignorovať Ste si istí, že sa chcete odhlásiť\? Označiť ako prečítané - Prihlásiť sa použitím jediného prihlásenia + Prihlásiť sa použitím jednotného prihlásenia SSO Pokročilé nastavenia oznámení Dôležitosť oznámení pre udalosti Vlastné nastavenia. @@ -1650,8 +1650,8 @@ Vlastné (%1$d) v %2$s Predvolené v %1$s Overiť porovnaním emotikonov - Namiesto toho overte porovnaním emoji - Ak nie ste osobne, porovnajte namiesto toho emoji + Namiesto toho overte porovnaním emotikonov + Ak nie ste osobne, porovnajte namiesto toho emotikony Skenovať pomocou tohto zariadenia Naskenujte kód pomocou iného zariadenia alebo prepnite a skenujte pomocou tohto zariadenia Naskenujte kód pomocou zariadenia druhého používateľa, aby ste sa navzájom bezpečne overili @@ -2017,7 +2017,7 @@ Vyberte si prosím heslo. Vyberte si prosím používateľské meno. Nepodarilo sa nastaviť krížové podpisovanie - Interaktívne overte pomocou emoji + Interaktívne overte pomocou emotikonov Manuálne overte pomocou textu Overte nové prihlásenie s prístupom k vášmu účtu: %1$s Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné @@ -2115,7 +2115,7 @@ Krížové podpisovanie nie je povolené Vaša nová relácia je teraz overená. Má prístup k vašim šifrovaným správam a ostatní používatelia ju uvidia ako dôveryhodnú. Porovnajte kód s kódom zobrazeným na obrazovke druhého používateľa. - Porovnajte jedinečné emoji a uistite sa, že sú zobrazené v rovnakom poradí. + Porovnajte jedinečné emotikony a uistite sa, že sú zobrazené v rovnakom poradí. Aby ste si boli istý, urobte to osobne alebo použite iný dôveryhodný spôsob komunikácie. Ak chcete byť v bezpečí, overte %s pomocou jednorazového kódu. Po zapnutí šifrovania pre miestnosť ho už nemožno vypnúť. Správy odoslané v zašifrovanej miestnosti nemôže vidieť server, iba účastníci miestnosti. Zapnutie šifrovania môže zabrániť správnemu fungovaniu mnohých botov a premostení. @@ -2324,8 +2324,8 @@ Kto má prístup\? Zmeny týkajúce sa toho, kto môže čítať históriu, sa budú vzťahovať len na budúce správy v tejto miestnosti. Viditeľnosť existujúcej histórie zostane nezmenená. Tento server neposkytuje žiadne zásady a pravidlá. - Pridať tlačidlo do editora správ na otvorenie klávesnice emodži - Zobraziť klávesnicu emodži + Pridať tlačidlo do editora správ na otvorenie klávesnice emotikonov + Zobraziť klávesnicu emotikonov Použite príkaz /confetti alebo pošlite správu obsahujúcu ❄️ alebo 🎉 Zobraziť efekty konverzácie Relácia bola odhlásená! @@ -2437,4 +2437,6 @@ %1$s, %2$s a ďalší %1$s a %2$s + Pokračovať pomocou jednotného prihlásenia SSO + jednotné prihlásenie SSO \ No newline at end of file From a251545e904cf0f0e3e43ba727e3c77f5adb8799 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 2 Mar 2022 09:39:16 +0000 Subject: [PATCH 038/405] Translated using Weblate (Albanian) Currently translated at 99.4% (2145 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- vector/src/main/res/values-sq/strings.xml | 57 ++++++----------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index c363b8e4e9..48fc274eb7 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -39,7 +39,6 @@ %1$s hoqi emrin e tij në ekran (%2$s) %1$s hoqi temën e dhomës %1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë - %s e përmirësoi këtë dhomë. Njëkohësimi fillestar: \nPo importohet llogaria… @@ -315,10 +314,6 @@ Hidheni tej Anëtarë liste Hidhu te të palexuarit - - - - Dilni nga dhoma Jeni i sigurt se doni të dilni nga dhoma? FJALOSJE TË DREJTPËRDREJTA @@ -501,8 +496,6 @@ Kreu Dhoma I ftuar - - Jeni përzënë prej %1$s nga %2$s Jeni dëbuar prej %1$s nga %2$s Arsye: %1$s @@ -520,7 +513,6 @@ %1$s: %2$s %d+ Po përgjohet për akte - Të parapëlqyer Ju lutemi, përshkruajeni të metën. Ç’po bënit? Ç’prisnit të ndodhte? Ç’ndodhi në fakt? Duket se po përplasni telefonin nga inati. Do të donit të hapej skena për njoftim të metash? @@ -528,17 +520,12 @@ Dështoi dërgimi i njoftimit të të metës (%s) Dilni Dërgoni mesazh zanor - Verifikimi i adresës email dështoi: sigurohuni se keni klikuar lidhjen te email-i - Ju lutemi, niseni ${app_name}-in në një tjetër pajisje që mund të shfshehtëzojë mesazhin, që kështu të mund të dërgojë kyçet te ky sesion. - %1$s & %2$s & të tjerë po shtypin… version olm Fikso dhomat me njoftime të humbura Vetëm anëtarët (që nga çasti i përzgjedhjes së kësaj mundësie) - - Përdor kamerë të brendshme Shtuat një sesion të ri \'%s\', i cili po kërkon kyçe fshehtëzimi. Sesioni juaj i paverifikuar \'%s\' po kërkon kyçe fshehtëzimi. @@ -551,7 +538,6 @@ Ju lutemi, %s për ta zmadhuar këtë kufi. Ju lutemi, %s që të vazhdoni përdorimin e këtij shërbimi. ose - Në qoftë e mundur, ju lutemi, përshkrimin shkruajeni në anglisht. Hëpërhë, s’keni të aktivizuar ndonjë pako ngjitësash. \n @@ -565,16 +551,11 @@ Po bëhet lidhja e thirrjes… Ana e largët dështoi të përgjigjet. - - Për të kryer thirrje audio, ${app_name}-i lyp leje të përdorë mikrofonin tuaj. - Për të kryer thirrje video, ${app_name}-i lyp leje të përdorë kamerën dhe mikrofonin tuaj. \n \nJu lutemi, lejoni përdorimin, që nga flluskat pasuese, që të jetë në gjendje të bëjë thirrjen. - JO - %d anëtar %d anëtarë @@ -585,14 +566,9 @@ %d mesazh i ri %d mesazhe të rinj - - - - Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet. Këto janë veçori eksperimentale që mund të ngecin në rrugë të papritura. Përdorini me kujdes. Ju lutemi, krijoni një frazëkalim për fshehtëzimin e kyçeve të eksportuar. Që të jeni në gjendje t’i importoni kyçet, do t’ju duhet të jepni të njëjtin frazëkalim. - Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar. Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër: Nëse s’përputhen, siguria e komunikimeve tuaja mund të jetë komprometuar. @@ -639,7 +615,6 @@ %d mesazh i njoftuar i palexuar %d mesazhe të njoftuar të palexuar - Dërgoni një ngjitës Dërgoni një ngjitës Thirrje @@ -700,7 +675,6 @@ ${app_name}-i nuk preket nga Optimizime Baterie. Nëse një përdorues e lë një pajisje jo në prizë dhe të palëvizshme për një periudhë, me ekranin të fikur, pajisja kalon nën mënyrën Dremitje. Kjo u parandalon aplikimeve të hyjnë në rrjet dhe shtyn për më vonë punët e tyre, njëkohësimet dhe alarmet standarde. Shpërfille Optimizimin - S’u gjet APK për Google Play Services. Njoftimet mund të mos punojnë saktë. Thirrje Video Në Kryerje e Sipër… Kopjeruajtje Kyçesh @@ -737,7 +711,6 @@ U bë Ruani Kyç Rimarrjesh Ruaje si Skedë - Ju lutemi, bëni një kopje Jepjani kyçin e rimarrjeve… Po prodhohet Kyç Rimarrjesh duke përdorur frazëkalim, ky proces mund të hajë disa sekonda. @@ -831,8 +804,6 @@ Po shkarkohen kyçe… Po importohen kyçe… Që të përdorni Kopjeruajtje Kyçesh në këtë sesion, rikthejeni tani përmes frazëkalimit tuaj ose kyçi rimarrjesh. - - Media Ngjeshje parazgjedhje Zgjidhni @@ -869,8 +840,6 @@ Shpërfille U verifikua! E mora vesh - - Kërkesë Verifikimi %s dëshiron të verifikojë sesionin tuaj Gabim i Panjohur @@ -965,7 +934,6 @@ Asnjë Shfuqizoje Shkëputu - S’kapet dot shërbyes Home te kjo URL, ju lutemi, kontrollojeni Mënyrë Njëkohësimi Në Prapaskenë E optimizuar për baterinë @@ -976,7 +944,6 @@ \nKjo do të ketë ndikim mbi përdorimin e baterisë dhe të transmetimit, do të shfaqet një njoftim i pandërprerë që pohon se ${app_name}-i po përgjon për akte. Pa njëkohësim në prapraskenë S’do të njoftoheni për mesazhe ardhës, kur aplikacioni gjendet në prapaskenë. - Administroni rregullimet tuaja për zbulime. S’po përdorni ndonjë shërbyes identitetesh Duket se po rrekeni të lidheni me një tjetër shërbyes Home. Doni të bëhet dalja\? @@ -1045,7 +1012,6 @@ Kjo lëndë është raportuar si e papërshtatshme. \n \nNëse s’doni të shihni më lëndë nga ky përdorues, mund ta shpërfillni, që të fshihen mesazhet e tij. - Integrime Përdorni një përgjegjës integrimesh që të administroni robotë, ura, widget-e dhe paketa ngjitësish. \nPërgjegjësit e integrimeve marrin të dhëna formësimi dhe mund të ndryshojnë widget-e, të dërgojnë ftesa për në dhoma dhe të caktojnë shkallë pushteti në emrin tuaj. @@ -1324,7 +1290,6 @@ %s u pranua Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin Nëse s’jeni vetë atje, krahasoni emoji-n - ${app_name} nuk trajton akte të llojit \'%1$s\' ${app_name} ndeshi një problem kur vizatohej lëndë e aktit me ID \'%1$s\' Ky sesion s’është në gjendje të ndajë këtë verifikim me sesionet tuaj të tjerë. @@ -1407,7 +1372,6 @@ Shtypeni dhe ruajeni diku në një vend të parrezik Ruajeni në një diskth USB ose pajisje kopjeruajtjesh Kopjojeni te depozita juaj personale në re - Fshehtëzimi u aktivizua Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj. Mësoni më tepër & verifikoni përdorues te profilet e tyre. Fshehtëzim jo i aktivizuar @@ -1811,7 +1775,6 @@ Fshihi të mëtejshmet Shfaq të mëtejshme %1$d nga %2$d - Jepe pranimin Shfuqizoje pranimin tim Keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. @@ -1863,8 +1826,6 @@ Shpërngule Lidhe Konsultohu së pari - - Thirrje aktive (%1$s) Pati një gabim gjatë kërkimit të numrit të telefonit Pjesa e numrave @@ -2028,7 +1989,7 @@ Unë dhe anëtarët e ekipit tim Një hapësirë private për të sistemuar dhomat tuaja Unë vetëm - Siguroni që personat e duhur të kenë hyrje te %s. Këtë mund të ndryshoni më vonë. + Siguroni që personat e duhur të kenë hyrje te %s. Me cilët po punoni\? Që të hyni në një hapësirë ekzistuese, ju duhet një ftesë. Këtë mund ta ndryshoni më vonë @@ -2102,7 +2063,6 @@ Jepni emrin e e një shërbyesi të ri që doni të eksploroni. Shtoni shërbyes të ri Shërbyesi juaj - Na ndjeni, ndodhi një gabim teksa provohej të hyhej: %s Adresë hapësire Adresa hapësire @@ -2302,7 +2262,6 @@ Krijoni Pyetësor A pranoni të dërgohen këto hollësi\? Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (email-e dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi. - Dërgo email-e dhe numra telefonash te %s Kontaktet tuaja janë private. Për të zbuluar përdorues prej kontakteve tuaja, na duhet leja juaj për të dërguar hollësi kontakti te shërbyesi juaj i identiteteve. Është bërë dalja nga sesioni! @@ -2435,4 +2394,18 @@ Kopjoje lidhjen te rrjedha Shiheni në dhomë Shihni Rrjedha + %1$s dhe %2$s + %1$s, %2$s dhe të tjerë + + %1$d më tepër + %1$d më tepër + + Njofto krejt dhomën + Përdorues + + %d ndryshim ACL-sh shërbyesi + %d ndryshimë ACL-sh shërbyesi + + Njoftim dhome + Shfaq më pak \ No newline at end of file From 0b6af0f2b290fee831ccf23316476b532b98de17 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 1 Mar 2022 02:27:26 +0000 Subject: [PATCH 039/405] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- vector/src/main/res/values-zh-rTW/strings.xml | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 060b739305..564a4ccf8a 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -39,7 +39,6 @@ 聊天室邀請 %1$s 和 %2$s 空聊天室 - 初始化同步: \n正在匯入帳號…… 初始化同步: @@ -243,7 +242,6 @@ 刪除 重新命名 回報內容 - 邀請 登出 @@ -266,7 +264,6 @@ 僅 Matrix 聯絡人 沒有結果 聊天室 - 社群 傳送記錄 傳送當機紀錄 @@ -300,11 +297,9 @@ 這看起不像是有效的電子郵件地址 此電子郵件位址已經被定義。 忘記密碼? - 這個家伺服器想要確定您不是機器人 必須輸入和您帳號關聯的電子郵件地址。 電子郵件地址驗證失敗: 請確保您已點擊郵件中的連結 - 請輸入有效的網址 異常的 JSON 沒有包含有效的 JSON @@ -321,14 +316,10 @@ 通話進行中…… 遠端未能接聽。 資訊 - - ${app_name} 需要權限來存取麥克風,來撥打語音通話。 - ${app_name} 需要權限來存取相機及麥克風來撥打視訊通話。 \n \n為了可以正常使用通話功能,請在下個彈跳視窗中允許存取。 - 繼續 @@ -337,8 +328,6 @@ 拒絕 列出成員 跳到未讀 - - %d 個成員 @@ -385,12 +374,9 @@ 請在另一個可以解密訊息的裝置上啟動 ${app_name},以便它將金鑰發送到此工作階段。 憑證已從以前受信任的更改為不受信任的憑證。伺服器可能已續訂其憑證。請與伺服器管理員聯繫以尋找所需的指紋。 僅當伺服器管理員發佈的指紋與上面的指紋匹配時才接受此憑證。 - 搜尋 過濾聊天室成員 沒有結果 - - 所有訊息 新增到主畫面 個人檔案圖片 @@ -449,7 +435,6 @@ 更新公開名稱 上次上線 %1$s @ %2$s - 授權 登入爲 家伺服器 @@ -488,7 +473,6 @@ 這些是可能以非預期的方式壞掉的實驗性功能。請小心服用。 設定為主要地址 取消設定為主要地址 - 主題 解密錯誤 公開名稱 @@ -500,7 +484,6 @@ 匯出 輸入通關密語 確認通關密語 - 匯入聊天室端到端加密密鑰 匯入聊天室金鑰 從本機檔案匯入金鑰 @@ -512,7 +495,6 @@ 驗證 透過將以下內容與您的其他工作階段中的使用者設定來確認: 如果不符合的話,您的通訊安全可能正受到威脅。 - 選擇一個聊天室目錄 伺服器名稱 在 %s 伺服器上的所有聊天室 @@ -520,7 +502,6 @@ %d 條未讀的已通知訊息 - %d 個聊天室 @@ -594,15 +575,9 @@ 對話在此繼續 這個聊天示是其他對話的延續 點選這裡以檢視更舊的訊息 - - - - %d 已選取 - - 系統警告 聯絡您的服務管理員 此家伺服器已經超過其中一項資源限制,所以有一些使用者將會無法登入 @@ -694,7 +669,6 @@ ${app_name} 不會被電池最佳化影響。 如果使用者不為裝置充電,並讓其靜置一段時間,且將螢幕關閉,裝置將會進入 Doze 模式。這可能會導致應用程式無法存取網路,並延遲它們的工作、同步與標準警報。 忽略最佳化 - 找不到有效的 Google Play 服務 APK。通知可能無法正常運作。 視訊通話進行中…… 金鑰備份 @@ -731,7 +705,6 @@ 完成 儲存復原金鑰 儲存為檔案 - 請複製 分享復原金鑰與…… 正在使用通關密語生成復原金鑰,這個過程可能需要數秒鐘。 @@ -814,7 +787,6 @@ 演算法 簽章 要在此工作階段上使用金鑰備份,現在就使用您的通關密語或復原金鑰復原。 - 正在計算復原金鑰…… 正在下載金鑰…… 正在匯出金鑰…… @@ -823,7 +795,6 @@ 使用 Enter 傳送訊息 軟體鍵盤的 Enter 按鈕將會傳送訊息而非換行 密碼無效 - 媒體 預設壓縮 選擇 @@ -858,8 +829,6 @@ 忽略 已驗證! 知道了 - - 驗證請求 %s 想要驗證您的工作階段 未知錯誤 @@ -956,7 +925,6 @@ 撤銷 斷線 - 無法在此 URL 找到家伺服器,請檢查 背景同步模式 為電池最佳化 @@ -967,7 +935,6 @@ \n這會影響到網路與電池的使用,並會顯示指出 ${app_name} 正在監聽某事件的永久通知。 無背景同步 當應用程式在背景時,您將不會收到訊息通知。 - 探索 管理您的探索設定。 您未使用任何身份識別伺服器 @@ -1036,7 +1003,6 @@ 此內容已被回報為不合適。 \n \n如果您不想要看到從此使用者而來的更多內容,您可以忽略他們以隱藏他們的訊息。 - 整合 使用整合管理員以管理機器人、橋接、小工具與貼紙包。 \n整合管理員可以代表您接收設定資料,調整小工具、傳送聊天室邀請並設定權力等級。 @@ -1251,7 +1217,6 @@ 驗證 %s 已驗證 %s 正在等待驗證 %s…… - 此聊天室中的訊息未端到端加密。 此聊天室中的訊息有端到端加密。 \n @@ -1394,7 +1359,6 @@ 列印並將其存放在安全的地方 將其儲存在 USB 隨身碟或備份磁碟上 將其複製到您的私人雲端儲存空間 - 加密已啟用 在此聊天室中的訊息已端到端加密。取得更多資訊並在使用者的個人檔案中驗證他們。 加密未啟用 @@ -1789,7 +1753,6 @@ 隱藏進階 顯示進階 %2$d 中的 %1$d - 給予同意 撤銷我的同意 您已同意傳送電子郵件與電話號碼到此身份提供者以從您的聯絡人中探索其他使用者。 @@ -1880,8 +1843,6 @@ 轉移 連線 先諮詢 - - 通話中 (%1$s) 查詢電話號碼時發生錯誤 撥號鍵盤 @@ -2075,7 +2036,6 @@ 輸入您想要探索的新伺服器名稱。 加入新的伺服器 您的伺服器 - 抱歉,試圖加入時發生錯誤:%s 空間地址 檢視與管理此空間的地址。 @@ -2272,7 +2232,6 @@ 投票問題或主題 建立投票 投票 - 向 %s 傳送電子郵件與電話號碼 您的通訊錄是私人的。要從您的通訊錄中探索使用者,我們需要您的權限來傳送聯絡人資訊到您的身份識別伺服器。 已登出工作階段! @@ -2411,4 +2370,6 @@ %d 伺服器 ACL 變更 + %1$s 與 %2$s + %1$s、%2$s 與其他人 \ No newline at end of file From cc7545b31fe1900f44b5d33cb4e6246afce3ee62 Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 1 Mar 2022 15:36:02 +0000 Subject: [PATCH 040/405] Translated using Weblate (German) Currently translated at 100.0% (51 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40104000.txt | 2 ++ fastlane/metadata/android/de-DE/changelogs/40104020.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40104000.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/40104020.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40104000.txt b/fastlane/metadata/android/de-DE/changelogs/40104000.txt new file mode 100644 index 0000000000..37de3cb4a2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Neues: Erstelle Threads, damit dein Chatverlauf nicht zugespammt wird. Nachrichtenblasen. +Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/de-DE/changelogs/40104020.txt b/fastlane/metadata/android/de-DE/changelogs/40104020.txt new file mode 100644 index 0000000000..6693401a24 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Neues: Unterstützung für @room, Verbesserungen der Abstimmungen und weitere kleine Änderungen +Ganzer Changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.2 From ee8e369294d84460cf59fb1787d20882d78936dc Mon Sep 17 00:00:00 2001 From: Glandos Date: Mon, 28 Feb 2022 19:52:43 +0000 Subject: [PATCH 041/405] Translated using Weblate (French) Currently translated at 100.0% (51 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr-FR/changelogs/40103170.txt | 2 ++ fastlane/metadata/android/fr-FR/changelogs/40103180.txt | 2 ++ fastlane/metadata/android/fr-FR/changelogs/40104000.txt | 2 ++ fastlane/metadata/android/fr-FR/changelogs/40104020.txt | 2 ++ 4 files changed, 8 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40103170.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40103180.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40104000.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40104020.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103170.txt b/fastlane/metadata/android/fr-FR/changelogs/40103170.txt new file mode 100644 index 0000000000..c264ea3703 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103170.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.17 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103180.txt b/fastlane/metadata/android/fr-FR/changelogs/40103180.txt new file mode 100644 index 0000000000..0b8a9542a5 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103180.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.18 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104000.txt b/fastlane/metadata/android/fr-FR/changelogs/40104000.txt new file mode 100644 index 0000000000..eaced9e3f4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Implémentation initial des fils de discussion. Bulles de messages. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104020.txt b/fastlane/metadata/android/fr-FR/changelogs/40104020.txt new file mode 100644 index 0000000000..068b4aac43 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Ajout du support pour @room et des sondages non terminé parmi plein d'autres changements mineurs. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.2 From 6e805beeedc41f9a0fdf7c94d21ae4009955f134 Mon Sep 17 00:00:00 2001 From: SPiRiT Date: Wed, 2 Mar 2022 21:02:50 +0000 Subject: [PATCH 042/405] Translated using Weblate (Hebrew) Currently translated at 81.2% (1753 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/he/ --- vector/src/main/res/values-iw/strings.xml | 95 +++++++---------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/vector/src/main/res/values-iw/strings.xml b/vector/src/main/res/values-iw/strings.xml index 1fea4fb060..bc0ab0ff34 100644 --- a/vector/src/main/res/values-iw/strings.xml +++ b/vector/src/main/res/values-iw/strings.xml @@ -82,7 +82,6 @@ ערכת נושא שחורה ערכת נושא כהה ערכת נושא בהירה - חדרים אנשי קשר מטריקס בלבד שיחות @@ -125,11 +124,9 @@ זו אינה כתובת שרת מטריקס חוקית אנא הכנס כתובת תקינה אנא עיין וקבל את המדיניות של שרת בית זה: - אימות כתובת הדוא\"ל נכשל: ודא שלחצת על הקישור בדוא\"ל יש להזין את כתובת הדוא\"ל המקושרת לחשבונך. שרת הבית רוצה לוודא שאתה לא רובוט - שכחת סיסמה\? מספר הטלפון הזה כבר קיים ומעודכן במערכת. כתובת הדוא\"ל הזו כבר קיימת ומוגדרת במערכת. @@ -162,7 +159,6 @@ שיחת האלמנט נכשלה האם אתה בטוח שברצונך להתחיל שיחת וידאו\? האם אתה בטוח שברצונך להתחיל שיחה קולית\? - עבור להודעה שלא נקראה חברי רשימה דחייה @@ -173,14 +169,10 @@ כן אפשר הרשאה לגשת לאנשי הקשר שלך. כדי לסרוק קוד QR, עליך לאפשר גישה למצלמה. - אלמנט זקוק להרשאה כדי לגשת למצלמה ולמיקרופון שלך כדי לבצע שיחות וידאו. \n \nאנא אפשר גישה בחלונות הקופצים הבאים כדי להיות מסוגל לבצע את השיחה. - אלמנט זקוק להרשאה כדי לגשת למיקרופון שלך כדי לבצע שיחות שמע. - - מידע הצד המרוחק לא הצליח להרים. שיחת וידאו מתבצעת … @@ -235,7 +227,6 @@ %d שניות עיכוב בין כל סינכרון - פסק-זמן לבקשה לסנכרון החל באתחול לא תקבל הודעה על הודעות נכנסות כאשר האפליקציה ברקע. @@ -310,7 +301,6 @@ סנן משתמשים מודרים סנן חברים מהחדר חפש - %d נבחר %d נבחרים @@ -410,20 +400,14 @@ תמונת פרופיל הוסף למסך הבית כל ההודעות - האם ברצונך לעזוב את החדר\? עזוב חדר - - - - %d חבר %d חברים %d חברים %d חברים - התחל אימות מושב לא מאומת מבקש מפתחות הצפנה. \nשם מושב: %1$s @@ -523,7 +507,6 @@ %d חדרים מועטים %d חדרים אחרים - הודעת התראה %d שלא נקראה %d הודעות התראה שלא נקראו @@ -534,7 +517,6 @@ כל החדרים בשרת %s כתובת אתר של שרת בית בחר מדריך חדרים - אם הם לא תואמים, אבטחת התקשורת שלך עלולה להיפגע. אשרו על ידי השוואה בין הדברים הבאים להגדרות המשתמש בפגישה האחרת שלכם: אמת @@ -556,7 +538,6 @@ נהל גיבוי מפתח שחזור הודעות מוצפנות מפתחות יוצאו בהצלחה - אנא צור משפט סיסמה להצפנת המפתחות המיוצאים. יהיה עליך להזין את אותו ביטוי סיסמה כדי שתוכל לייבא את המפתחות. יצא יצא מפתחות לקובץ מקומי @@ -567,7 +548,6 @@ שם ציבורי שגיאת פענוח ערכת נושא - ביטול ההגדרה ככתובת הראשית הגדר ככתובת ראשית אלה תכונות ניסיוניות שעשויות להישבר בדרכים לא צפויות. השתמש בזהירות. @@ -690,8 +670,6 @@ סיבה: %1$s חסום על ידי %2$s מ- %1$s אתה נבעט מ- %1$s על ידי %2$s - - הוזמנו חדרים בית @@ -748,7 +726,6 @@ לעולם אל תאבד הודעות מוצפנות להגן מפני אובדן גישה להודעות ונתונים מוצפנים גיבוי מאובטח - מחק גיבוי למחוק את מפתחות ההצפנה המגובים שלך מהשרת\? לא תוכל עוד להשתמש במפתח השחזור שלך כדי לקרוא את היסטוריית ההודעות המוצפנת. מחק את הגיבוי בודק מצב גיבוי @@ -794,7 +771,6 @@ נראה שכבר יש לך גיבוי מפתח הגדרה מהפעלה אחרת. האם אתה רוצה להחליף אותו לזה שאתה יוצר\? גיבוי כבר קיים בשרת הבית שלך מפתח השחזור נשמר. - שמירת קובץ בשם שיתוף שמור מפתח שחזור @@ -998,7 +974,6 @@ כל ההודעות כל ההודעות (רועשות) התעלם ממשתמש זה - תוכן זה דווח כבלתי הולם. \n \nאם אינך רוצה לראות תוכן נוסף ממשתמש זה, תוכל להתעלם ממנו כדי להסתיר את ההודעות שלו. @@ -1058,7 +1033,6 @@ אנא הזן את כתובת ה- URL של שרת הזהות לא ניתן היה להתחבר לשרת זהות הזן כתובת אתר של שרת זהות - תן הסכמה בטל את הסכמתי נתת את הסכמתך לשלוח מיילים ומספרי טלפון לשרת זהות זה כדי לגלות משתמשים אחרים מאנשי הקשר שלך. @@ -1157,7 +1131,6 @@ \nההודעות שלך מאובטחות במנעולים ורק לך ולמקבל יש את המפתחות הייחודיים לפתיחתם. ההודעות כאן אינן מוצפנות מקצה לקצה. הודעות בחדר זה אינן מוצפנות מקצה לקצה. - ממתין ל- %s… %s מאומת אמת. את %s @@ -1219,11 +1192,8 @@ שגיאה לא ידועה %s רוצה לאמת את ההפעלה שלך בקשת אימות - - הבנתי מאומת! - חתימה אלגוריתם גירסה @@ -1253,7 +1223,6 @@ הצג תוכן בהתראות קוד PIN הוא הדרך היחידה לפתוח את Element. אפשר ביומטריה ספציפית למכשירים, כמו טביעות אצבע וזיהוי פנים. - גיבוי מאובטח הוסף לחצן במלחין ההודעות כדי לפתוח מקלדת אימוג\'י הצג מקלדת אימוג\'י @@ -1284,11 +1253,9 @@ צפה ועדכן את התפקידים הנדרשים לשינוי חלקים שונים בחדר. הרשאות חדרים חדר זה אינו ציבורי. לא תוכל להצטרף שוב ללא הזמנה. - (%%%s)התקדמות אנשים מועדפים - ברירת מחדל מערכת קפצו לקבלת קריאה הודעה ישירה @@ -1370,7 +1337,6 @@ שרת ביתי מחובר כ הזדהות - נראה לאחרונה עדכן שם ציבורי שם ציבורי @@ -1378,7 +1344,6 @@ אלמנט אוסף ניתוח אנונימי כדי לאפשר לנו לשפר את היישום. שלח נתוני ניתוח ניתוח נתונים - נהל את הגדרות הגילוי שלך. תַגלִית הפוך את המשתמש שלי ללא פעיל @@ -1568,7 +1533,6 @@ אם תבטל עכשיו, אתה עלול לאבד הודעות ונתונים מוצפנים אם תאבד את הגישה לכניסות שלך. \n \nאתה יכול גם להגדיר גיבוי מאובטח ולנהל את המפתחות שלך בהגדרות. - העתק אותו לאחסון הענן האישי שלך שמור אותו במפתח USB או בכונן גיבוי הדפיסו ואחסנו במקום בטוח @@ -1731,55 +1695,55 @@ סיימת את השיחה. %s סיים את השיחה. ענית לשיחה. - %s שנה לשיחה. + %s ענה לשיחה. שלחת מידע לביצוע שיחה. %s שלח מידע לביצוע שיחה. ביצעת שיחת אודיו. - %s ביצע שיחת אודיו. + %s ביצע/ה שיחה קולית. ביצעת שיחת וידאו. - %s ביצע שיחת וידאו . + %s ביצע/ה שיחת וידאו. שינית את שם החדר ל: %1$s %1$s שינה את שם החדר ל: %2$s - שינית את דמות החדר - %1$s שינה את דמות החדר - שינית את השם ל: %1$s + שינית את תמונת החדר + %1$s שינה את תמונת החדר + שינית את הנושא ל: %1$s %1$s שינה את הנושא ל: %2$s - ביטלתם את שם התצוגה שלכם (שם קודם %1$s) + הסרת את שם התצוגה שלך (הוא היה %1$s) %1$sהורידו את שם התצוגה שלהם (היה קודם %2$s) שינית את שמך מ %1$s ל %2$s %1$s שינו את שמותיהם מ %2$s ל %3$s שינית את שמך ל %1$s %1$s שינה את שמו ל %2$s - שינית את דמותך באפליקציה - %1$s שינה את דמותו + שינית תמונת פרופיל + %1$s שינה תמונת פרופיל משכת את הזמנתו של %1$s\'s - %1$s משך את הזמנתו של %2$s\'s + %1$s ביטל/ה את ההזמנה של %2$s\'s חסמת את %1$s - %1$s חסם את%2$s - חסמת את %1$s - %1$s הסיר מחסימה את %2$s + %1$s חסם/ה את %2$s + הסרת את החסימה של %1$s + %1$s הסיר/ה מחסימה את %2$s הרחקת את %1$s - %1$s הרחיק את%2$s + %1$s הרחיק/ה את %2$s דחית את ההזמנה %1$s דחה את ההזמנה עזבת את החדר - %1$s עזב את החדר + %1$s עזב/ה את החדר עזבת את החדר - %1$s עזב את החדר + %1$s עזב/ה את החדר הצטרפת - %1$s הצטרף + %1$s הצטרף/ה הצטרפת לחדר - %1$s הצטרף לחדר - %1$s הזמין אותך + %1$s הצטרף/ה לחדר + %1$s הזמין/ה אותך הזמנת את %1$s - %1$s הזמין %2$s + %1$s הזמין/ה את %2$s יצרת את הדיון - %1$s יצר את הדיון + %1$s יצר/ה את הדיון יצרת את החדר - %1$s יצר את החדר + %1$s יצר/ה את החדר הזמנתך - %s הזמנה - כל חברי החדר, מהנקודה בה הם הוזמנו. + ההזמנה של %s + כל חברי החדר, מהרגע שבו הוזמנו. את/ה הפכת הודעות עתידיות לגלויות בפני %1$s %1$s הפך הודעות עתידיות לגלויות בפני %2$s חסמת את %1$s. סיבה: %2$s @@ -1830,7 +1794,6 @@ \nממתין לתגובת השרת… חדר ריק (היה %s) חדר ריק - %1$s, %2$s, %3$s ו-%4$d אחר %1$s, %2$s, %3$s ו-%4$d אחרים @@ -1894,10 +1857,10 @@ שדרגת כאן. %s שודרג כאן. שדרגת את החדר הזה. - %s שדרג את החדר הזה. + %s שידרג/ה חדר זה. כל אחד. כל חברי החדר. - כל חברי החדר, מהנקודה בה הצטרפו. + כל חברי החדר, מהרגע שבו הצטרפו. מפה שתף מיקום מיקום @@ -1924,8 +1887,8 @@ מיפוי מיקומי משתמשים בציר הזמן לאחר ההפעלה, תוכל לשלוח את המיקום שלך לכל חדר אפשר שיתוף מיקום - לפתוח בעזרת - ${app_name} לא הצליח לגשת למיקום שלך. בבקשה נסה שוב מאוחר יותר. + פתח באמצעות + ${app_name} לא הצליח לגשת למיקום שלך. אנא נסה שוב מאוחר יותר. ${app_name} לא הצליח לגשת למיקום שלך שתף מיקום אפשר התראת דוא\"ל עבור %s @@ -2096,4 +2059,6 @@ %1$s משך את ההזמנה של %2$s. סיבה: %3$s קיבלת את ההזמנה עבור %1$s. סיבה: %2$s %1$s קיבל את ההזמנה עבור %2$s. סיבה: %3$s + שיתפו את מיקומם + מיקום \ No newline at end of file From ed7d1927f5466f61d231cccc5a0dc87a357ebf4a Mon Sep 17 00:00:00 2001 From: sagi korin Date: Wed, 2 Mar 2022 15:41:11 +0000 Subject: [PATCH 043/405] Translated using Weblate (Hebrew) Currently translated at 81.2% (1753 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/he/ --- vector/src/main/res/values-iw/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vector/src/main/res/values-iw/strings.xml b/vector/src/main/res/values-iw/strings.xml index bc0ab0ff34..57a8cf6630 100644 --- a/vector/src/main/res/values-iw/strings.xml +++ b/vector/src/main/res/values-iw/strings.xml @@ -2061,4 +2061,10 @@ %1$s קיבל את ההזמנה עבור %2$s. סיבה: %3$s שיתפו את מיקומם מיקום + משתמשים + הצג פחות + דלג על השאלה + יש לי כבר חשבון + צור חשבון + תקשורת מאובטחת. \ No newline at end of file From e8024688f4cbc3110323c930b4a01dac207fbec8 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 2 Mar 2022 09:37:09 +0000 Subject: [PATCH 044/405] Translated using Weblate (Albanian) Currently translated at 100.0% (51 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40104000.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104020.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40104000.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104020.txt diff --git a/fastlane/metadata/android/sq/changelogs/40104000.txt b/fastlane/metadata/android/sq/changelogs/40104000.txt new file mode 100644 index 0000000000..f917c7c0cb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104000.txt @@ -0,0 +1,2 @@ +Ndryshime kryesore në këtë version: Sendërtimi fillestar i mesazheve në rrjedha. Flluska mesazhesh. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/sq/changelogs/40104020.txt b/fastlane/metadata/android/sq/changelogs/40104020.txt new file mode 100644 index 0000000000..2fbe4f2bf6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104020.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: shtim mbulimi për @room dhe për pyetësorë jopublikë, mes mjaft ndryshimesh të tjera të vockla. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.2 From 33b170077ea5d342d35a550e4808abdd8f374d18 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 3 Mar 2022 13:49:53 +0200 Subject: [PATCH 045/405] force refresh home server capabilities --- .../sdk/internal/database/migration/MigrateSessionTo026.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt index ac097c916b..f108a91ecf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEnti import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities import org.matrix.android.sdk.internal.util.database.RealmMigrator /** @@ -59,5 +60,6 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) { realm.schema.get("HomeServerCapabilitiesEntity") ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() } } From 719e254bb46875965c553c6cc01849c6044fa17a Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 3 Mar 2022 13:51:41 +0200 Subject: [PATCH 046/405] Format Code --- .../sdk/internal/session/room/timeline/LoadTimelineStrategy.kt | 2 +- .../android/sdk/internal/session/room/timeline/TimelineChunk.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index a26008369a..a9e7b3bcdc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -198,7 +198,7 @@ internal class LoadTimelineStrategy( } } if (mode is Mode.Thread) { - return timelineChunk?.loadMoreThread(count, Timeline.Direction.BACKWARDS) ?: LoadMoreResult.FAILURE + return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE } return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 7ab2e17b9d..04a5256510 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -169,7 +169,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, * This function will fetch more live thread timeline events using the /relations api. It will * always fetch results, while we want our data to be up to dated. */ - suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult { + suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult { val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE return if (direction == Timeline.Direction.BACKWARDS) { try { From a77637b751a3cadd8c1e6cffdcac2d1b13aaeb32 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 3 Mar 2022 13:05:29 +0000 Subject: [PATCH 047/405] Translated using Weblate (Japanese) Currently translated at 98.3% (2121 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index fbd6540dd6..c1f3304fb8 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -1489,8 +1489,8 @@ 画像 スクリーンショット 接続 - 投票 - 投票 + アンケート + アンケート コード 役割 送信 @@ -1515,9 +1515,9 @@ 警告! 位置情報 メディア - 投票終了 - 投票を終了しますか? - 投票を削除 + アンケート終了 + アンケートを終了しますか? + アンケートを削除 スペースを作成 スペースに参加 スペースを退出 @@ -1531,10 +1531,10 @@ 非公開のルームをアップグレード 音声メッセージを一時停止 このメールアドレスをアカウントにリンク - 投票を編集 + アンケートを編集 地図 - 投票を終了 - 投票を終了 + アンケートを終了 + アンケートを終了 選択肢%1$d 選択肢を作成 位置情報 @@ -1549,7 +1549,7 @@ ファイルをアップロード 連絡先を開く ステッカーを送信 - 投票を作成 + アンケートを作成 位置情報を共有 スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 @@ -1587,8 +1587,8 @@ ルームの暗号化の有効化 スペースのメインアドレスの変更 スペースのアバターの変更 - 投票を作成 - 投票を作成 + アンケートを作成 + アンケートを作成 暗号化が正しく設定されていないため、メッセージを送ることができません。クリックして設定を開いてください。 暗号化が正しく設定されていないため、メッセージを送ることができません。管理者に連絡して、暗号化を正しい状態に復元してください。 %2$dの%1$d @@ -1741,12 +1741,12 @@ 続行するには%sを入力してください 有効なリカバリーキーではありません リカバリーキーを入力してください - 投票の種類 - 実施中の投票 + アンケートの種類 + 投票の際に結果を公開 投票する 投票した人には、投票の際に即座に結果が表示されます - 終了した投票 - 結果は投票を終了した後でのみ明らかにされます + アンケートの終了後に結果を公開 + 結果はアンケートを終了した後でのみ明らかにされます 以下で開く 暗号化のアップグレードが利用可能です SSSSキーをリカバリーキーから生成しています @@ -1762,7 +1762,7 @@ %1$d個の投票 - 投票を締め切り、投票の最終結果を表示します。 + アンケートを締め切り、最終結果を表示します。 招待者のみ参加可能。個人やチームに最適 スペースを作成 連絡先をスペースに招待 @@ -1789,7 +1789,7 @@ 音声メッセージを録音しています 録音を削除 音声メッセージがアクティブの間は返信や編集はできません - この投票を削除してよろしいですか?一度削除すると復元することはできません。 + このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 共有データの取り扱いに失敗しました 回転とクロップ ルームを探索 @@ -1815,7 +1815,7 @@ \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 %1$sにより%2$sに 質問あるいはトピック - 投票の質問あるいはトピック + アンケートの質問あるいはトピック 少々お待ちください。少し時間がかかるかもしれません。 ルームのバージョン 👓 不安定 From 39bd437f759c36d2b8c59f5107b7c283c5cc8b31 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 3 Mar 2022 17:04:08 +0200 Subject: [PATCH 048/405] Temp fix Realm crash --- .../session/room/timeline/SendingEventsDataSource.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index b7a2cf2fce..4c5322d073 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -66,7 +66,12 @@ internal class RealmSendingEventsDataSource( private fun updateFrozenResults(sendingEvents: RealmList?) { // Makes sure to close the previous frozen realm - frozenSendingTimelineEvents?.realm?.close() + // TODO find a better way to avoid thread timeline crash: + // - Make RealmSendingEventsDataSource Singleton + // - Do not initialize RealmSendingEventsDataSource when we are in a thread timeline while + // we already have an instance from the main timeline + // - Close Main timeline before Opening a thread timeline + // frozenSendingTimelineEvents?.realm?.close() frozenSendingTimelineEvents = sendingEvents?.freeze() } From daafddbe7127037bda084327365a035ecd16cdb2 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 3 Mar 2022 19:10:40 +0200 Subject: [PATCH 049/405] fix Realm crash --- .../room/timeline/SendingEventsDataSource.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index 4c5322d073..1262c09d97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -66,12 +66,9 @@ internal class RealmSendingEventsDataSource( private fun updateFrozenResults(sendingEvents: RealmList?) { // Makes sure to close the previous frozen realm - // TODO find a better way to avoid thread timeline crash: - // - Make RealmSendingEventsDataSource Singleton - // - Do not initialize RealmSendingEventsDataSource when we are in a thread timeline while - // we already have an instance from the main timeline - // - Close Main timeline before Opening a thread timeline - // frozenSendingTimelineEvents?.realm?.close() + if (frozenSendingTimelineEvents?.isValid == true) { + frozenSendingTimelineEvents?.realm?.close() + } frozenSendingTimelineEvents = sendingEvents?.freeze() } @@ -79,13 +76,15 @@ internal class RealmSendingEventsDataSource( val builtSendingEvents = mutableListOf() uiEchoManager.getInMemorySendingEvents() .addWithUiEcho(builtSendingEvents) - frozenSendingTimelineEvents - ?.filter { timelineEvent -> - builtSendingEvents.none { it.eventId == timelineEvent.eventId } - } - ?.map { - timelineEventMapper.map(it) - }?.addWithUiEcho(builtSendingEvents) + if (frozenSendingTimelineEvents?.isValid == true) { + frozenSendingTimelineEvents + ?.filter { timelineEvent -> + builtSendingEvents.none { it.eventId == timelineEvent.eventId } + } + ?.map { + timelineEventMapper.map(it) + }?.addWithUiEcho(builtSendingEvents) + } return builtSendingEvents } From 10ea166b2a898468afb723d79e5cf79231e69b13 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 25 Feb 2022 16:07:06 +0100 Subject: [PATCH 050/405] Extract olm cache store --- .../sdk/account/AccountCreationTest.kt | 152 +++++++++++ .../sdk/internal/crypto/SimpleE2EEChatTest.kt | 244 ++++++++++++++++++ .../sdk/internal/crypto/MXOlmDevice.kt | 22 +- .../sdk/internal/crypto/OlmSessionStore.kt | 152 +++++++++++ .../crypto/algorithms/olm/MXOlmDecryption.kt | 2 +- .../crypto/store/db/RealmCryptoStore.kt | 41 +-- 6 files changed, 575 insertions(+), 38 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index 486bc02769..6f25b24d9c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -16,7 +16,16 @@ package org.matrix.android.sdk.account +import android.util.Log import androidx.test.filters.LargeTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.junit.FixMethodOrder import org.junit.Ignore import org.junit.Test @@ -28,6 +37,9 @@ import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.random.Random @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -62,4 +74,144 @@ class AccountCreationTest : InstrumentedTest { res.cleanUp(commonTestHelper) } + + @Test + fun testConcurrentDecrypt() { +// val res = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + // ============================= + // ARRANGE + // ============================= + + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) + cryptoTestHelper.initializeCrossSigning(bobSession) + val bobSession2 = commonTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + bobSession2.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId ?: "") + bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession2.sessionParams.deviceId ?: "") + + val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + // ============================= + // ACT + // ============================= + + val timelineEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + val secondEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 2", 1).first() + val thirdEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 3", 1).first() + val forthEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 4", 1).first() + + // await for bob unverified session to get the message + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId) != null + } + } + + val eventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! + val secondEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)!! + val thirdEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(thirdEvent.eventId)!! + val forthEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId)!! + + // let's try to decrypt concurrently and check that we are not getting exceptions + val dispatcher = Executors + .newFixedThreadPool(100) + .asCoroutineDispatcher() + val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + + val eventList = listOf(eventBobPOV, secondEventBobPOV, thirdEventBobPOV, forthEventBobPOV) + +// commonTestHelper.runBlockingTest { +// val export = bobSession.cryptoService().exportRoomKeys("foo") + +// } + val atomicAsError = AtomicBoolean() + val deff = mutableListOf>() +// for (i in 1..100) { +// GlobalScope.launch { +// val index = Random.nextInt(eventList.size) +// try { +// val event = eventList[index] +// bobSession.cryptoService().decryptEvent(event.root, "") +// Log.d("#TEST", "Decrypt Success $index :${Thread.currentThread().name}") +// } catch (failure: Throwable) { +// Log.d("#TEST", "Failed to decrypt $index :$failure") +// } +// } +// } + val cryptoService = bobSession.cryptoService() + + coroutineScope.launch { + for (spawn in 1..100) { + delay((Random.nextFloat() * 1000).toLong()) + aliceSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) + } + } + + for (spawn in 1..8000) { + eventList.random().let { event -> + coroutineScope.async { + try { + cryptoService.decryptEvent(event.root, "") + Log.d("#TEST", "[$spawn] Decrypt Success ${event.eventId} :${Thread.currentThread().name}") + } catch (failure: Throwable) { + atomicAsError.set(true) + Log.e("#TEST", "Failed to decrypt $spawn/${event.eventId} :$failure") + } + }.let { + deff.add(it) + } + } +// coroutineScope.async { +// val index = Random.nextInt(eventList.size) +// try { +// val event = eventList[index] +// cryptoService.decryptEvent(event.root, "") +// for (other in eventList.indices) { +// if (other != index) { +// cryptoService.decryptEventAsync(eventList[other].root, "", object : MatrixCallback { +// override fun onFailure(failure: Throwable) { +// Log.e("#TEST", "Failed to decrypt $spawn/$index :$failure") +// } +// }) +// } +// } +// Log.d("#TEST", "[$spawn] Decrypt Success $index :${Thread.currentThread().name}") +// } catch (failure: Throwable) { +// Log.e("#TEST", "Failed to decrypt $spawn/$index :$failure") +// } +// }.let { +// deff.add(it) +// } + } + + coroutineScope.launch { + for (spawn in 1..100) { + delay((Random.nextFloat() * 1000).toLong()) + bobSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) + } + } + + commonTestHelper.runBlockingTest(10 * 60_000) { + deff.awaitAll() + delay(10_000) + assert(!atomicAsError.get()) + // There should be no errors? +// deff.map { it.await() }.forEach { +// it.fold({ +// Log.d("#TEST", "Decrypt Success :${it}") +// }, { +// Log.d("#TEST", "Failed to decrypt :$it") +// }) +// val hasFailure = deff.any { it.await().exceptionOrNull() != null } +// assert(!hasFailure) +// } + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + commonTestHelper.signOutAndClose(bobSession2) + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt new file mode 100644 index 0000000000..d2b2495b76 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import kotlinx.coroutines.delay +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class SimpleE2EEChatTest : InstrumentedTest { + + private val testHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(testHelper) + + /** + * Simple test that create an e2ee room. + * Some new members are added, and a message is sent. + * We check that the message is e2e and can be decrypted. + * + * Additional users join, we check that they can't decrypt history + * + * Alice sends a new message, then check that the new one can be decrypted + */ + @Test + fun testSendingE2EEMessages() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // add some more users and invite them + val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)) + } + + Log.v("#E2E TEST", "All accounts created") + // we want to invite them in the room + otherAccounts.forEach { + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.invite(it.myUserId) + } + } + + // All user should accept invite + otherAccounts.forEach { otherSession -> + waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) + Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") + } + + // check that alice see them as joined (not really necessary?) + ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID) + + Log.v("#E2E TEST", "All users have joined the room") + + Log.v("#E2E TEST", "Alice is sending the message") + + val text = "This is my message" + val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text) + // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() + Assert.assertTrue("Message should be sent", sentEventId != null) + + // All should be able to decrypt + otherAccounts.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + + // Add a new user to the room, and check that he can't decrypt + val newAccount = listOf("adam") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)) + } + + newAccount.forEach { + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.invite(it.myUserId) + } + } + + newAccount.forEach { + waitForAndAcceptInviteInRoom(it, e2eRoomID) + } + + ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) + + // wait a bit + testHelper.runBlockingTest { + delay(3_000) + } + + // check that messages are encrypted (uisi) + newAccount.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!).also { + Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") + } + timeLineEvent != null && + timeLineEvent.root.getClearType() == EventType.ENCRYPTED && + timeLineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + } + } + } + + // Let alice send a new message + Log.v("#E2E TEST", "Alice sends a new message") + + val secondMessage = "2 This is my message" + val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage) + + // new members should be able to decrypt it + newAccount.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondSentEventId!!).also { + Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") + } + timeLineEvent != null && + timeLineEvent.root.getClearType() == EventType.MESSAGE && + secondMessage.equals(timeLineEvent.root.getClearContent().toModel()?.body) + } + } + } + + otherAccounts.forEach { + testHelper.signOutAndClose(it) + } + newAccount.forEach { testHelper.signOutAndClose(it) } + + cryptoTestData.cleanUp(testHelper) + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { + aliceRoomPOV.sendTextMessage(text) + var sentEventId: String? = null + testHelper.waitWithLatch(4 * 60_000) { + val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60)) + timeline.start() + + testHelper.retryPeriodicallyWithLatch(it) { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { + Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null + } + + timeline.dispose() + } + return sentEventId + } + + private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + otherAccounts.map { + aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + } + + private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") + } + } + } + } + + testHelper.runBlockingTest(60_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index e1a706df79..bfe986d1fd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -47,6 +47,7 @@ internal class MXOlmDevice @Inject constructor( * The store where crypto data is saved. */ private val store: IMXCryptoStore, + private val olmSessionStore: OlmSessionStore, private val inboundGroupSessionStore: InboundGroupSessionStore ) { @@ -190,6 +191,7 @@ internal class MXOlmDevice @Inject constructor( it.groupSession.releaseSession() } outboundGroupSessionCache.clear() + olmSessionStore.clear() } /** @@ -257,7 +259,8 @@ internal class MXOlmDevice @Inject constructor( // this session olmSessionWrapper.onMessageReceived() - store.storeSession(olmSessionWrapper, theirIdentityKey) + olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) +// store.storeSession(olmSessionWrapper, theirIdentityKey) val sessionIdentifier = olmSession.sessionIdentifier() @@ -324,7 +327,7 @@ internal class MXOlmDevice @Inject constructor( // This counts as a received message: set last received message time to now olmSessionWrapper.onMessageReceived() - store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { Timber.e(e, "## createInboundSession() : decryptMessage failed") } @@ -357,8 +360,8 @@ internal class MXOlmDevice @Inject constructor( * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. * @return a list of known session ids for the device. */ - fun getSessionIds(theirDeviceIdentityKey: String): List? { - return store.getDeviceSessionIds(theirDeviceIdentityKey) + fun getSessionIds(theirDeviceIdentityKey: String): List { + return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey) } /** @@ -368,7 +371,7 @@ internal class MXOlmDevice @Inject constructor( * @return the session id, or null if no established session. */ fun getSessionId(theirDeviceIdentityKey: String): String? { - return store.getLastUsedSessionId(theirDeviceIdentityKey) + return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey) } /** @@ -390,7 +393,8 @@ internal class MXOlmDevice @Inject constructor( // Timber.v("## encryptMessage() : payloadString: " + payloadString); olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString) - store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) +// store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) res = HashMap() res["body"] = olmMessage.mCipherText @@ -427,7 +431,8 @@ internal class MXOlmDevice @Inject constructor( try { payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage) olmSessionWrapper.onMessageReceived() - store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) +// store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { Timber.e(e, "## decryptMessage() : decryptMessage failed") } @@ -819,7 +824,8 @@ internal class MXOlmDevice @Inject constructor( private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { // sanity check return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { - store.getDeviceSession(sessionId, theirDeviceIdentityKey) + olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) +// store.getDeviceSession(sessionId, theirDeviceIdentityKey) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt new file mode 100644 index 0000000000..6044095e7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import javax.inject.Inject + +/** + * Keep the used olm session in memory and load them from the data layer when needed + * Access is synchronized for thread safety + */ +internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) { + + /* + * map of device key to list of olm sessions (it is possible to have several active sessions with a device) + */ + private val olmSessions = HashMap>() + + /** + * Store a session between the logged-in user and another device. + * This will be called after creation but also after any use of the ratchet + * in order to persist the correct state for next run + * @param olmSessionWrapper the end-to-end session. + * @param deviceKey the public key of the other device. + */ + @Synchronized + fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { + // This could be a newly created session or one that was just created + // Anyhow we should persist ratchet state for futur app lifecycle + addNewSessionInCache(olmSessionWrapper, deviceKey) + store.storeSession(olmSessionWrapper, deviceKey) + } + + /** + * Retrieve the end-to-end session ids between the logged-in user and another + * device. + * + * @param deviceKey the public key of the other device. + * @return A set of sessionId, or empty if device is not known + */ + @Synchronized + fun getDeviceSessionIds(deviceKey: String): List { + return internalGetAllSessions(deviceKey) + } + + private fun internalGetAllSessions(deviceKey: String): MutableList { + // we need to get the persisted ids first + val persistedKnownSessions = store.getDeviceSessionIds(deviceKey) + .orEmpty() + .toMutableList() + // Do we have some in cache not yet persisted? + olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached -> + tryOrNull("Olm session was released") { cached.olmSession.sessionIdentifier() }?.let { cachedSessionId -> + if (!persistedKnownSessions.contains(cachedSessionId)) { + persistedKnownSessions.add(cachedSessionId) + } + } + } + return persistedKnownSessions + } + + /** + * Retrieve an end-to-end session between the logged-in user and another + * device. + * + * @param sessionId the session Id. + * @param deviceKey the public key of the other device. + * @return The Base64 end-to-end session, or null if not found + */ + @Synchronized + fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { + // get from cache or load and add to cache + return internalGetSession(sessionId, deviceKey) + } + + /** + * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist + * + * @param deviceKey the public key of the other device. + * @return last used sessionId, or null if not found + */ + @Synchronized + fun getLastUsedSessionId(deviceKey: String): String? { + // We want to avoid to load in memory old session if possible + val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey) + var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) } + // we should check if we have one in cache with a higher last message received? + olmSessions[deviceKey].orEmpty().forEach { inCache -> + if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) { + candidate = inCache + } + } + + return candidate?.olmSession?.sessionIdentifier() + } + + /** + * Release all sessions and clear cache + */ + @Synchronized + fun clear() { + olmSessions.entries.onEach { entry -> + entry.value.onEach { it.olmSession.releaseSession() } + } + olmSessions.clear() + } + + private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { + return getSessionInCache(sessionId, deviceKey) + ?: // deserialize from store + return store.getDeviceSession(sessionId, deviceKey)?.also { + addNewSessionInCache(it, deviceKey) + } + } + + private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? { + return olmSessions[deviceKey]?.firstOrNull { + it.olmSession.isReleased && it.olmSession.sessionIdentifier() == sessionId + } + } + + private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) { + val sessionId = tryOrNull { session.olmSession.sessionIdentifier() } ?: return + olmSessions.getOrPut(deviceKey) { mutableListOf() }.let { + val existing = it.firstOrNull { tryOrNull { it.olmSession.sessionIdentifier() } == sessionId } + it.add(session) + // remove and release if was there but with different instance + if (existing != null && existing.olmSession != session.olmSession) { + // mm not sure when this could happen + // anyhow we should remove and release the one known + it.remove(existing) + existing.olmSession.releaseSession() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index f1bca4fbc6..eb93abfb61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -154,7 +154,7 @@ internal class MXOlmDecryption( * @return payload, if decrypted successfully. */ private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { - val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty() + val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) val messageBody = message["body"] as? String ?: return null val messageType = when (val typeAsVoid = message["type"]) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index a07827c033..c13ed77eb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -125,7 +125,7 @@ internal class RealmCryptoStore @Inject constructor( private var olmAccount: OlmAccount? = null // Cache for OlmSession, to release them properly - private val olmSessionsToRelease = HashMap() +// private val olmSessionsToRelease = HashMap() // Cache for InboundGroupSession, to release them properly private val inboundGroupSessionToRelease = HashMap() @@ -213,11 +213,6 @@ internal class RealmCryptoStore @Inject constructor( monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) } - olmSessionsToRelease.forEach { - it.value.olmSession.releaseSession() - } - olmSessionsToRelease.clear() - inboundGroupSessionToRelease.forEach { it.value.olmInboundGroupSession?.releaseSession() } @@ -680,13 +675,6 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) - // Release memory of previously known session, if it is not the same one - if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) { - olmSessionsToRelease[key]?.olmSession?.releaseSession() - } - - olmSessionsToRelease[key] = olmSessionWrapper - doRealmTransaction(realmConfiguration) { val realmOlmSession = OlmSessionEntity().apply { primaryKey = key @@ -703,23 +691,18 @@ internal class RealmCryptoStore @Inject constructor( override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) - - // If not in cache (or not found), try to read it from realm - if (olmSessionsToRelease[key] == null) { - doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - } - ?.let { - val olmSession = it.getOlmSession() - if (olmSession != null && it.sessionId != null) { - olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) - } - } + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) + .findFirst() } - - return olmSessionsToRelease[key] + ?.let { + val olmSession = it.getOlmSession() + if (olmSession != null && it.sessionId != null) { + return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) + } + null + } } override fun getLastUsedSessionId(deviceKey: String): String? { From 33f9bc52cbacd77432ae8bfffe6b306857809f8b Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 08:55:03 +0100 Subject: [PATCH 051/405] Protect olm session from concurrent access --- .../sdk/api/session/crypto/CryptoService.kt | 2 +- .../internal/crypto/DefaultCryptoService.kt | 4 +- .../sdk/internal/crypto/EventDecryptor.kt | 4 +- .../sdk/internal/crypto/MXOlmDevice.kt | 40 ++++++++++--------- .../sdk/internal/crypto/OlmSessionStore.kt | 24 ++++++++--- .../crypto/actions/MessageEncrypter.kt | 2 +- .../crypto/algorithms/IMXDecrypting.kt | 2 +- .../algorithms/megolm/MXMegolmDecryption.kt | 2 +- .../crypto/algorithms/olm/MXOlmDecryption.kt | 4 +- .../crypto/model/OlmSessionWrapper.kt | 5 ++- .../threads/FetchThreadTimelineTask.kt | 2 +- .../room/summary/RoomSummaryUpdater.kt | 5 ++- .../room/timeline/TimelineEventDecryptor.kt | 7 +++- .../session/sync/SyncResponseHandler.kt | 1 + .../session/sync/handler/CryptoSyncHandler.kt | 6 ++- .../sync/handler/room/RoomSyncHandler.kt | 7 +++- .../notifications/NotifiableEventResolver.kt | 2 +- 17 files changed, 75 insertions(+), 44 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index e3f00a24b6..65f69e17c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -121,7 +121,7 @@ interface CryptoService { fun discardOutboundSession(roomId: String) @Throws(MXCryptoError::class) - fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 0646e4d2b8..a66e0d4077 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -723,7 +723,7 @@ internal class DefaultCryptoService @Inject constructor( * @return the MXEventDecryptionResult data, or throw in case of error */ @Throws(MXCryptoError::class) - override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { return internalDecryptEvent(event, timeline) } @@ -746,7 +746,7 @@ internal class DefaultCryptoService @Inject constructor( * @return the MXEventDecryptionResult data, or null in case of error */ @Throws(MXCryptoError::class) - private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { return eventDecryptor.decryptEvent(event, timeline) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 57381eacfb..8a11e45740 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -63,7 +63,7 @@ internal class EventDecryptor @Inject constructor( * @return the MXEventDecryptionResult data, or throw in case of error */ @Throws(MXCryptoError::class) - fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { return internalDecryptEvent(event, timeline) } @@ -91,7 +91,7 @@ internal class EventDecryptor @Inject constructor( * @return the MXEventDecryptionResult data, or null in case of error */ @Throws(MXCryptoError::class) - private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content if (eventContent == null) { Timber.e("## CRYPTO | decryptEvent : empty event content") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index bfe986d1fd..ae7a4076cc 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto +import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JsonDict @@ -382,31 +383,30 @@ internal class MXOlmDevice @Inject constructor( * @param payloadString the payload to be encrypted and sent * @return the cipher text */ - fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { - var res: MutableMap? = null - val olmMessage: OlmMessage + suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) if (olmSessionWrapper != null) { try { Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") - // Timber.v("## encryptMessage() : payloadString: " + payloadString); - olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString) -// store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - res = HashMap() - - res["body"] = olmMessage.mCipherText - res["type"] = olmMessage.mType - } catch (e: Exception) { - Timber.e(e, "## encryptMessage() : failed") + val olmMessage = olmSessionWrapper.mutex.withLock { + olmSessionWrapper.olmSession.encryptMessage(payloadString) + } + return mapOf( + "body" to olmMessage.mCipherText, + "type" to olmMessage.mType, + ).also { + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } + } catch (e: Throwable) { + Timber.e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") + return null } } else { Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId") + return null } - - return res } /** @@ -418,7 +418,7 @@ internal class MXOlmDevice @Inject constructor( * @param sessionId the id of the active session. * @return the decrypted payload. */ - fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { + suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { var payloadString: String? = null val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) @@ -429,8 +429,12 @@ internal class MXOlmDevice @Inject constructor( olmMessage.mType = messageType.toLong() try { - payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage) - olmSessionWrapper.onMessageReceived() + payloadString = + olmSessionWrapper.mutex.withLock { + olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { + olmSessionWrapper.onMessageReceived() + } + } olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) // store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index 6044095e7b..bd59916658 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -16,17 +16,20 @@ package org.matrix.android.sdk.internal.crypto -import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.olm.OlmSession +import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO) + /** * Keep the used olm session in memory and load them from the data layer when needed * Access is synchronized for thread safety */ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) { - /* * map of device key to list of olm sessions (it is possible to have several active sessions with a device) */ @@ -66,7 +69,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS .toMutableList() // Do we have some in cache not yet persisted? olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached -> - tryOrNull("Olm session was released") { cached.olmSession.sessionIdentifier() }?.let { cachedSessionId -> + getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId -> if (!persistedKnownSessions.contains(cachedSessionId)) { persistedKnownSessions.add(cachedSessionId) } @@ -131,14 +134,23 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? { return olmSessions[deviceKey]?.firstOrNull { - it.olmSession.isReleased && it.olmSession.sessionIdentifier() == sessionId + getSafeSessionIdentifier(it.olmSession) == sessionId + } + } + + private fun getSafeSessionIdentifier(session: OlmSession): String? { + return try { + session.sessionIdentifier() + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session") + null } } private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) { - val sessionId = tryOrNull { session.olmSession.sessionIdentifier() } ?: return + val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return olmSessions.getOrPut(deviceKey) { mutableListOf() }.let { - val existing = it.firstOrNull { tryOrNull { it.olmSession.sessionIdentifier() } == sessionId } + val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId } it.add(session) // remove and release if was there but with different instance if (existing != null && existing.olmSession != session.olmSession) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt index 165f200bac..b37def5c1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt @@ -42,7 +42,7 @@ internal class MessageEncrypter @Inject constructor( * @param deviceInfos list of device infos to encrypt for. * @return the content for an m.room.encrypted event. */ - fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { + suspend fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } val payloadJson = payloadFields.toMutableMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 79c7608cbf..b6c1d99aa5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -36,7 +36,7 @@ internal interface IMXDecrypting { * @return the decryption information, or an error */ @Throws(MXCryptoError::class) - fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult /** * Handle a key event. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 2ee24dfbb0..f7ab12adeb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -71,7 +71,7 @@ internal class MXMegolmDecryption(private val userId: String, // private var pendingEvents: MutableMap>> = HashMap() @Throws(MXCryptoError::class) - override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { // If cross signing is enabled, we don't send request until the keys are trusted // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index eb93abfb61..6679cb7b83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -38,7 +38,7 @@ internal class MXOlmDecryption( IMXDecrypting { @Throws(MXCryptoError::class) - override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val olmEventContent = event.content.toModel() ?: run { Timber.e("## decryptEvent() : bad event format") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, @@ -153,7 +153,7 @@ internal class MXOlmDecryption( * @param message message object, with 'type' and 'body' fields. * @return payload, if decrypted successfully. */ - private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { + private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) val messageBody = message["body"] as? String ?: return null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt index 15b92f105a..b6be4d6864 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.model +import kotlinx.coroutines.sync.Mutex import org.matrix.olm.OlmSession /** @@ -25,7 +26,9 @@ data class OlmSessionWrapper( // The associated olm session. val olmSession: OlmSession, // Timestamp at which the session last received a message. - var lastReceivedMessageTs: Long = 0) { + var lastReceivedMessageTs: Long = 0, + + var mutex: Mutex = Mutex()) { /** * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index e0d501c515..cd06d47f05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -156,7 +156,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( * Invoke the event decryption mechanism for a specific event */ - private fun decryptIfNeeded(event: Event, roomId: String) { + private suspend fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 1c1d59fb3d..fb2adc8f5e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm import io.realm.kotlin.createObject +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType @@ -165,7 +166,9 @@ internal class RoomSummaryUpdater @Inject constructor( Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") // mmm i want to decrypt now or is it ok to do it async? tryOrNull { - eventDecryptor.decryptEvent(root.asDomain(), "") + runBlocking { + eventDecryptor.decryptEvent(root.asDomain(), "") + } } ?.let { root.setDecryptionResult(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 49a8a8b55a..bacac58d84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.Realm import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking 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 @@ -99,7 +100,9 @@ internal class TimelineEventDecryptor @Inject constructor( } executor?.execute { Realm.getInstance(realmConfiguration).use { realm -> - processDecryptRequest(request, realm) + runBlocking { + processDecryptRequest(request, realm) + } } } } @@ -115,7 +118,7 @@ internal class TimelineEventDecryptor @Inject constructor( threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity) } } - private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) { + private suspend fun processDecryptRequest(request: DecryptionRequest, realm: Realm) { val event = request.event val timelineId = request.timelineId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index f93da9705d..1bbf54a788 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -110,6 +110,7 @@ internal class SyncResponseHandler @Inject constructor( // Start one big transaction monarchy.awaitTransaction { realm -> + // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) measureTimeMillis { Timber.v("Handle rooms") reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index f299d3effa..28cfbc7342 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -38,7 +38,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, private val verificationService: DefaultVerificationService) { - fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { + suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { val total = toDevice.events?.size ?: 0 toDevice.events?.forEachIndexed { index, event -> progressReporter?.reportProgress(index * 100F / total) @@ -66,7 +66,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: * @param timelineId the timeline identifier * @return true if the event has been decrypted */ - private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { + private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { Timber.v("## CRYPTO | decryptToDeviceEvent") if (event.getClearType() == EventType.ENCRYPTED) { var result: MXEventDecryptionResult? = null @@ -80,6 +80,8 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: it.identityKey() == senderKey }?.deviceId ?: senderKey Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}") } if (null != result) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 99e6521eb7..a5ad19bbf8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import dagger.Lazy import io.realm.Realm import io.realm.kotlin.createObject +import kotlinx.coroutines.runBlocking 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 @@ -379,7 +380,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val isInitialSync = insertType == EventInsertType.INITIAL_SYNC if (event.isEncrypted() && !isInitialSync) { - decryptIfNeeded(event, roomId) + runBlocking { + decryptIfNeeded(event, roomId) + } } var contentToInject: String? = null if (!isInitialSync) { @@ -455,7 +458,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return chunkEntity } - private fun decryptIfNeeded(event: Event, roomId: String) { + private suspend fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index ec034173fc..3cdc9e8c76 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -190,7 +190,7 @@ class NotifiableEventResolver @Inject constructor( } } - private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { + private suspend fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { if (root.isEncrypted() && root.mxDecryptionResult == null) { // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync From 9df5f17132376a72043dc5d5c185b5f52c9c39b6 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 09:25:25 +0100 Subject: [PATCH 052/405] protect olm account access --- .../sdk/internal/crypto/MXOlmDevice.kt | 61 +++++++++++++------ .../crypto/store/db/RealmCryptoStore.kt | 1 + 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index ae7a4076cc..8e798619a9 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -106,13 +106,13 @@ internal class MXOlmDevice @Inject constructor( } try { - deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + deviceCurve25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } } catch (e: Exception) { Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") } try { - deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + deviceEd25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } } catch (e: Exception) { Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") } @@ -123,7 +123,7 @@ internal class MXOlmDevice @Inject constructor( */ fun getOneTimeKeys(): Map>? { try { - return store.getOlmAccount().oneTimeKeys() + return doWithOlmAccount { it.oneTimeKeys() } } catch (e: Exception) { Timber.e(e, "## getOneTimeKeys() : failed") } @@ -135,7 +135,18 @@ internal class MXOlmDevice @Inject constructor( * @return The maximum number of one-time keys the olm account can store. */ fun getMaxNumberOfOneTimeKeys(): Long { - return store.getOlmAccount().maxOneTimeKeys() + return doWithOlmAccount { it.maxOneTimeKeys() } + } + + /** + * Olm account access should be synchronized + */ + private fun doWithOlmAccount(block: (OlmAccount) -> T): T { + return store.getOlmAccount().let { olmAccount -> + synchronized(olmAccount) { + block.invoke(olmAccount) + } + } } /** @@ -145,7 +156,7 @@ internal class MXOlmDevice @Inject constructor( */ fun getFallbackKey(): MutableMap>? { try { - return store.getOlmAccount().fallbackKey() + return doWithOlmAccount { it.fallbackKey() } } catch (e: Exception) { Timber.e("## getFallbackKey() : failed") } @@ -160,8 +171,10 @@ internal class MXOlmDevice @Inject constructor( fun generateFallbackKeyIfNeeded(): Boolean { try { if (!hasUnpublishedFallbackKey()) { - store.getOlmAccount().generateFallbackKey() - store.saveOlmAccount() + doWithOlmAccount { + it.generateFallbackKey() + store.saveOlmAccount() + } return true } } catch (e: Exception) { @@ -176,8 +189,10 @@ internal class MXOlmDevice @Inject constructor( fun forgetFallbackKey() { try { - store.getOlmAccount().forgetFallbackKey() - store.saveOlmAccount() + doWithOlmAccount { + it.forgetFallbackKey() + store.saveOlmAccount() + } } catch (e: Exception) { Timber.e("## forgetFallbackKey() : failed") } @@ -203,7 +218,7 @@ internal class MXOlmDevice @Inject constructor( */ fun signMessage(message: String): String? { try { - return store.getOlmAccount().signMessage(message) + return doWithOlmAccount { it.signMessage(message) } } catch (e: Exception) { Timber.e(e, "## signMessage() : failed") } @@ -216,8 +231,10 @@ internal class MXOlmDevice @Inject constructor( */ fun markKeysAsPublished() { try { - store.getOlmAccount().markOneTimeKeysAsPublished() - store.saveOlmAccount() + doWithOlmAccount { + it.markOneTimeKeysAsPublished() + store.saveOlmAccount() + } } catch (e: Exception) { Timber.e(e, "## markKeysAsPublished() : failed") } @@ -230,8 +247,10 @@ internal class MXOlmDevice @Inject constructor( */ fun generateOneTimeKeys(numKeys: Int) { try { - store.getOlmAccount().generateOneTimeKeys(numKeys) - store.saveOlmAccount() + doWithOlmAccount { + it.generateOneTimeKeys(numKeys) + store.saveOlmAccount() + } } catch (e: Exception) { Timber.e(e, "## generateOneTimeKeys() : failed") } @@ -251,7 +270,9 @@ internal class MXOlmDevice @Inject constructor( try { olmSession = OlmSession() - olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey) + doWithOlmAccount { olmAccount -> + olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) + } val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) @@ -292,7 +313,9 @@ internal class MXOlmDevice @Inject constructor( try { try { olmSession = OlmSession() - olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext) + doWithOlmAccount { olmAccount -> + olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) + } } catch (e: Exception) { Timber.e(e, "## createInboundSession() : the session creation failed") return null @@ -301,8 +324,10 @@ internal class MXOlmDevice @Inject constructor( Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") try { - store.getOlmAccount().removeOneTimeKeys(olmSession) - store.saveOlmAccount() + doWithOlmAccount { olmAccount -> + olmAccount.removeOneTimeKeys(olmSession) + store.saveOlmAccount() + } } catch (e: Exception) { Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index c13ed77eb7..fec2e04458 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -246,6 +246,7 @@ internal class RealmCryptoStore @Inject constructor( return olmAccount!! } + @Synchronized override fun getOrCreateOlmAccount(): OlmAccount { doRealmTransaction(realmConfiguration) { val metaData = it.where().findFirst() From 87d930819a9fcfb8998aba734068b73c7ffb587d Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 10:23:09 +0100 Subject: [PATCH 053/405] Fix test compilation --- .../sdk/internal/crypto/UnwedgingTest.kt | 6 +++-- .../crypto/gossiping/KeyShareTests.kt | 24 +++++++++++++++---- .../crypto/gossiping/WithHeldTests.kt | 18 ++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 0a8ce67680..8a619043c4 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -227,8 +227,10 @@ class UnwedgingTest : InstrumentedTest { testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { // we should get back the key and be able to decrypt - val result = tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") + val result = testHelper.runBlockingTest { + tryOrNull { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") + } } Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") result != null diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 82aee454eb..cd20ab477c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest { assert(receivedEvent!!.isEncrypted()) try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } fail("should fail") } catch (failure: Throwable) { } @@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest { } try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } fail("should fail") } catch (failure: Throwable) { } @@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest { } try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } } catch (failure: Throwable) { fail("should have been able to decrypt") } @@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest { val roomRoomBobPov = aliceSession.getRoom(roomId) val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId) - var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") } + var dRes = tryOrNull { + commonTestHelper.runBlockingTest { + bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") + } + } assert(dRes == null) @@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest { Thread.sleep(3_000) // With the bug the first session would have improperly reshare that key :/ - dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") } + dRes = tryOrNull { + commonTestHelper.runBlockingTest { + bobSession.cryptoService().decryptEvent(beforeJoin.root, "") + } + } Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}") assert(dRes?.clearEvent == null) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 9fda21763a..65c65660b5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld try { // .. might need to wait a bit for stability? - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + testHelper.runBlockingTest { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest { // Previous message should still be undecryptable (partially withheld session) try { // .. might need to wait a bit for stability? - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + testHelper.runBlockingTest { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest { val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) try { // .. might need to wait a bit for stability? - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + testHelper.runBlockingTest { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest { testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { // try to decrypt and force key request - tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") } + tryOrNull { + testHelper.runBlockingTest { + bobSecondSession.cryptoService().decryptEvent(it.root, "") + } + } } sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId timeLineEvent != null From 24c51ea41a0be33d93bf0851e657b9d3695764ce Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 15:11:18 +0100 Subject: [PATCH 054/405] Clean megolm import code --- .../crypto/InboundGroupSessionStore.kt | 14 ++ .../sdk/internal/crypto/MXOlmDevice.kt | 136 ++++++++++-------- 2 files changed, 91 insertions(+), 59 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index e7a46750b0..2532062c12 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -70,8 +70,22 @@ internal class InboundGroupSessionStore @Inject constructor( } } + @Synchronized + fun replaceGroupSession(old: OlmInboundGroupSessionWrapper2, new: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { + Timber.v("## Replacing outdated session ${old.roomId}-${old.senderKey}") + dirtySession.remove(old) + store.removeInboundGroupSession(sessionId, senderKey) + sessionCache.remove(CacheKey(sessionId, senderKey)) + + internalStoreGroupSession(new, sessionId, senderKey) + } + @Synchronized fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { + internalStoreGroupSession(wrapper, sessionId, senderKey) + } + + private fun internalStoreGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}") // We want to batch this a bit for performances dirtySession.add(wrapper) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 8e798619a9..f84d259d6d 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JsonDict @@ -612,52 +613,63 @@ internal class MXOlmDevice @Inject constructor( forwardingCurve25519KeyChain: List, keysClaimed: Map, exportFormat: Boolean): Boolean { - val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) - runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } - .fold( - { - // If we already have this session, consider updating it - Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + val existingSession = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + // If we have an existing one we should check if the new one is not better + if (existingSession != null) { + Timber.d("## addInboundGroupSession() check if known session is better than candidate session") + try { + val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also { + // This is quite unexpected, could throw if native was released? + Timber.e("## addInboundGroupSession() null firstKnownIndex on existing session") + candidateSession.olmInboundGroupSession?.releaseSession() + // Probably should discard it? + } + val newKnownFirstIndex = candidateSession.firstKnownIndex + // If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + Timber.d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") + candidateSession.olmInboundGroupSession?.releaseSession() + return false + } + } catch (failure: Throwable) { + Timber.e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") + candidateSession.olmInboundGroupSession?.releaseSession() + return false + } + } - val existingFirstKnown = it.firstKnownIndex!! - val newKnownFirstIndex = session.firstKnownIndex + Timber.d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - // If our existing session is better we keep it - if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - session.olmInboundGroupSession?.releaseSession() - return false - } - }, - { - // Nothing to do in case of error - } - ) - - // sanity check - if (null == session.olmInboundGroupSession) { - Timber.e("## addInboundGroupSession : invalid session") + // sanity check on the new session + val candidateOlmInboundSession = candidateSession.olmInboundGroupSession + if (null == candidateOlmInboundSession) { + Timber.e("## addInboundGroupSession : invalid session ") return false } try { - if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) { + if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - session.olmInboundGroupSession!!.releaseSession() + candidateOlmInboundSession.releaseSession() return false } - } catch (e: Exception) { - session.olmInboundGroupSession?.releaseSession() + } catch (e: Throwable) { + candidateOlmInboundSession.releaseSession() Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed") return false } - session.senderKey = senderKey - session.roomId = roomId - session.keysClaimed = keysClaimed - session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + candidateSession.senderKey = senderKey + candidateSession.roomId = roomId + candidateSession.keysClaimed = keysClaimed + candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain - inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey) -// store.storeInboundGroupSessions(listOf(session)) + if (existingSession != null) { + inboundGroupSessionStore.replaceGroupSession(existingSession, candidateSession, sessionId, senderKey) + } else { + inboundGroupSessionStore.storeInBoundGroupSession(candidateSession, sessionId, senderKey) + } return true } @@ -672,57 +684,63 @@ internal class MXOlmDevice @Inject constructor( val sessions = ArrayList(megolmSessionsData.size) for (megolmSessionData in megolmSessionsData) { - val sessionId = megolmSessionData.sessionId - val senderKey = megolmSessionData.senderKey + val sessionId = megolmSessionData.sessionId ?: continue + val senderKey = megolmSessionData.senderKey ?: continue val roomId = megolmSessionData.roomId - var session: OlmInboundGroupSessionWrapper2? = null + var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null try { - session = OlmInboundGroupSessionWrapper2(megolmSessionData) + candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) } catch (e: Exception) { Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") } // sanity check - if (session?.olmInboundGroupSession == null) { + if (candidateSessionToImport?.olmInboundGroupSession == null) { Timber.e("## importInboundGroupSession : invalid session") continue } + val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession try { - if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) { + if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession() + candidateOlmInboundGroupSession?.releaseSession() continue } } catch (e: Exception) { Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed") - session.olmInboundGroupSession!!.releaseSession() + candidateOlmInboundGroupSession?.releaseSession() continue } - runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } - .fold( - { - // If we already have this session, consider updating it - Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + val existingSession = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - // For now we just ignore updates. TODO: implement something here - if (it.firstKnownIndex!! <= session.firstKnownIndex!!) { - // Ignore this, keep existing - session.olmInboundGroupSession!!.releaseSession() - } else { - sessions.add(session) - } - Unit - }, - { - // Session does not already exist, add it - sessions.add(session) - } + if (existingSession == null) { + // Session does not already exist, add it + Timber.d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") + sessions.add(candidateSessionToImport) + } else { + Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } + val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } - ) + if (existingFirstKnown == null || candidateFirstKnownIndex == null) { + // should not happen? + candidateSessionToImport.olmInboundGroupSession?.releaseSession() + Timber.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") + } else { + if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { + // Ignore this, keep existing + candidateOlmInboundGroupSession.releaseSession() + } else { + // update cache with better session + inboundGroupSessionStore.replaceGroupSession(existingSession, candidateSessionToImport, sessionId, senderKey) + sessions.add(candidateSessionToImport) + } + } + } } store.storeInboundGroupSessions(sessions) From c97de48474c621077d2b3359b1e45c801b633de8 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 15:11:29 +0100 Subject: [PATCH 055/405] Added e2ee sanity tests --- .../android/sdk/common/TestConstants.kt | 2 +- .../sdk/internal/crypto/E2eeSanityTests.kt | 519 ++++++++++++++++++ .../sdk/internal/crypto/SimpleE2EEChatTest.kt | 244 -------- 3 files changed, 520 insertions(+), 245 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt index 5c9b79361e..0f79896b2c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt @@ -23,7 +23,7 @@ object TestConstants { const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" // Time out to use when waiting for server response. - private const val AWAIT_TIME_OUT_MILLIS = 30_000 + private const val AWAIT_TIME_OUT_MILLIS = 60_000 // Time out to use when waiting for server response, when the debugger is connected. 10 minutes private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt new file mode 100644 index 0000000000..a8aa3e55d3 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import kotlinx.coroutines.delay +import org.amshove.kluent.fail +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeSanityTests : InstrumentedTest { + + private val testHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(testHelper) + + /** + * Simple test that create an e2ee room. + * Some new members are added, and a message is sent. + * We check that the message is e2e and can be decrypted. + * + * Additional users join, we check that they can't decrypt history + * + * Alice sends a new message, then check that the new one can be decrypted + */ + @Test + fun testSendingE2EEMessages() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // add some more users and invite them + val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)) + } + + Log.v("#E2E TEST", "All accounts created") + // we want to invite them in the room + otherAccounts.forEach { + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.invite(it.myUserId) + } + } + + // All user should accept invite + otherAccounts.forEach { otherSession -> + waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) + Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") + } + + // check that alice see them as joined (not really necessary?) + ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID) + + Log.v("#E2E TEST", "All users have joined the room") + + Log.v("#E2E TEST", "Alice is sending the message") + + val text = "This is my message" + val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text) + // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() + Assert.assertTrue("Message should be sent", sentEventId != null) + + // All should be able to decrypt + otherAccounts.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + + // Add a new user to the room, and check that he can't decrypt + val newAccount = listOf("adam") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)) + } + + newAccount.forEach { + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.invite(it.myUserId) + } + } + + newAccount.forEach { + waitForAndAcceptInviteInRoom(it, e2eRoomID) + } + + ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) + + // wait a bit + testHelper.runBlockingTest { + delay(3_000) + } + + // check that messages are encrypted (uisi) + newAccount.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!).also { + Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") + } + timeLineEvent != null && + timeLineEvent.root.getClearType() == EventType.ENCRYPTED && + timeLineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + } + } + } + + // Let alice send a new message + Log.v("#E2E TEST", "Alice sends a new message") + + val secondMessage = "2 This is my message" + val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage) + + // new members should be able to decrypt it + newAccount.forEach { otherSession -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondSentEventId!!).also { + Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") + } + timeLineEvent != null && + timeLineEvent.root.getClearType() == EventType.MESSAGE && + secondMessage.equals(timeLineEvent.root.getClearContent().toModel()?.body) + } + } + } + + otherAccounts.forEach { + testHelper.signOutAndClose(it) + } + newAccount.forEach { testHelper.signOutAndClose(it) } + + cryptoTestData.cleanUp(testHelper) + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { + aliceRoomPOV.sendTextMessage(text) + var sentEventId: String? = null + testHelper.waitWithLatch(4 * 60_000) { + val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60)) + timeline.start() + + testHelper.retryPeriodicallyWithLatch(it) { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { + Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null + } + + timeline.dispose() + } + return sentEventId + } + + private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + otherAccounts.map { + aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + } + + private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") + } + } + } + } + + testHelper.runBlockingTest(60_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } + + /** + * Quick test for basic keybackup + * 1. Create e2e between Alice and Bob + * 2. Alice sends 3 messages, using 3 different sessions + * 3. Ensure bob can decrypt + * 4. Create backup for bob and uplaod keys + * + * 5. Sign out alice and bob to ensure no gossiping will happen + * + * 6. Let bob sign in with a new session + * 7. Ensure history is UISI + * 8. Import backup + * 9. Check that new session can decrypt + */ + @Test + fun testBasicBackupImport() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + Log.v("#E2E TEST", "Create and start keybackup for bob ...") + val keysBackupService = bobSession.cryptoService().keysBackupService() + val keyBackupPassword = "FooBarBaz" + val megolmBackupCreationInfo = testHelper.doSync { + keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) + } + val version = testHelper.doSync { + keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + Log.v("#E2E TEST", "... Key backup started and enabled for bob") + // Bob session should now have + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // let's send a few message to bob + val sentEventIds = mutableListOf() + val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") + messagesText.forEach { text -> + val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + sentEventIds.add(it) + } + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + // we want more so let's discard the session + aliceSession.cryptoService().discardOutboundSession(e2eRoomID) + + testHelper.runBlockingTest { + delay(1_000) + } + } + Log.v("#E2E TEST", "Bob received all and can decrypt") + + // Let's wait a bit to be sure that bob has backed up the session + + Log.v("#E2E TEST", "Force key backup for Bob...") + testHelper.waitWithLatch { latch -> + keysBackupService.backupAllGroupSessions( + null, + TestMatrixCallback(latch, true) + ) + } + Log.v("#E2E TEST", "... Keybackup done for Bob") + + // Now lets logout both alice and bob to ensure that we won't have any gossiping + + val bobUserId = bobSession.myUserId + Log.v("#E2E TEST", "Logout alice and bob...") + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSession) + Log.v("#E2E TEST", "..Logout alice and bob...") + + testHelper.runBlockingTest { + delay(1_000) + } + + // Create a new session for bob + Log.v("#E2E TEST", "Create a new session for Bob") + val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true)) + + // check that bob can't currently decrypt + Log.v("#E2E TEST", "check that bob can't currently decrypt") + sentEventIds.forEach { sentEventId -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId)?.also { + Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") + } + timeLineEvent != null && + timeLineEvent.root.getClearType() == EventType.ENCRYPTED + } + } + } + // after initial sync events are not decrypted, so we have to try manually + ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + + // Let's now import keys from backup + + newBobSession.cryptoService().keysBackupService().let { keysBackupService -> + val keyVersionResult = testHelper.doSync { + keysBackupService.getVersion(version.version, it) + } + + val importedResult = testHelper.doSync { + keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!, + keyBackupPassword, + null, + null, + null, it) + } + + assertEquals(3, importedResult.totalNumberOfKeys) + } + + // ensure bob can now decrypt + ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + } + + private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) { + sentEventIds.forEachIndexed { index, sentEventId -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val event = session.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } + } catch (error: MXCryptoError) { + // nop + } + } + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel()?.body + } + } + } + } + + /** + * Check that a new verified session that was not supposed to get the keys initially will + * get them from an older one. + */ + @Test + fun testSimpleGossip() { + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + cryptoTestHelper.initializeCrossSigning(bobSession) + + // let's send a few message to bob + val sentEventIds = mutableListOf() + val messagesText = listOf("1. Hello", "2. Bob") + + Log.v("#E2E TEST", "Alice sends some messages") + messagesText.forEach { text -> + val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + sentEventIds.add(it) + } + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + + // Ensure bob can decrypt + ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID) + + // Let's now add a new bob session + // Create a new session for bob + Log.v("#E2E TEST", "Create a new session for Bob") + val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + // check that new bob can't currently decrypt + Log.v("#E2E TEST", "check that new bob can't currently decrypt") + + ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + + // Try to request + sentEventIds.forEach { sentEventId -> + val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root + newBobSession.cryptoService().requestRoomKeyForEvent(event) + } + + // wait a bit + testHelper.runBlockingTest { + delay(10_000) + } + + // Ensure that new bob still can't decrypt (keys must have been withheld) + ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD) + + // Now mark new bob session as verified + + bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) + newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!) + + + // now let new session re-request + sentEventIds.forEach { sentEventId -> + val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root + newBobSession.cryptoService().reRequestRoomKeyForEvent(event) + } + + // wait a bit + testHelper.runBlockingTest { + delay(10_000) + } + + ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + + } + + private fun ensureIsDecrypted(sentEventIds: MutableList, session: Session, e2eRoomID: String) { + testHelper.waitWithLatch { latch -> + sentEventIds.forEach { sentEventId -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = session.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + } + + private fun ensureCannotDecrypt(sentEventIds: MutableList, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) { + sentEventIds.forEach { sentEventId -> + val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(event, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + if (expectedError == null) { + Assert.assertTrue(errorType != null) + } else { + assertEquals(expectedError, errorType, "Message expected to be UISI") + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt deleted file mode 100644 index d2b2495b76..0000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/SimpleE2EEChatTest.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.Log -import androidx.test.filters.LargeTest -import kotlinx.coroutines.delay -import org.junit.Assert -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -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.Room -import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.SessionTestParams - -@RunWith(JUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -@LargeTest -class SimpleE2EEChatTest : InstrumentedTest { - - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - - /** - * Simple test that create an e2ee room. - * Some new members are added, and a message is sent. - * We check that the message is e2e and can be decrypted. - * - * Additional users join, we check that they can't decrypt history - * - * Alice sends a new message, then check that the new one can be decrypted - */ - @Test - fun testSendingE2EEMessages() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val aliceSession = cryptoTestData.firstSession - val e2eRoomID = cryptoTestData.roomId - - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - - // add some more users and invite them - val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)) - } - - Log.v("#E2E TEST", "All accounts created") - // we want to invite them in the room - otherAccounts.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.invite(it.myUserId) - } - } - - // All user should accept invite - otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) - Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") - } - - // check that alice see them as joined (not really necessary?) - ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID) - - Log.v("#E2E TEST", "All users have joined the room") - - Log.v("#E2E TEST", "Alice is sending the message") - - val text = "This is my message" - val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text) - // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() - Assert.assertTrue("Message should be sent", sentEventId != null) - - // All should be able to decrypt - otherAccounts.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } - } - } - - // Add a new user to the room, and check that he can't decrypt - val newAccount = listOf("adam") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)) - } - - newAccount.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.invite(it.myUserId) - } - } - - newAccount.forEach { - waitForAndAcceptInviteInRoom(it, e2eRoomID) - } - - ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) - - // wait a bit - testHelper.runBlockingTest { - delay(3_000) - } - - // check that messages are encrypted (uisi) - newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!).also { - Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timeLineEvent != null && - timeLineEvent.root.getClearType() == EventType.ENCRYPTED && - timeLineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID - } - } - } - - // Let alice send a new message - Log.v("#E2E TEST", "Alice sends a new message") - - val secondMessage = "2 This is my message" - val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage) - - // new members should be able to decrypt it - newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondSentEventId!!).also { - Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timeLineEvent != null && - timeLineEvent.root.getClearType() == EventType.MESSAGE && - secondMessage.equals(timeLineEvent.root.getClearContent().toModel()?.body) - } - } - } - - otherAccounts.forEach { - testHelper.signOutAndClose(it) - } - newAccount.forEach { testHelper.signOutAndClose(it) } - - cryptoTestData.cleanUp(testHelper) - } - - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendTextMessage(text) - var sentEventId: String? = null - testHelper.waitWithLatch(4 * 60_000) { - val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60)) - timeline.start() - - testHelper.retryPeriodicallyWithLatch(it) { - val decryptedMsg = timeline.getSnapshot() - .filter { it.root.getClearType() == EventType.MESSAGE } - .also { - Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } - - timeline.dispose() - } - return sentEventId - } - - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - otherAccounts.map { - aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } - } - } - } - - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } - } - } - } - - testHelper.runBlockingTest(60_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } - } - - Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } - } - } -} From 9b3c5d21530b8be5b173bd1bdd3ba32034a53f9e Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 22:00:39 +0100 Subject: [PATCH 056/405] Improve inbound group session cache + mutex --- .../crypto/InboundGroupSessionStore.kt | 68 ++++++++++++------- .../sdk/internal/crypto/MXOlmDevice.kt | 63 ++++++++--------- .../sdk/internal/crypto/OlmSessionStore.kt | 2 +- .../crypto/algorithms/IMXGroupEncryption.kt | 2 +- .../algorithms/megolm/MXMegolmDecryption.kt | 37 ++++++---- .../algorithms/megolm/MXMegolmEncryption.kt | 63 +++++++++-------- .../crypto/store/db/RealmCryptoStore.kt | 41 ++--------- 7 files changed, 138 insertions(+), 138 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index 2532062c12..6deecafdd7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto import android.util.LruCache import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -28,6 +30,14 @@ import java.util.Timer import java.util.TimerTask import javax.inject.Inject +data class InboundGroupSessionHolder( + val wrapper: OlmInboundGroupSessionWrapper2, + val mutex: Mutex = Mutex() +) + + +private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO) + /** * 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 @@ -42,12 +52,13 @@ internal class InboundGroupSessionStore @Inject constructor( val senderKey: String ) - private val sessionCache = object : LruCache(30) { - override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) { - if (evicted && oldValue != null) { + private val sessionCache = object : LruCache(100) { + override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) { + if (oldValue != null) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}") - store.storeInboundGroupSessions(listOf(oldValue)) + Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") + store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) + oldValue.wrapper.olmInboundGroupSession?.releaseSession() } } } @@ -59,41 +70,50 @@ internal class InboundGroupSessionStore @Inject constructor( private val dirtySession = mutableListOf() @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) - } - } + fun clear() { + sessionCache.evictAll() } @Synchronized - fun replaceGroupSession(old: OlmInboundGroupSessionWrapper2, new: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { - Timber.v("## Replacing outdated session ${old.roomId}-${old.senderKey}") - dirtySession.remove(old) + fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? { + val known = sessionCache[CacheKey(sessionId, senderKey)] + Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}") + return known + ?: store.getInboundGroupSession(sessionId, senderKey)?.also { + Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}") + sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it)) + }?.let { + InboundGroupSessionHolder(it) + } + } + + @Synchronized + fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") + dirtySession.remove(old.wrapper) store.removeInboundGroupSession(sessionId, senderKey) sessionCache.remove(CacheKey(sessionId, senderKey)) + // release removed session + old.wrapper.olmInboundGroupSession?.releaseSession() + internalStoreGroupSession(new, sessionId, senderKey) } @Synchronized - fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { - internalStoreGroupSession(wrapper, sessionId, senderKey) + fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + internalStoreGroupSession(holder, sessionId, senderKey) } - private fun internalStoreGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { - Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}") + private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") // We want to batch this a bit for performances - dirtySession.add(wrapper) + dirtySession.add(holder.wrapper) if (sessionCache[CacheKey(sessionId, senderKey)] == null) { // first time seen, put it in memory cache while waiting for batch insert // If it's already known, no need to update cache it's already there - sessionCache.put(CacheKey(sessionId, senderKey), wrapper) + sessionCache.put(CacheKey(sessionId, senderKey), holder) } timerTask?.cancel() @@ -110,7 +130,7 @@ internal class InboundGroupSessionStore @Inject constructor( val toSave = mutableListOf().apply { addAll(dirtySession) } dirtySession.clear() cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}") + Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") tryOrNull { store.storeInboundGroupSessions(toSave) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index f84d259d6d..6ab60f7fd7 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -208,6 +208,7 @@ internal class MXOlmDevice @Inject constructor( it.groupSession.releaseSession() } outboundGroupSessionCache.clear() + inboundGroupSessionStore.clear() olmSessionStore.clear() } @@ -585,7 +586,7 @@ internal class MXOlmDevice @Inject constructor( if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { try { return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) - } catch (e: Exception) { + } catch (e: Throwable) { Timber.e(e, "## encryptGroupMessage() : failed") } } @@ -614,7 +615,8 @@ internal class MXOlmDevice @Inject constructor( keysClaimed: Map, exportFormat: Boolean): Boolean { val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) - val existingSession = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSession = existingSessionHolder?.wrapper // If we have an existing one we should check if the new one is not better if (existingSession != null) { Timber.d("## addInboundGroupSession() check if known session is better than candidate session") @@ -666,9 +668,9 @@ internal class MXOlmDevice @Inject constructor( candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSession, candidateSession, sessionId, senderKey) + inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) } else { - inboundGroupSessionStore.storeInBoundGroupSession(candidateSession, sessionId, senderKey) + inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) } return true @@ -715,7 +717,8 @@ internal class MXOlmDevice @Inject constructor( continue } - val existingSession = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSession = existingSessionHolder?.wrapper if (existingSession == null) { // Session does not already exist, add it @@ -736,7 +739,7 @@ internal class MXOlmDevice @Inject constructor( candidateOlmInboundGroupSession.releaseSession() } else { // update cache with better session - inboundGroupSessionStore.replaceGroupSession(existingSession, candidateSessionToImport, sessionId, senderKey) + inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSessionToImport), sessionId, senderKey) sessions.add(candidateSessionToImport) } } @@ -748,18 +751,6 @@ internal class MXOlmDevice @Inject constructor( return sessions } - /** - * Remove an inbound group session - * - * @param sessionId the session identifier. - * @param sessionKey base64-encoded secret key. - */ - fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) { - if (null != sessionId && null != sessionKey) { - store.removeInboundGroupSession(sessionId, sessionKey) - } - } - /** * Decrypt a received message with an inbound group session. * @@ -771,17 +762,22 @@ internal class MXOlmDevice @Inject constructor( * @return the decrypting result. Nil if the sessionId is unknown. */ @Throws(MXCryptoError::class) - fun decryptGroupMessage(body: String, - roomId: String, - timeline: String?, - sessionId: String, - senderKey: String): OlmDecryptionResult { - val session = getInboundGroupSession(sessionId, senderKey, roomId) + suspend fun decryptGroupMessage(body: String, + roomId: String, + timeline: String?, + sessionId: String, + senderKey: String): OlmDecryptionResult { + val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) + val wrapper = sessionHolder.wrapper + val inboundGroupSession = wrapper.olmInboundGroupSession + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") // Check that the room id matches the original one for the session. This stops // the HS pretending a message was targeting a different room. - if (roomId == session.roomId) { + if (roomId == wrapper.roomId) { val decryptResult = try { - session.olmInboundGroupSession!!.decryptMessage(body) + sessionHolder.mutex.withLock { + inboundGroupSession.decryptMessage(body) + } } catch (e: OlmException) { Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") throw MXCryptoError.OlmError(e) @@ -801,7 +797,7 @@ internal class MXOlmDevice @Inject constructor( timelineSet.add(messageIndexKey) } - inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey) + inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) val payload = try { val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) @@ -813,12 +809,12 @@ internal class MXOlmDevice @Inject constructor( return OlmDecryptionResult( payload, - session.keysClaimed, + wrapper.keysClaimed, senderKey, - session.forwardingCurve25519KeyChain + wrapper.forwardingCurve25519KeyChain ) } else { - val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) Timber.e("## decryptGroupMessage() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) } @@ -885,12 +881,13 @@ internal class MXOlmDevice @Inject constructor( * @param senderKey the base64-encoded curve25519 key of the sender. * @return the inbound group session. */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 { + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) } - val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) + val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) + val session = holder?.wrapper if (session != null) { // Check that the room id matches the original one for the session. This stops @@ -900,7 +897,7 @@ internal class MXOlmDevice @Inject constructor( Timber.e("## getInboundGroupSession() : $errorDescription") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) } else { - return session + return holder } } else { Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index bd59916658..e914ea9d6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -45,7 +45,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS @Synchronized fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { // This could be a newly created session or one that was just created - // Anyhow we should persist ratchet state for futur app lifecycle + // Anyhow we should persist ratchet state for future app lifecycle addNewSessionInCache(olmSessionWrapper, deviceKey) store.storeSession(olmSessionWrapper, deviceKey) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt index 1fd5061a65..6f488def0a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt @@ -45,7 +45,7 @@ internal interface IMXGroupEncryption { * * @return true in case of success */ - suspend fun reshareKey(sessionId: String, + suspend fun reshareKey(groupSessionId: String, userId: String, deviceId: String, senderKey: String): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index f7ab12adeb..e9aceda092 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -79,7 +80,7 @@ internal class MXMegolmDecryption(private val userId: String, } @Throws(MXCryptoError::class) - private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { + private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) @@ -345,7 +346,23 @@ internal class MXMegolmDecryption(private val userId: String, return } val userId = request.userId ?: return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + + val body = request.requestBody + val sessionHolder = try { + olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body") + return@launch + } + + val export = sessionHolder.mutex.withLock { + sessionHolder.wrapper.exportKeys() + } ?: return@launch Unit.also { + Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}") + } + runCatching { deviceListManager.downloadKeys(listOf(userId), false) } .mapCatching { val deviceId = request.deviceId @@ -355,7 +372,6 @@ internal class MXMegolmDecryption(private val userId: String, } else { val devicesByUser = mapOf(userId to listOf(deviceInfo)) val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val body = request.requestBody val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) if (olmSessionResult?.sessionId == null) { // no session with this device, probably because there @@ -365,19 +381,10 @@ internal class MXMegolmDecryption(private val userId: String, } Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") - val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) - runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } - .fold( - { - // TODO - payloadJson["content"] = it.exportKeys() ?: "" - }, - { - // TODO - Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body") - } - - ) + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 389036a1f8..23e8749a04 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -432,20 +433,20 @@ internal class MXMegolmEncryption( } } - override suspend fun reshareKey(sessionId: String, + override suspend fun reshareKey(groupSessionId: String, userId: String, deviceId: String, senderKey: String): Boolean { - Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId") + Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId") val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") } // Get the chain index of the key we previously sent this device - val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo) + val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo) if (!wasSessionSharedWithUser.found) { // This session was never shared with this user // Send a room key with held - notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) + notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED) Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device") return false } @@ -456,42 +457,48 @@ internal class MXMegolmEncryption( } val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) - olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys. - // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. - ?: return false.also { - Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") - } + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + null + } + val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + return false.also { + Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") + } + } + Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}") - Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") + val sessionHolder = try { + olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId") + return false + } - val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) + val export = sessionHolder.mutex.withLock { + sessionHolder.wrapper.exportKeys() + } ?: return false.also { + Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${groupSessionId}") + } - runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) } - .fold( - { - // TODO - payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: "" - }, - { - // TODO - Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId") - } - - ) + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId") + Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) return try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId") + Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId") true } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId") false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index fec2e04458..c2d2fbc681 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -104,7 +104,6 @@ import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject -import kotlin.collections.set @SessionScope internal class RealmCryptoStore @Inject constructor( @@ -124,12 +123,6 @@ internal class RealmCryptoStore @Inject constructor( // The olm account private var olmAccount: OlmAccount? = null - // Cache for OlmSession, to release them properly -// private val olmSessionsToRelease = HashMap() - - // Cache for InboundGroupSession, to release them properly - private val inboundGroupSessionToRelease = HashMap() - private val newSessionListeners = ArrayList() override fun addNewSessionListener(listener: NewSessionListener) { @@ -213,11 +206,6 @@ internal class RealmCryptoStore @Inject constructor( monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) } - inboundGroupSessionToRelease.forEach { - it.value.olmInboundGroupSession?.releaseSession() - } - inboundGroupSessionToRelease.clear() - olmAccount?.releaseAccount() realmLocker?.close() @@ -745,13 +733,6 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) - // Release memory of previously known session, if it is not the same one - if (inboundGroupSessionToRelease[key] != session) { - inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() - } - - inboundGroupSessionToRelease[key] = session - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { primaryKey = key sessionId = sessionIdentifier @@ -768,20 +749,12 @@ internal class RealmCryptoStore @Inject constructor( override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - // If not in cache (or not found), try to read it from realm - if (inboundGroupSessionToRelease[key] == null) { - doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - ?.getInboundGroupSession() - } - ?.let { - inboundGroupSessionToRelease[key] = it - } + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.getInboundGroupSession() } - - return inboundGroupSessionToRelease[key] } override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? { @@ -837,10 +810,6 @@ internal class RealmCryptoStore @Inject constructor( override fun removeInboundGroupSession(sessionId: String, senderKey: String) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - // Release memory of previously known session - inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() - inboundGroupSessionToRelease.remove(key) - doRealmTransaction(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) From ade16a0aa16e868d206a2f90b7963991d87d6396 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 23:25:20 +0100 Subject: [PATCH 057/405] protect race on prekey + logs --- .../crypto/algorithms/olm/MXOlmDecryption.kt | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 6679cb7b83..3ee3b4d74e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.olm +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.logger.LoggerTag 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 @@ -30,6 +32,8 @@ import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.util.convertFromUTF8 import timber.log.Timber + +private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO) internal class MXOlmDecryption( // The olm device interface private val olmDevice: MXOlmDevice, @@ -40,25 +44,25 @@ internal class MXOlmDecryption( @Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val olmEventContent = event.content.toModel() ?: run { - Timber.e("## decryptEvent() : bad event format") + Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) } val cipherText = olmEventContent.ciphertext ?: run { - Timber.e("## decryptEvent() : missing cipher text") + Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) } val senderKey = olmEventContent.senderKey ?: run { - Timber.e("## decryptEvent() : missing sender key") + Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) } val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { - Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") + Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) } @@ -69,7 +73,7 @@ internal class MXOlmDecryption( val decryptedPayload = decryptMessage(message, senderKey) if (decryptedPayload == null) { - Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") + Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } val payloadString = convertFromUTF8(decryptedPayload) @@ -78,30 +82,30 @@ internal class MXOlmDecryption( val payload = adapter.fromJson(payloadString) if (payload == null) { - Timber.e("## decryptEvent failed : null payload") + Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) } val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { - Timber.e("## decryptEvent() : bad olmPayloadContent format") + Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) } if (olmPayloadContent.recipient.isNullOrBlank()) { val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") - Timber.e("## decryptEvent() : $reason") + Timber.tag(loggerTag.value).e("## decryptEvent() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) } if (olmPayloadContent.recipient != userId) { - Timber.e("## decryptEvent() : Event ${event.eventId}:" + + Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" + " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) } val recipientKeys = olmPayloadContent.recipientKeys ?: run { - Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + + Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + " property; cannot prevent unknown-key attack") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) @@ -110,31 +114,31 @@ internal class MXOlmDecryption( val ed25519 = recipientKeys["ed25519"] if (ed25519 != olmDevice.deviceEd25519Key) { - Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") + Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, MXCryptoError.BAD_RECIPIENT_KEY_REASON) } if (olmPayloadContent.sender.isNullOrBlank()) { - Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") + Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) } if (olmPayloadContent.sender != event.senderId) { - Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") + Timber.tag(loggerTag.value).e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) } if (olmPayloadContent.roomId != event.roomId) { - Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") + Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)) } val keys = olmPayloadContent.keys ?: run { - Timber.e("## decryptEvent failed : null keys") + Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) } @@ -166,11 +170,32 @@ internal class MXOlmDecryption( // Try each session in turn // decryptionErrors = {}; + + val isPreKey = messageType == 0 + // we want to synchronize on prekey if not we could end up create two olm sessions + // Not very clear but it looks like the js-sdk for consistency + return if (isPreKey) { + olmDevice.mutex.withLock { + reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) + } + } else { + reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) + } + } + + private suspend fun reallyDecryptMessage(sessionIds: List, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? { + Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions") for (sessionId in sessionIds) { - val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) + val payload = try { + olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) + } catch (throwable: Exception) { + // As we are trying one by one, we don't really care of the error here + Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId") + null + } if (null != payload) { - Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") + Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") return payload } else { val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) @@ -178,7 +203,7 @@ internal class MXOlmDecryption( if (foundSession) { // Decryption failed, but it was a prekey message matching this // session, so it should have worked. - Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") + Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") return null } } @@ -189,9 +214,9 @@ internal class MXOlmDecryption( // didn't work. if (sessionIds.isEmpty()) { - Timber.e("## decryptMessage() : No existing sessions") + Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions") } else { - Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") } return null @@ -199,14 +224,17 @@ internal class MXOlmDecryption( // prekey message which doesn't match any existing sessions: make a new // session. + // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions? + Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey") + val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) if (null == res) { - Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") return null } - Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") + Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") return res["payload"] } From 9eb0473d74680fbfc2db8a889299c5a554ba7315 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 23:25:29 +0100 Subject: [PATCH 058/405] better logs --- .../sdk/internal/crypto/MXOlmDevice.kt | 123 +++++++++--------- .../algorithms/megolm/MXMegolmEncryption.kt | 11 +- .../crypto/crosssigning/UpdateTrustWorker.kt | 10 +- 3 files changed, 73 insertions(+), 71 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 6ab60f7fd7..6779b5c8dc 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.crypto +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JsonDict @@ -42,6 +44,8 @@ import timber.log.Timber import java.net.URLEncoder import javax.inject.Inject +private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) + // The libolm wrapper. @SessionScope internal class MXOlmDevice @Inject constructor( @@ -53,6 +57,7 @@ internal class MXOlmDevice @Inject constructor( private val inboundGroupSessionStore: InboundGroupSessionStore ) { + val mutex = Mutex() /** * @return the Curve25519 key for the account. */ @@ -96,26 +101,26 @@ internal class MXOlmDevice @Inject constructor( try { store.getOrCreateOlmAccount() } catch (e: Exception) { - Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") + Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount") } try { olmUtility = OlmUtility() } catch (e: Exception) { - Timber.e(e, "## MXOlmDevice : OlmUtility failed with error") + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error") olmUtility = null } try { deviceCurve25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } } catch (e: Exception) { - Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") } try { deviceEd25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } } catch (e: Exception) { - Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") } } @@ -126,7 +131,7 @@ internal class MXOlmDevice @Inject constructor( try { return doWithOlmAccount { it.oneTimeKeys() } } catch (e: Exception) { - Timber.e(e, "## getOneTimeKeys() : failed") + Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") } return null @@ -159,7 +164,7 @@ internal class MXOlmDevice @Inject constructor( try { return doWithOlmAccount { it.fallbackKey() } } catch (e: Exception) { - Timber.e("## getFallbackKey() : failed") + Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") } return null } @@ -179,7 +184,7 @@ internal class MXOlmDevice @Inject constructor( return true } } catch (e: Exception) { - Timber.e("## generateFallbackKey() : failed") + Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed") } return false } @@ -195,7 +200,7 @@ internal class MXOlmDevice @Inject constructor( store.saveOlmAccount() } } catch (e: Exception) { - Timber.e("## forgetFallbackKey() : failed") + Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed") } } @@ -222,7 +227,7 @@ internal class MXOlmDevice @Inject constructor( try { return doWithOlmAccount { it.signMessage(message) } } catch (e: Exception) { - Timber.e(e, "## signMessage() : failed") + Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") } return null @@ -238,7 +243,7 @@ internal class MXOlmDevice @Inject constructor( store.saveOlmAccount() } } catch (e: Exception) { - Timber.e(e, "## markKeysAsPublished() : failed") + Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed") } } @@ -254,7 +259,7 @@ internal class MXOlmDevice @Inject constructor( store.saveOlmAccount() } } catch (e: Exception) { - Timber.e(e, "## generateOneTimeKeys() : failed") + Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed") } } @@ -267,7 +272,7 @@ internal class MXOlmDevice @Inject constructor( * @return the session id for the outbound session. */ fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { - Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") + Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") var olmSession: OlmSession? = null try { @@ -288,10 +293,10 @@ internal class MXOlmDevice @Inject constructor( val sessionIdentifier = olmSession.sessionIdentifier() - Timber.v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") + Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") return sessionIdentifier } catch (e: Exception) { - Timber.e(e, "## createOutboundSession() failed") + Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed") olmSession?.releaseSession() } @@ -308,7 +313,7 @@ internal class MXOlmDevice @Inject constructor( * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. */ fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { - Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") + Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") var olmSession: OlmSession? = null @@ -319,11 +324,11 @@ internal class MXOlmDevice @Inject constructor( olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) } } catch (e: Exception) { - Timber.e(e, "## createInboundSession() : the session creation failed") + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed") return null } - Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") + Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") try { doWithOlmAccount { olmAccount -> @@ -331,15 +336,15 @@ internal class MXOlmDevice @Inject constructor( store.saveOlmAccount() } } catch (e: Exception) { - Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") } - Timber.v("## createInboundSession() : ciphertext: $ciphertext") + Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext") try { val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8")) - Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256") + Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256") } catch (e: Exception) { - Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") + Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") } val olmMessage = OlmMessage() @@ -357,7 +362,7 @@ internal class MXOlmDevice @Inject constructor( olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { - Timber.e(e, "## createInboundSession() : decryptMessage failed") + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed") } val res = HashMap() @@ -374,7 +379,7 @@ internal class MXOlmDevice @Inject constructor( return res } catch (e: Exception) { - Timber.e(e, "## createInboundSession() : OlmSession creation failed") + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed") olmSession?.releaseSession() } @@ -415,7 +420,7 @@ internal class MXOlmDevice @Inject constructor( if (olmSessionWrapper != null) { try { - Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") + Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") val olmMessage = olmSessionWrapper.mutex.withLock { olmSessionWrapper.olmSession.encryptMessage(payloadString) @@ -427,11 +432,11 @@ internal class MXOlmDevice @Inject constructor( olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } } catch (e: Throwable) { - Timber.e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") + Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") return null } } else { - Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId") + Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId") return null } } @@ -445,6 +450,7 @@ internal class MXOlmDevice @Inject constructor( * @param sessionId the id of the active session. * @return the decrypted payload. */ + @kotlin.jvm.Throws suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { var payloadString: String? = null @@ -455,18 +461,13 @@ internal class MXOlmDevice @Inject constructor( olmMessage.mCipherText = ciphertext olmMessage.mType = messageType.toLong() - try { - payloadString = - olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived() - } + payloadString = + olmSessionWrapper.mutex.withLock { + olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { + olmSessionWrapper.onMessageReceived() } - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) -// store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } catch (e: Exception) { - Timber.e(e, "## decryptMessage() : decryptMessage failed") - } + } + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } return payloadString @@ -505,7 +506,7 @@ internal class MXOlmDevice @Inject constructor( store.storeCurrentOutboundGroupSessionForRoom(roomId, session) return session.sessionIdentifier() } catch (e: Exception) { - Timber.e(e, "createOutboundGroupSession") + Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession") session?.releaseSession() } @@ -557,7 +558,7 @@ internal class MXOlmDevice @Inject constructor( try { return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey() } catch (e: Exception) { - Timber.e(e, "## getSessionKey() : failed") + Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed") } } return null @@ -587,7 +588,7 @@ internal class MXOlmDevice @Inject constructor( try { return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) } catch (e: Throwable) { - Timber.e(e, "## encryptGroupMessage() : failed") + Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed") } } return null @@ -619,46 +620,46 @@ internal class MXOlmDevice @Inject constructor( val existingSession = existingSessionHolder?.wrapper // If we have an existing one we should check if the new one is not better if (existingSession != null) { - Timber.d("## addInboundGroupSession() check if known session is better than candidate session") + Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") try { val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also { // This is quite unexpected, could throw if native was released? - Timber.e("## addInboundGroupSession() null firstKnownIndex on existing session") + Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") candidateSession.olmInboundGroupSession?.releaseSession() // Probably should discard it? } val newKnownFirstIndex = candidateSession.firstKnownIndex // If our existing session is better we keep it if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - Timber.d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") candidateSession.olmInboundGroupSession?.releaseSession() return false } } catch (failure: Throwable) { - Timber.e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") + Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") candidateSession.olmInboundGroupSession?.releaseSession() return false } } - Timber.d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") // sanity check on the new session val candidateOlmInboundSession = candidateSession.olmInboundGroupSession if (null == candidateOlmInboundSession) { - Timber.e("## addInboundGroupSession : invalid session ") + Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") return false } try { if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { - Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") candidateOlmInboundSession.releaseSession() return false } } catch (e: Throwable) { candidateOlmInboundSession.releaseSession() - Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed") + Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") return false } @@ -695,24 +696,24 @@ internal class MXOlmDevice @Inject constructor( try { candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) } catch (e: Exception) { - Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") } // sanity check if (candidateSessionToImport?.olmInboundGroupSession == null) { - Timber.e("## importInboundGroupSession : invalid session") + Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session") continue } val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession try { if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { - Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") candidateOlmInboundGroupSession?.releaseSession() continue } } catch (e: Exception) { - Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed") + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed") candidateOlmInboundGroupSession?.releaseSession() continue } @@ -722,17 +723,17 @@ internal class MXOlmDevice @Inject constructor( if (existingSession == null) { // Session does not already exist, add it - Timber.d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") + Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") sessions.add(candidateSessionToImport) } else { - Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } if (existingFirstKnown == null || candidateFirstKnownIndex == null) { // should not happen? candidateSessionToImport.olmInboundGroupSession?.releaseSession() - Timber.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") + Timber.tag(loggerTag.value).w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") } else { if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { // Ignore this, keep existing @@ -779,7 +780,7 @@ internal class MXOlmDevice @Inject constructor( inboundGroupSession.decryptMessage(body) } } catch (e: OlmException) { - Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") + Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") throw MXCryptoError.OlmError(e) } @@ -790,7 +791,7 @@ internal class MXOlmDevice @Inject constructor( if (timelineSet.contains(messageIndexKey)) { val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.e("## decryptGroupMessage() : $reason") + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) } @@ -803,7 +804,7 @@ internal class MXOlmDevice @Inject constructor( val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) adapter.fromJson(payloadString) } catch (e: Exception) { - Timber.e("## decryptGroupMessage() : fails to parse the payload") + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) } @@ -815,7 +816,7 @@ internal class MXOlmDevice @Inject constructor( ) } else { val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) - Timber.e("## decryptGroupMessage() : $reason") + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) } } @@ -894,13 +895,13 @@ internal class MXOlmDevice @Inject constructor( // the HS pretending a message was targeting a different room. if (roomId != session.roomId) { val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) - Timber.e("## getInboundGroupSession() : $errorDescription") + Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) } else { return holder } } else { - Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.tag(loggerTag.value).w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 23e8749a04..46b0e455a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -89,7 +89,7 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") - Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -143,8 +143,9 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ") val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) - val keysClaimedMap = HashMap() - keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! + val keysClaimedMap = mapOf( + "ed25519" to olmDevice.deviceEd25519Key!! + ) olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, emptyList(), keysClaimedMap, false) @@ -308,7 +309,7 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with ${devicesByUser.entries.map { "${it.key} (${it.value.map { it.deviceId }})" }} ") } } else { Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") @@ -349,7 +350,7 @@ internal class MXMegolmEncryption( try { sendToDeviceTask.execute(params) } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for ${targets.map { "${it.userId}|${it.deviceId}" }} session: $sessionId ") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 5cd647ff6f..9325355d28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -96,7 +96,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses if (userList.isNotEmpty()) { // 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 users: ${userList.logLimit()}") + Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}") updateTrust(userList) } @@ -148,7 +148,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses myUserId -> myTrustResult else -> { crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also { - Timber.d("## CrossSigning - user:${entry.key} result:$it") + Timber.v("## CrossSigning - user:${entry.key} result:$it") } } } @@ -178,7 +178,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses // 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)}") + Timber.v("## 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 @@ -216,7 +216,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true) .findFirst() ?.let { roomSummary -> - Timber.d("## CrossSigning - Check shield state for room $roomId") + Timber.v("## CrossSigning - Check shield state for room $roomId") val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds() try { val updatedTrust = computeRoomShield( @@ -277,7 +277,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses cryptoRealm: Realm, activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { - Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}") + Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}") // The set of “all users” depends on the type of room: // For regular / topic rooms which have more than 2 members (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 From 11e888162c0b7125d3fe4385afb6cd28619809bd Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 23:25:45 +0100 Subject: [PATCH 059/405] test forward better key --- .../sdk/internal/crypto/E2eeSanityTests.kt | 318 ++++++++++++------ 1 file changed, 224 insertions(+), 94 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index a8aa3e55d3..f3c22772e0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreat import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -184,72 +185,6 @@ class E2eeSanityTests : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendTextMessage(text) - var sentEventId: String? = null - testHelper.waitWithLatch(4 * 60_000) { - val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60)) - timeline.start() - - testHelper.retryPeriodicallyWithLatch(it) { - val decryptedMsg = timeline.getSnapshot() - .filter { it.root.getClearType() == EventType.MESSAGE } - .also { - Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } - - timeline.dispose() - } - return sentEventId - } - - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - otherAccounts.map { - aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } - } - } - } - - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } - } - } - } - - testHelper.runBlockingTest(60_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } - } - - Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } - } - } - /** * Quick test for basic keybackup * 1. Create e2e between Alice and Bob @@ -373,32 +308,8 @@ class E2eeSanityTests : InstrumentedTest { // ensure bob can now decrypt ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) - } - private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) { - sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val event = session.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "").let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } - } catch (error: MXCryptoError) { - // nop - } - } - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel()?.body - } - } - } + testHelper.signOutAndClose(newBobSession) } /** @@ -469,7 +380,6 @@ class E2eeSanityTests : InstrumentedTest { bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!) - // now let new session re-request sentEventIds.forEach { sentEventId -> val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root @@ -483,9 +393,229 @@ class E2eeSanityTests : InstrumentedTest { ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(newBobSession) } - private fun ensureIsDecrypted(sentEventIds: MutableList, session: Session, e2eRoomID: String) { + /** + * Test that if a better key is forwared (lower index, it is then used) + */ + @Test + fun testForwardBetterKey() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSessionWithBetterKey = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey) + + // let's send a few message to bob + var firstEventId: String + val firstMessage = "1. Hello" + + Log.v("#E2E TEST", "Alice sends some messages") + firstMessage.let { text -> + firstEventId = sendMessageInRoom(aliceRoomPOV, text)!! + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimeLineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + + // Ensure bob can decrypt + ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) + + // Let's add a new unverified session from bob + val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) + + // check that new bob can't currently decrypt + Log.v("#E2E TEST", "check that new bob can't currently decrypt") + ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + + // Now let alice send a new message. this time the new bob session will be able to decrypt + var secondEventId: String + val secondMessage = "2. New Device?" + + Log.v("#E2E TEST", "Alice sends some messages") + secondMessage.let { text -> + secondEventId = sendMessageInRoom(aliceRoomPOV, text)!! + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } + } + } + + // check that both messages have same sessionId (it's just that we don't have index 0) + val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimeLineEvent(firstEventId) + val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondEventId) + + val firstSessionId = firstEventNewBobPov!!.root.content.toModel()!!.sessionId!! + val secondSessionId = secondEventNewBobPov!!.root.content.toModel()!!.sessionId!! + + Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) + + // Confirm we can decrypt one but not the other + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType) + } + } + + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt event") + } + } + + // Now let's verify bobs session, and re-request keys + bobSessionWithBetterKey.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) + + newBobSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) + + // now let new session request + newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root) + + // wait a bit + testHelper.runBlockingTest { + delay(10_000) + } + + // old session should have shared the key at earliest known index now + // we should be able to decrypt both + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt first event now $error") + } + } + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt event $error") + } + } + + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(newBobSession) + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { + aliceRoomPOV.sendTextMessage(text) + var sentEventId: String? = null + testHelper.waitWithLatch(4 * 60_000) { + val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60)) + timeline.start() + + testHelper.retryPeriodicallyWithLatch(it) { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { + Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null + } + + timeline.dispose() + } + return sentEventId + } + + private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + otherAccounts.map { + aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + } + + private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") + } + } + } + } + + testHelper.runBlockingTest(60_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } + + private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) { + sentEventIds.forEachIndexed { index, sentEventId -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val event = session.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } + } catch (error: MXCryptoError) { + // nop + } + } + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel()?.body + } + } + } + } + + private fun ensureIsDecrypted(sentEventIds: List, session: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> sentEventIds.forEach { sentEventId -> testHelper.retryPeriodicallyWithLatch(latch) { @@ -498,7 +628,7 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun ensureCannotDecrypt(sentEventIds: MutableList, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) { + private fun ensureCannotDecrypt(sentEventIds: List, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) { sentEventIds.forEach { sentEventId -> val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root testHelper.runBlockingTest { From 2f665dd08f3f562cff4934b26c260d324ccc2d38 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 28 Feb 2022 23:49:20 +0100 Subject: [PATCH 060/405] cleaning --- .../android/sdk/internal/crypto/E2eeSanityTests.kt | 3 +-- .../sdk/internal/crypto/InboundGroupSessionStore.kt | 1 - .../matrix/android/sdk/internal/crypto/MXOlmDevice.kt | 11 +++++++++-- .../android/sdk/internal/crypto/OlmSessionStore.kt | 6 +++--- .../crypto/algorithms/megolm/MXMegolmDecryption.kt | 1 - .../crypto/algorithms/megolm/MXMegolmEncryption.kt | 7 ++++--- .../internal/crypto/algorithms/olm/MXOlmDecryption.kt | 10 ++++++---- 7 files changed, 23 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index f3c22772e0..da14c9a2c5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -318,7 +318,6 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testSimpleGossip() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index 6deecafdd7..34bef61c98 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -35,7 +35,6 @@ data class InboundGroupSessionHolder( val mutex: Mutex = Mutex() ) - private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 6779b5c8dc..2be08915c6 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -58,6 +58,7 @@ internal class MXOlmDevice @Inject constructor( ) { val mutex = Mutex() + /** * @return the Curve25519 key for the account. */ @@ -733,14 +734,20 @@ internal class MXOlmDevice @Inject constructor( if (existingFirstKnown == null || candidateFirstKnownIndex == null) { // should not happen? candidateSessionToImport.olmInboundGroupSession?.releaseSession() - Timber.tag(loggerTag.value).w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") + Timber.tag(loggerTag.value) + .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") } else { if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { // Ignore this, keep existing candidateOlmInboundGroupSession.releaseSession() } else { // update cache with better session - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSessionToImport), sessionId, senderKey) + inboundGroupSessionStore.replaceGroupSession( + existingSessionHolder, + InboundGroupSessionHolder(candidateSessionToImport), + sessionId, + senderKey + ) sessions.add(candidateSessionToImport) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index e914ea9d6a..989c9ab86e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO) * Access is synchronized for thread safety */ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) { - /* - * map of device key to list of olm sessions (it is possible to have several active sessions with a device) + /** + * map of device key to list of olm sessions (it is possible to have several active sessions with a device) */ private val olmSessions = HashMap>() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index e9aceda092..e94daa0e76 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -348,7 +348,6 @@ internal class MXMegolmDecryption(private val userId: String, val userId = request.userId ?: return cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val body = request.requestBody val sessionHolder = try { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 46b0e455a8..37e81df25d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -309,7 +309,7 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with ${devicesByUser.entries.map { "${it.key} (${it.value.map { it.deviceId }})" }} ") + Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>") } } else { Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") @@ -350,7 +350,8 @@ internal class MXMegolmEncryption( try { sendToDeviceTask.execute(params) } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for ${targets.map { "${it.userId}|${it.deviceId}" }} session: $sessionId ") + Timber.tag(loggerTag.value) + .e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}") } } @@ -481,7 +482,7 @@ internal class MXMegolmEncryption( val export = sessionHolder.mutex.withLock { sessionHolder.wrapper.exportKeys() } ?: return false.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${groupSessionId}") + Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId") } val payloadJson = mapOf( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 3ee3b4d74e..afa249801d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -32,7 +32,6 @@ import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.util.convertFromUTF8 import timber.log.Timber - private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO) internal class MXOlmDecryption( // The olm device interface @@ -120,19 +119,22 @@ internal class MXOlmDecryption( } if (olmPayloadContent.sender.isNullOrBlank()) { - Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") + Timber.tag(loggerTag.value) + .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) } if (olmPayloadContent.sender != event.senderId) { - Timber.tag(loggerTag.value).e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") + Timber.tag(loggerTag.value) + .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) } if (olmPayloadContent.roomId != event.roomId) { - Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") + Timber.tag(loggerTag.value) + .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)) } From 122e785f14941868d364e35902720828127ad28e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 00:06:03 +0100 Subject: [PATCH 061/405] clean test --- .../sdk/account/AccountCreationTest.kt | 62 ++++--------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index 6f25b24d9c..cb7de260b6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.junit.FixMethodOrder @@ -123,24 +124,9 @@ class AccountCreationTest : InstrumentedTest { val eventList = listOf(eventBobPOV, secondEventBobPOV, thirdEventBobPOV, forthEventBobPOV) -// commonTestHelper.runBlockingTest { -// val export = bobSession.cryptoService().exportRoomKeys("foo") - -// } val atomicAsError = AtomicBoolean() val deff = mutableListOf>() -// for (i in 1..100) { -// GlobalScope.launch { -// val index = Random.nextInt(eventList.size) -// try { -// val event = eventList[index] -// bobSession.cryptoService().decryptEvent(event.root, "") -// Log.d("#TEST", "Decrypt Success $index :${Thread.currentThread().name}") -// } catch (failure: Throwable) { -// Log.d("#TEST", "Failed to decrypt $index :$failure") -// } -// } -// } + val cryptoService = bobSession.cryptoService() coroutineScope.launch { @@ -164,27 +150,6 @@ class AccountCreationTest : InstrumentedTest { deff.add(it) } } -// coroutineScope.async { -// val index = Random.nextInt(eventList.size) -// try { -// val event = eventList[index] -// cryptoService.decryptEvent(event.root, "") -// for (other in eventList.indices) { -// if (other != index) { -// cryptoService.decryptEventAsync(eventList[other].root, "", object : MatrixCallback { -// override fun onFailure(failure: Throwable) { -// Log.e("#TEST", "Failed to decrypt $spawn/$index :$failure") -// } -// }) -// } -// } -// Log.d("#TEST", "[$spawn] Decrypt Success $index :${Thread.currentThread().name}") -// } catch (failure: Throwable) { -// Log.e("#TEST", "Failed to decrypt $spawn/$index :$failure") -// } -// }.let { -// deff.add(it) -// } } coroutineScope.launch { @@ -198,20 +163,15 @@ class AccountCreationTest : InstrumentedTest { deff.awaitAll() delay(10_000) assert(!atomicAsError.get()) - // There should be no errors? -// deff.map { it.await() }.forEach { -// it.fold({ -// Log.d("#TEST", "Decrypt Success :${it}") -// }, { -// Log.d("#TEST", "Failed to decrypt :$it") -// }) -// val hasFailure = deff.any { it.await().exceptionOrNull() != null } -// assert(!hasFailure) -// } - - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) - commonTestHelper.signOutAndClose(bobSession2) + // There should be no errors } + + + + coroutineScope.cancel() + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + commonTestHelper.signOutAndClose(bobSession2) } } From 2d9beb67b4f8aa1133a1b351bff98fce1becd4f7 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 00:09:38 +0100 Subject: [PATCH 062/405] extract test to dedicated class --- .../sdk/account/AccountCreationTest.kt | 112 ------------- .../crypto/ConcurrentDecryptionTest.kt | 148 ++++++++++++++++++ 2 files changed, 148 insertions(+), 112 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index cb7de260b6..486bc02769 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -16,17 +16,7 @@ package org.matrix.android.sdk.account -import android.util.Log import androidx.test.filters.LargeTest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.junit.FixMethodOrder import org.junit.Ignore import org.junit.Test @@ -38,9 +28,6 @@ import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.random.Random @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -75,103 +62,4 @@ class AccountCreationTest : InstrumentedTest { res.cleanUp(commonTestHelper) } - - @Test - fun testConcurrentDecrypt() { -// val res = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - // ============================= - // ARRANGE - // ============================= - - val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - cryptoTestHelper.initializeCrossSigning(bobSession) - val bobSession2 = commonTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - - bobSession2.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId ?: "") - bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession2.sessionParams.deviceId ?: "") - - val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) - val roomAlicePOV = aliceSession.getRoom(roomId)!! - - // ============================= - // ACT - // ============================= - - val timelineEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() - val secondEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 2", 1).first() - val thirdEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 3", 1).first() - val forthEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 4", 1).first() - - // await for bob unverified session to get the message - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId) != null - } - } - - val eventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! - val secondEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)!! - val thirdEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(thirdEvent.eventId)!! - val forthEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId)!! - - // let's try to decrypt concurrently and check that we are not getting exceptions - val dispatcher = Executors - .newFixedThreadPool(100) - .asCoroutineDispatcher() - val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - - val eventList = listOf(eventBobPOV, secondEventBobPOV, thirdEventBobPOV, forthEventBobPOV) - - val atomicAsError = AtomicBoolean() - val deff = mutableListOf>() - - val cryptoService = bobSession.cryptoService() - - coroutineScope.launch { - for (spawn in 1..100) { - delay((Random.nextFloat() * 1000).toLong()) - aliceSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) - } - } - - for (spawn in 1..8000) { - eventList.random().let { event -> - coroutineScope.async { - try { - cryptoService.decryptEvent(event.root, "") - Log.d("#TEST", "[$spawn] Decrypt Success ${event.eventId} :${Thread.currentThread().name}") - } catch (failure: Throwable) { - atomicAsError.set(true) - Log.e("#TEST", "Failed to decrypt $spawn/${event.eventId} :$failure") - } - }.let { - deff.add(it) - } - } - } - - coroutineScope.launch { - for (spawn in 1..100) { - delay((Random.nextFloat() * 1000).toLong()) - bobSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) - } - } - - commonTestHelper.runBlockingTest(10 * 60_000) { - deff.awaitAll() - delay(10_000) - assert(!atomicAsError.get()) - // There should be no errors - } - - - - coroutineScope.cancel() - - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) - commonTestHelper.signOutAndClose(bobSession2) - } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt new file mode 100644 index 0000000000..4ac6314955 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.random.Random + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class ConcurrentDecryptionTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun testConcurrentDecrypt() { +// val res = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + // ============================= + // ARRANGE + // ============================= + + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) + cryptoTestHelper.initializeCrossSigning(bobSession) + val bobSession2 = commonTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + bobSession2.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId ?: "") + bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession2.sessionParams.deviceId ?: "") + + val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + // ============================= + // ACT + // ============================= + + val timelineEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + val secondEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 2", 1).first() + val thirdEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 3", 1).first() + val forthEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 4", 1).first() + + // await for bob unverified session to get the message + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId) != null + } + } + + val eventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! + val secondEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)!! + val thirdEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(thirdEvent.eventId)!! + val forthEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId)!! + + // let's try to decrypt concurrently and check that we are not getting exceptions + val dispatcher = Executors + .newFixedThreadPool(100) + .asCoroutineDispatcher() + val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + + val eventList = listOf(eventBobPOV, secondEventBobPOV, thirdEventBobPOV, forthEventBobPOV) + + val atomicAsError = AtomicBoolean() + val deff = mutableListOf>() + + val cryptoService = bobSession.cryptoService() + + coroutineScope.launch { + for (spawn in 1..100) { + delay((Random.nextFloat() * 1000).toLong()) + aliceSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) + } + } + + for (spawn in 1..8000) { + eventList.random().let { event -> + coroutineScope.async { + try { + cryptoService.decryptEvent(event.root, "") + Log.d("#TEST", "[$spawn] Decrypt Success ${event.eventId} :${Thread.currentThread().name}") + } catch (failure: Throwable) { + atomicAsError.set(true) + Log.e("#TEST", "Failed to decrypt $spawn/${event.eventId} :$failure") + } + }.let { + deff.add(it) + } + } + } + + coroutineScope.launch { + for (spawn in 1..100) { + delay((Random.nextFloat() * 1000).toLong()) + bobSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) + } + } + + commonTestHelper.runBlockingTest(10 * 60_000) { + deff.awaitAll() + delay(10_000) + assert(!atomicAsError.get()) + // There should be no errors + } + + coroutineScope.cancel() + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + commonTestHelper.signOutAndClose(bobSession2) + } +} From f2387394385eb41b78cdbcc508078712eddceef8 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 16:18:43 +0100 Subject: [PATCH 063/405] Clean ensure olm, fix unwedging, better logs --- .../internal/crypto/DefaultCryptoService.kt | 8 + .../sdk/internal/crypto/EventDecryptor.kt | 157 +++++++++++------- .../EnsureOlmSessionsForDevicesAction.kt | 115 ++++++------- .../internal/crypto/model/CryptoDeviceInfo.kt | 2 + .../session/sync/handler/CryptoSyncHandler.kt | 4 +- 5 files changed, 165 insertions(+), 121 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index a66e0d4077..2667c25ea4 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -434,6 +434,14 @@ internal class DefaultCryptoService @Inject constructor( val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) } + + // unwedge if needed + try { + eventDecryptor.unwedgeDevicesIfNeeded() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed") + } + // There is a limit of to_device events returned per sync. // If we are in a case of such limited to_device sync we can't try to generate/upload // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 8a11e45740..9bf5cd594e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -18,17 +18,15 @@ 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.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag 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.MXOlmSessionResult 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 @@ -40,6 +38,8 @@ import javax.inject.Inject private const val SEND_TO_DEVICE_RETRY_COUNT = 3 +private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) + @SessionScope internal class EventDecryptor @Inject constructor( private val cryptoCoroutineScope: CoroutineScope, @@ -47,13 +47,22 @@ internal class EventDecryptor @Inject constructor( private val roomDecryptorProvider: RoomDecryptorProvider, private val messageEncrypter: MessageEncrypter, private val sendToDeviceTask: SendToDeviceTask, + private val deviceListManager: DeviceListManager, 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() + /** + * Rate limit unwedge attempt, should we persist that? + */ + private val lastNewSessionForcedDates = mutableMapOf() + + data class WedgedDeviceInfo( + val userId: String, + val senderKey: String? + ) + + private val wedgedDevices = mutableListOf() /** * Decrypt an event @@ -94,35 +103,29 @@ internal class EventDecryptor @Inject constructor( private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content if (eventContent == null) { - Timber.e("## CRYPTO | decryptEvent : empty event content") + Timber.e("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") + Timber.e("decryptEvent() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { try { return alg.decryptEvent(event, timeline) } catch (mxCryptoError: MXCryptoError) { - Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + Timber.d("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() - cryptoStore.getUserDevices(event.senderId ?: "") - ?.values - ?.firstOrNull { it.identityKey() == olmContent?.senderKey } - ?.let { - markOlmSessionForUnwedging(event.senderId ?: "", it) - } - ?: run { - Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging") - } + val olmContent = event.content.toModel() + if (event.senderId != null && olmContent?.senderKey != null) { + markOlmSessionForUnwedging(event.senderId, olmContent.senderKey) + } else { + Timber.tag(loggerTag.value).d("Can't mark as wedge malformed") } } } @@ -132,53 +135,87 @@ internal class EventDecryptor @Inject constructor( } } - // coroutineDispatchers.crypto scope - private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { - val deviceKey = deviceInfo.identityKey() + private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) { + val info = WedgedDeviceInfo(senderId, senderKey) + if (!wedgedDevices.contains(info)) { + Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged") + wedgedDevices.add(info) + } + } - val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 + // coroutineDispatchers.crypto scope + suspend fun unwedgeDevicesIfNeeded() { + // handle wedged devices + // Some olm decryption have failed and some device are wedged + // we should force start a new session for those + Timber.tag(loggerTag.value).d("Unwedging: ${wedgedDevices.size} are wedged") + // get the one that should be retried according to rate limit val now = System.currentTimeMillis() - if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + val toUnwedge = wedgedDevices.filter { + val lastForcedDate = lastNewSessionForcedDates[it] ?: 0 + if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { + Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate") + return@filter false + } + // let's already mark that we tried now + lastNewSessionForcedDates[it] = now + true + } + + if (toUnwedge.isEmpty()) { + Timber.tag(loggerTag.value).d("Nothing to unwedge") return } + Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices") - Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") - lastNewSessionForcedDates.setObject(senderId, deviceKey, now) + toUnwedge + .chunked(100) // safer to chunk if we ever have lots of wedged devices + .forEach { wedgedList -> + // lets download keys if needed + deviceListManager.downloadKeys(wedgedList.map { it.userId }, false) - // offload this from crypto thread (?) - cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold( - onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) }, - onFailure = { - Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}") - } - ) - } - } + // find the matching devices + wedgedList.groupBy { it.userId } + .map { groupedByUser -> + val userId = groupedByUser.key + val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey } + val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty() + userId to wedgeSenderKeysForUser.mapNotNull { senderKey -> + knownDevices.firstOrNull { it.identityKey() == senderKey } + } + } + .toMap() + .let { deviceList -> + try { + // force creating new outbound session and mark them as most recent to + // be used for next encryption (dummy) + val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true) + Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to") - private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap, deviceInfo: CryptoDeviceInfo, senderId: String) { - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") + // 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) + val payloadJson = mapOf( + "type" to EventType.DUMMY + ) + val sendToDeviceMap = MXUsersDevicesMap() + sessionToUse.map.values + .flatMap { it.values } + .map { it.deviceInfo } + .forEach { deviceInfo -> + Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}") + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload) + } - // 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("type" to EventType.DUMMY) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") - withContext(coroutineDispatchers.io) { - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) - } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") - } - } + // now let's send that + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) + } catch (failure: Throwable) { + deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let { + Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}") + } + } + } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index ab2ed04dfb..58765a7043 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXKey import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.model.toDebugString import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber import javax.inject.Inject @@ -31,89 +31,84 @@ private const val ONE_TIME_KEYS_RETRY_COUNT = 3 private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO) +@SessionScope internal class EnsureOlmSessionsForDevicesAction @Inject constructor( private val olmDevice: MXOlmDevice, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { + /** + * We want to synchronize a bit here, because we are iterating to check existing olm session and + * also adding some + */ + @Synchronized suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap { - val devicesWithoutSession = ArrayList() - + val deviceList = devicesByUser.flatMap { it.value } + Timber.tag(loggerTag.value) + .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}") val results = MXUsersDevicesMap() - - for ((userId, deviceList) in devicesByUser) { - for (deviceInfo in deviceList) { + val devicesToCreateSessionWith = mutableListOf() + if (force) { + // we take all devices and will query otk for them + devicesToCreateSessionWith.addAll(deviceList) + } else { + // only peek devices without active session + deviceList.forEach { deviceInfo -> val deviceId = deviceInfo.deviceId - val key = deviceInfo.identityKey() - if (key == null) { - Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key") - continue + val userId = deviceInfo.userId + val key = deviceInfo.identityKey() ?: return@forEach Unit.also { + Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key") } + // is there a session that as been already used? val sessionId = olmDevice.getSessionId(key) - - if (sessionId.isNullOrEmpty() || force) { - Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)") - devicesWithoutSession.add(deviceInfo) + if (sessionId.isNullOrEmpty()) { + Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list") + devicesToCreateSessionWith.add(deviceInfo) } else { Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") + val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) + results.setObject(userId, deviceId, olmSessionResult) } - - val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) - results.setObject(userId, deviceId, olmSessionResult) } } - Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" + - " ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}") - if (devicesWithoutSession.size == 0) { + if (devicesToCreateSessionWith.isEmpty()) { + // no session to create return results } - - // Prepare the request for claiming one-time keys - val usersDevicesToClaim = MXUsersDevicesMap() - - val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE - - for (device in devicesWithoutSession) { - usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm) + val usersDevicesToClaim = MXUsersDevicesMap().apply { + devicesToCreateSessionWith.forEach { + setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE) + } } - // TODO: this has a race condition - if we try to send another message - // while we are claiming a key, we will end up claiming two and setting up - // two sessions. - // - // That should eventually resolve itself, but it's poor form. - - Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}") - + // Let's now claim one time keys val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT) - Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") - for ((userId, deviceInfos) in devicesByUser) { - for (deviceInfo in deviceInfos) { - var oneTimeKey: MXKey? = null - val deviceIds = oneTimeKeys.getUserDeviceIds(userId) - if (null != deviceIds) { - for (deviceId in deviceIds) { - val olmSessionResult = results.getObject(userId, deviceId) - if (olmSessionResult?.sessionId != null && !force) { - // We already have a result for this device - continue - } - val key = oneTimeKeys.getObject(userId, deviceId) - if (key?.type == oneTimeKeyAlgorithm) { - oneTimeKey = key - } - if (oneTimeKey == null) { - Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId") - continue - } - // Update the result for this device in results - olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) - } + val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) + + // let now start olm session using the new otks + devicesToCreateSessionWith.forEach { deviceInfo -> + val userId = deviceInfo.userId + val deviceId = deviceInfo.deviceId + // Did we get an OTK + val oneTimeKey = oneTimeKeys.getObject(userId, deviceId) + if (oneTimeKey == null) { + Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}") + } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) { + Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}") + } else { + val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + if (olmSessionId != null) { + val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId) + results.setObject(userId, deviceId, olmSessionResult) + } else { + Timber + .tag(loggerTag.value) + .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}") } } } + return results } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt index 5e7744853a..b3638dc414 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt @@ -70,6 +70,8 @@ data class CryptoDeviceInfo( keys?.let { map["keys"] = it } return map } + + fun shortDebugString() = "$userId|$deviceId" } internal fun CryptoDeviceInfo.toRest(): DeviceKeys { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index 28cfbc7342..9ae7b82777 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -93,7 +93,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: ) return true } else { - // should not happen + // Could happen for to device events + // None of the known session could decrypt the message + // In this case unwedging process might have been started (rate limited) Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") } } From 078ed1b2d1c7572ad51a0ef1fa3b9101aea637ca Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 16:28:57 +0100 Subject: [PATCH 064/405] dispatch network calls to io --- .../matrix/android/sdk/internal/crypto/EventDecryptor.kt | 9 +++++++-- .../crypto/actions/EnsureOlmSessionsForDevicesAction.kt | 7 ++++++- .../crypto/algorithms/megolm/MXMegolmEncryption.kt | 9 +++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 9bf5cd594e..0a983167ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -18,6 +18,7 @@ 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.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag @@ -172,7 +173,9 @@ internal class EventDecryptor @Inject constructor( .chunked(100) // safer to chunk if we ever have lots of wedged devices .forEach { wedgedList -> // lets download keys if needed - deviceListManager.downloadKeys(wedgedList.map { it.userId }, false) + withContext(coroutineDispatchers.io) { + deviceListManager.downloadKeys(wedgedList.map { it.userId }, false) + } // find the matching devices wedgedList.groupBy { it.userId } @@ -209,7 +212,9 @@ internal class EventDecryptor @Inject constructor( // now let's send that val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) + } } catch (failure: Throwable) { deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let { Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 58765a7043..c7059ae4f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.crypto.actions +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo @@ -34,6 +36,7 @@ private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag @SessionScope internal class EnsureOlmSessionsForDevicesAction @Inject constructor( private val olmDevice: MXOlmDevice, + private val coroutineDispatchers: MatrixCoroutineDispatchers, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { /** @@ -84,7 +87,9 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( // Let's now claim one time keys val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) + val oneTimeKeys = withContext(coroutineDispatchers.io) { + oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) + } // let now start olm session using the new otks devicesToCreateSessionWith.forEach { deviceInfo -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 37e81df25d..85a6f5a7ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -305,7 +306,9 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { - sendToDeviceTask.execute(sendToDeviceParams) + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(sendToDeviceParams) + } Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... @@ -348,7 +351,9 @@ internal class MXMegolmEncryption( } ) try { - sendToDeviceTask.execute(params) + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(params) + } } catch (failure: Throwable) { Timber.tag(loggerTag.value) .e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}") From b7bf39b99abd2e909ae5ac40371952a9bd7d6223 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 17:13:52 +0100 Subject: [PATCH 065/405] resurrect unwedge test + cleaning --- .../crypto/ConcurrentDecryptionTest.kt | 148 ------------------ .../sdk/internal/crypto/UnwedgingTest.kt | 7 +- .../internal/crypto/DefaultCryptoService.kt | 3 + .../sdk/internal/crypto/EventDecryptor.kt | 3 +- .../sdk/internal/crypto/MXOlmDevice.kt | 6 + .../crypto/actions/MessageEncrypter.kt | 5 +- 6 files changed, 16 insertions(+), 156 deletions(-) delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt deleted file mode 100644 index 4ac6314955..0000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ConcurrentDecryptionTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.Log -import androidx.test.filters.LargeTest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestConstants -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.random.Random - -@RunWith(JUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -@LargeTest -class ConcurrentDecryptionTest : InstrumentedTest { - - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - - @Test - fun testConcurrentDecrypt() { -// val res = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - // ============================= - // ARRANGE - // ============================= - - val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - cryptoTestHelper.initializeCrossSigning(bobSession) - val bobSession2 = commonTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - - bobSession2.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId ?: "") - bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession2.sessionParams.deviceId ?: "") - - val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) - val roomAlicePOV = aliceSession.getRoom(roomId)!! - - // ============================= - // ACT - // ============================= - - val timelineEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() - val secondEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 2", 1).first() - val thirdEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 3", 1).first() - val forthEvent = commonTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob 4", 1).first() - - // await for bob unverified session to get the message - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId) != null - } - } - - val eventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! - val secondEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)!! - val thirdEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(thirdEvent.eventId)!! - val forthEventBobPOV = bobSession.getRoom(roomId)?.getTimeLineEvent(forthEvent.eventId)!! - - // let's try to decrypt concurrently and check that we are not getting exceptions - val dispatcher = Executors - .newFixedThreadPool(100) - .asCoroutineDispatcher() - val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - - val eventList = listOf(eventBobPOV, secondEventBobPOV, thirdEventBobPOV, forthEventBobPOV) - - val atomicAsError = AtomicBoolean() - val deff = mutableListOf>() - - val cryptoService = bobSession.cryptoService() - - coroutineScope.launch { - for (spawn in 1..100) { - delay((Random.nextFloat() * 1000).toLong()) - aliceSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) - } - } - - for (spawn in 1..8000) { - eventList.random().let { event -> - coroutineScope.async { - try { - cryptoService.decryptEvent(event.root, "") - Log.d("#TEST", "[$spawn] Decrypt Success ${event.eventId} :${Thread.currentThread().name}") - } catch (failure: Throwable) { - atomicAsError.set(true) - Log.e("#TEST", "Failed to decrypt $spawn/${event.eventId} :$failure") - } - }.let { - deff.add(it) - } - } - } - - coroutineScope.launch { - for (spawn in 1..100) { - delay((Random.nextFloat() * 1000).toLong()) - bobSession.cryptoService().requestRoomKeyForEvent(eventList.random().root) - } - } - - commonTestHelper.runBlockingTest(10 * 60_000) { - deff.awaitAll() - delay(10_000) - assert(!atomicAsError.get()) - // There should be no errors - } - - coroutineScope.cancel() - - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) - commonTestHelper.signOutAndClose(bobSession2) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 8a619043c4..fb5d58b127 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe import org.junit.Assert import org.junit.Before import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest { * -> This is automatically fixed after SDKs restarted the olm session */ @Test - @Ignore("This test will be ignored until it is fixed") fun testUnwedging() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest { val bobSession = cryptoTestData.secondSession!! val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting - - // bobSession.cryptoService().setWarnOnUnknownDevices(false) - // aliceSession.cryptoService().setWarnOnUnknownDevices(false) + val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! @@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest { Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!) + olmDevice.clearOlmSessionCache() Thread.sleep(6_000) // Force new session, and key share diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 2667c25ea4..db44abc36f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -1372,6 +1372,9 @@ internal class DefaultCryptoService @Inject constructor( @VisibleForTesting val cryptoStoreForTesting = cryptoStore + @VisibleForTesting + val olmDeviceForTest = olmDevice + companion object { const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 0a983167ae..808fd85747 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -195,8 +195,7 @@ internal class EventDecryptor @Inject constructor( val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true) Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to") - // 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) + // Now send a dummy message on that session so the other side knows about it. val payloadJson = mapOf( "type" to EventType.DUMMY ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 2be08915c6..8372698e7f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto +import androidx.annotation.VisibleForTesting import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.extensions.tryOrNull @@ -924,4 +925,9 @@ internal class MXOlmDevice @Inject constructor( fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess } + + @VisibleForTesting + fun clearOlmSessionCache() { + olmSessionStore.clear() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt index b37def5c1f..4e158602c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.actions +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.internal.crypto.MXOlmDevice @@ -28,6 +29,8 @@ import org.matrix.android.sdk.internal.util.convertToUTF8 import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO) + internal class MessageEncrypter @Inject constructor( @UserId private val userId: String, @@ -66,7 +69,7 @@ internal class MessageEncrypter @Inject constructor( val sessionId = olmDevice.getSessionId(deviceKey) if (!sessionId.isNullOrEmpty()) { - Timber.v("Using sessionid $sessionId for device $deviceKey") + Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey") payloadJson["recipient"] = deviceInfo.userId payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) From 87de51b1840445abafe7de6f9ce20982fcc0e8be Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 17:16:48 +0100 Subject: [PATCH 066/405] Use loggerTag --- .../matrix/android/sdk/internal/crypto/EventDecryptor.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 808fd85747..d10019e1d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -104,20 +104,20 @@ internal class EventDecryptor @Inject constructor( private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content if (eventContent == null) { - Timber.e("decryptEvent : empty event content") + Timber.tag(loggerTag.value).e("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("decryptEvent() : $reason") + Timber.tag(loggerTag.value).e("decryptEvent() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { try { return alg.decryptEvent(event, timeline) } catch (mxCryptoError: MXCryptoError) { - Timber.d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + Timber.tag(loggerTag.value).d("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) { From 49d33f3a4b955b685aacce8b6cbdeb14d25d7592 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 17:26:52 +0100 Subject: [PATCH 067/405] avoid duplicate userId on key download --- .../org/matrix/android/sdk/internal/crypto/EventDecryptor.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index d10019e1d3..f053168bd7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -172,13 +172,14 @@ internal class EventDecryptor @Inject constructor( toUnwedge .chunked(100) // safer to chunk if we ever have lots of wedged devices .forEach { wedgedList -> + val groupedByUserId = wedgedList.groupBy { it.userId } // lets download keys if needed withContext(coroutineDispatchers.io) { - deviceListManager.downloadKeys(wedgedList.map { it.userId }, false) + deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false) } // find the matching devices - wedgedList.groupBy { it.userId } + groupedByUserId .map { groupedByUser -> val userId = groupedByUser.key val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey } From 6546f9885805b4e3526386af0a8505b49d2520ed Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 19:16:25 +0100 Subject: [PATCH 068/405] use mutex on suspend and not synchronized --- .../EnsureOlmSessionsForDevicesAction.kt | 134 +++++++++--------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index c7059ae4f8..87c176612d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.crypto.actions +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag @@ -39,82 +41,84 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { + private val ensureMutex = Mutex() + /** * We want to synchronize a bit here, because we are iterating to check existing olm session and * also adding some */ - @Synchronized suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap { - val deviceList = devicesByUser.flatMap { it.value } - Timber.tag(loggerTag.value) - .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}") - val results = MXUsersDevicesMap() - val devicesToCreateSessionWith = mutableListOf() - if (force) { - // we take all devices and will query otk for them - devicesToCreateSessionWith.addAll(deviceList) - } else { - // only peek devices without active session - deviceList.forEach { deviceInfo -> - val deviceId = deviceInfo.deviceId - val userId = deviceInfo.userId - val key = deviceInfo.identityKey() ?: return@forEach Unit.also { - Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key") - } + ensureMutex.withLock { + val results = MXUsersDevicesMap() + val deviceList = devicesByUser.flatMap { it.value } + Timber.tag(loggerTag.value) + .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}") + val devicesToCreateSessionWith = mutableListOf() + if (force) { + // we take all devices and will query otk for them + devicesToCreateSessionWith.addAll(deviceList) + } else { + // only peek devices without active session + deviceList.forEach { deviceInfo -> + val deviceId = deviceInfo.deviceId + val userId = deviceInfo.userId + val key = deviceInfo.identityKey() ?: return@forEach Unit.also { + Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key") + } - // is there a session that as been already used? - val sessionId = olmDevice.getSessionId(key) - if (sessionId.isNullOrEmpty()) { - Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list") - devicesToCreateSessionWith.add(deviceInfo) - } else { - Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") - val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) - results.setObject(userId, deviceId, olmSessionResult) + // is there a session that as been already used? + val sessionId = olmDevice.getSessionId(key) + if (sessionId.isNullOrEmpty()) { + Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list") + devicesToCreateSessionWith.add(deviceInfo) + } else { + Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") + val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) + results.setObject(userId, deviceId, olmSessionResult) + } } } - } - if (devicesToCreateSessionWith.isEmpty()) { - // no session to create + if (devicesToCreateSessionWith.isEmpty()) { + // no session to create + return results + } + val usersDevicesToClaim = MXUsersDevicesMap().apply { + devicesToCreateSessionWith.forEach { + setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE) + } + } + + // Let's now claim one time keys + val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) + val oneTimeKeys = withContext(coroutineDispatchers.io) { + oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) + } + + // let now start olm session using the new otks + devicesToCreateSessionWith.forEach { deviceInfo -> + val userId = deviceInfo.userId + val deviceId = deviceInfo.deviceId + // Did we get an OTK + val oneTimeKey = oneTimeKeys.getObject(userId, deviceId) + if (oneTimeKey == null) { + Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}") + } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) { + Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}") + } else { + val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + if (olmSessionId != null) { + val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId) + results.setObject(userId, deviceId, olmSessionResult) + } else { + Timber + .tag(loggerTag.value) + .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}") + } + } + } return results } - val usersDevicesToClaim = MXUsersDevicesMap().apply { - devicesToCreateSessionWith.forEach { - setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE) - } - } - - // Let's now claim one time keys - val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - val oneTimeKeys = withContext(coroutineDispatchers.io) { - oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) - } - - // let now start olm session using the new otks - devicesToCreateSessionWith.forEach { deviceInfo -> - val userId = deviceInfo.userId - val deviceId = deviceInfo.deviceId - // Did we get an OTK - val oneTimeKey = oneTimeKeys.getObject(userId, deviceId) - if (oneTimeKey == null) { - Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}") - } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) { - Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}") - } else { - val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) - if (olmSessionId != null) { - val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId) - results.setObject(userId, deviceId, olmSessionResult) - } else { - Timber - .tag(loggerTag.value) - .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}") - } - } - } - - return results } private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { From 714e1d79b7ebd16255a019a1841f6f73e7addab6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 1 Mar 2022 19:16:37 +0100 Subject: [PATCH 069/405] clean log level --- .../org/matrix/android/sdk/internal/crypto/EventDecryptor.kt | 4 ++-- .../org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index f053168bd7..00efd3d6a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -149,7 +149,7 @@ internal class EventDecryptor @Inject constructor( // handle wedged devices // Some olm decryption have failed and some device are wedged // we should force start a new session for those - Timber.tag(loggerTag.value).d("Unwedging: ${wedgedDevices.size} are wedged") + Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged") // get the one that should be retried according to rate limit val now = System.currentTimeMillis() val toUnwedge = wedgedDevices.filter { @@ -164,7 +164,7 @@ internal class EventDecryptor @Inject constructor( } if (toUnwedge.isEmpty()) { - Timber.tag(loggerTag.value).d("Nothing to unwedge") + Timber.tag(loggerTag.value).v("Nothing to unwedge") return } Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 8372698e7f..0c9d92be81 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -909,7 +909,7 @@ internal class MXOlmDevice @Inject constructor( return holder } } else { - Timber.tag(loggerTag.value).w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } From ada83d0ba6f4b24bcac9eefda38779d489b7069e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 2 Mar 2022 09:11:26 +0100 Subject: [PATCH 070/405] fix test --- .../matrix/android/sdk/internal/crypto/PreShareKeysTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index a7a81bacf5..46c1dacf78 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest { private val cryptoTestHelper = CryptoTestHelper(testHelper) @Test - @Ignore("This test will be ignored until it is fixed") fun ensure_outbound_session_happy_path() { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val e2eRoomID = testData.roomId @@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest { // Just send a real message as test val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() - assertEquals(megolmSessionId, sentEvent.root.content.toModel()?.sessionId, "Unexpected megolm session") + assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId,) testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE From 5d952feef9c3bc93286a8f5254a45bc3369ceb1d Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 09:39:48 +0100 Subject: [PATCH 071/405] code review cleaning --- .../sdk/internal/crypto/E2eeSanityTests.kt | 2 +- .../sdk/internal/crypto/MXOlmDevice.kt | 37 +++++++------------ .../crypto/model/OlmSessionWrapper.kt | 3 +- .../internal/crypto/store/IMXCryptoStore.kt | 2 +- .../crypto/store/db/RealmCryptoStore.kt | 11 +++++- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index da14c9a2c5..be76961b4c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -637,7 +637,7 @@ class E2eeSanityTests : InstrumentedTest { } catch (error: MXCryptoError) { val errorType = (error as? MXCryptoError.Base)?.errorType if (expectedError == null) { - Assert.assertTrue(errorType != null) + Assert.assertNotNull(errorType) } else { assertEquals(expectedError, errorType, "Message expected to be UISI") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 0c9d92be81..79f2dc6ad3 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -114,13 +114,13 @@ internal class MXOlmDevice @Inject constructor( } try { - deviceCurve25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } + deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") } try { - deviceEd25519Key = doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } + deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") } @@ -131,7 +131,7 @@ internal class MXOlmDevice @Inject constructor( */ fun getOneTimeKeys(): Map>? { try { - return doWithOlmAccount { it.oneTimeKeys() } + return store.doWithOlmAccount { it.oneTimeKeys() } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") } @@ -143,18 +143,7 @@ internal class MXOlmDevice @Inject constructor( * @return The maximum number of one-time keys the olm account can store. */ fun getMaxNumberOfOneTimeKeys(): Long { - return doWithOlmAccount { it.maxOneTimeKeys() } - } - - /** - * Olm account access should be synchronized - */ - private fun doWithOlmAccount(block: (OlmAccount) -> T): T { - return store.getOlmAccount().let { olmAccount -> - synchronized(olmAccount) { - block.invoke(olmAccount) - } - } + return store.doWithOlmAccount { it.maxOneTimeKeys() } } /** @@ -164,7 +153,7 @@ internal class MXOlmDevice @Inject constructor( */ fun getFallbackKey(): MutableMap>? { try { - return doWithOlmAccount { it.fallbackKey() } + return store.doWithOlmAccount { it.fallbackKey() } } catch (e: Exception) { Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") } @@ -179,7 +168,7 @@ internal class MXOlmDevice @Inject constructor( fun generateFallbackKeyIfNeeded(): Boolean { try { if (!hasUnpublishedFallbackKey()) { - doWithOlmAccount { + store.doWithOlmAccount { it.generateFallbackKey() store.saveOlmAccount() } @@ -197,7 +186,7 @@ internal class MXOlmDevice @Inject constructor( fun forgetFallbackKey() { try { - doWithOlmAccount { + store.doWithOlmAccount { it.forgetFallbackKey() store.saveOlmAccount() } @@ -227,7 +216,7 @@ internal class MXOlmDevice @Inject constructor( */ fun signMessage(message: String): String? { try { - return doWithOlmAccount { it.signMessage(message) } + return store.doWithOlmAccount { it.signMessage(message) } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") } @@ -240,7 +229,7 @@ internal class MXOlmDevice @Inject constructor( */ fun markKeysAsPublished() { try { - doWithOlmAccount { + store.doWithOlmAccount { it.markOneTimeKeysAsPublished() store.saveOlmAccount() } @@ -256,7 +245,7 @@ internal class MXOlmDevice @Inject constructor( */ fun generateOneTimeKeys(numKeys: Int) { try { - doWithOlmAccount { + store.doWithOlmAccount { it.generateOneTimeKeys(numKeys) store.saveOlmAccount() } @@ -279,7 +268,7 @@ internal class MXOlmDevice @Inject constructor( try { olmSession = OlmSession() - doWithOlmAccount { olmAccount -> + store.doWithOlmAccount { olmAccount -> olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) } @@ -322,7 +311,7 @@ internal class MXOlmDevice @Inject constructor( try { try { olmSession = OlmSession() - doWithOlmAccount { olmAccount -> + store.doWithOlmAccount { olmAccount -> olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) } } catch (e: Exception) { @@ -333,7 +322,7 @@ internal class MXOlmDevice @Inject constructor( Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") try { - doWithOlmAccount { olmAccount -> + store.doWithOlmAccount { olmAccount -> olmAccount.removeOneTimeKeys(olmSession) store.saveOlmAccount() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt index b6be4d6864..263cb3b036 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -28,7 +28,8 @@ data class OlmSessionWrapper( // Timestamp at which the session last received a message. var lastReceivedMessageTs: Long = 0, - var mutex: Mutex = Mutex()) { + val mutex: Mutex = Mutex() +) { /** * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 96ea5c03fa..c28a1985b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -54,7 +54,7 @@ internal interface IMXCryptoStore { /** * @return the olm account */ - fun getOlmAccount(): OlmAccount + fun doWithOlmAccount(block: (OlmAccount) -> T): T fun getOrCreateOlmAccount(): OlmAccount diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index c2d2fbc681..585b3d2d25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -230,8 +230,15 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOlmAccount(): OlmAccount { - return olmAccount!! + /** + * Olm account access should be synchronized + */ + override fun doWithOlmAccount(block: (OlmAccount) -> T): T { + return olmAccount!!.let { olmAccount -> + synchronized(olmAccount) { + block.invoke(olmAccount) + } + } } @Synchronized From 7616e2d14c53e4ea9b6ee2ea7c93e6b3eddc3f74 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 19:07:36 +0100 Subject: [PATCH 072/405] better comment Co-authored-by: poljar --- .../org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index 989c9ab86e..f526245c6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -36,7 +36,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS private val olmSessions = HashMap>() /** - * Store a session between the logged-in user and another device. + * Store a session between our own device and another device. * This will be called after creation but also after any use of the ratchet * in order to persist the correct state for next run * @param olmSessionWrapper the end-to-end session. From 31d3fe38aab232ab39d241efa16712f40478a375 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 19:07:46 +0100 Subject: [PATCH 073/405] Better comment Co-authored-by: poljar --- .../org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index f526245c6a..4dba45046e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -37,7 +37,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS /** * Store a session between our own device and another device. - * This will be called after creation but also after any use of the ratchet + * This will be called after the session has been created but also every time it has been used * in order to persist the correct state for next run * @param olmSessionWrapper the end-to-end session. * @param deviceKey the public key of the other device. From 99a07af9de057b582725ec2bc268bac3b0476f72 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 19:07:58 +0100 Subject: [PATCH 074/405] Better comment Co-authored-by: poljar --- .../org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index 4dba45046e..8f8bb62c85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -51,7 +51,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS } /** - * Retrieve the end-to-end session ids between the logged-in user and another + * Get all the Olm Sessions we are sharing with the given user * device. * * @param deviceKey the public key of the other device. From db84c679b47e215831c79a145a26c9d62728af28 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 19:19:18 +0100 Subject: [PATCH 075/405] Code review cleaning --- .../matrix/android/sdk/internal/crypto/MXOlmDevice.kt | 2 -- .../android/sdk/internal/crypto/OlmSessionStore.kt | 11 +++-------- .../crypto/algorithms/megolm/MXMegolmEncryption.kt | 5 ++--- .../sdk/internal/crypto/store/IMXCryptoStore.kt | 4 ++-- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 79f2dc6ad3..501fb42db2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -280,7 +280,6 @@ internal class MXOlmDevice @Inject constructor( olmSessionWrapper.onMessageReceived() olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) -// store.storeSession(olmSessionWrapper, theirIdentityKey) val sessionIdentifier = olmSession.sessionIdentifier() @@ -866,7 +865,6 @@ internal class MXOlmDevice @Inject constructor( // sanity check return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) -// store.getDeviceSession(sessionId, theirDeviceIdentityKey) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index 8f8bb62c85..f4fbca6a0f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -51,18 +51,13 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS } /** - * Get all the Olm Sessions we are sharing with the given user - * device. + * Get all the Olm Sessions we are sharing with the given device. * * @param deviceKey the public key of the other device. * @return A set of sessionId, or empty if device is not known */ @Synchronized fun getDeviceSessionIds(deviceKey: String): List { - return internalGetAllSessions(deviceKey) - } - - private fun internalGetAllSessions(deviceKey: String): MutableList { // we need to get the persisted ids first val persistedKnownSessions = store.getDeviceSessionIds(deviceKey) .orEmpty() @@ -79,12 +74,12 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS } /** - * Retrieve an end-to-end session between the logged-in user and another + * Retrieve an end-to-end session between our own device and another * device. * * @param sessionId the session Id. * @param deviceKey the public key of the other device. - * @return The Base64 end-to-end session, or null if not found + * @return the session wrapper if found */ @Synchronized fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 85a6f5a7ef..cf9733dc2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -471,9 +471,8 @@ internal class MXMegolmEncryption( } val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) if (olmSessionResult?.sessionId == null) { - return false.also { - Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") - } + Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") + return false } Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index c28a1985b4..e662ff74e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -261,7 +261,7 @@ internal interface IMXCryptoStore { fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) /** - * Retrieve the end-to-end session ids between the logged-in user and another + * Retrieve all end-to-end session ids between our own device and another * device. * * @param deviceKey the public key of the other device. @@ -270,7 +270,7 @@ internal interface IMXCryptoStore { fun getDeviceSessionIds(deviceKey: String): List? /** - * Retrieve an end-to-end session between the logged-in user and another + * Retrieve an end-to-end session between our own device and another * device. * * @param sessionId the session Id. From 3c931d6f6dc809ed6a812e8cf7d8e14c671b85c4 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 4 Mar 2022 19:19:43 +0100 Subject: [PATCH 076/405] Save valid backup key before downloading keys --- .../internal/crypto/keysbackup/DefaultKeysBackupService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index b20168eaa3..3e0e8eb00a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -671,7 +671,6 @@ internal class DefaultKeysBackupService @Inject constructor( Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") throw InvalidParameterException("Invalid recovery key") } - // Get a PK decryption instance pkDecryptionFromRecoveryKey(recoveryKey) } @@ -680,6 +679,10 @@ internal class DefaultKeysBackupService @Inject constructor( Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") throw InvalidParameterException("Invalid recovery key") } + + // Save for next time and for gossiping + // Save now as it's valid, don't wait for the import as it could take long. + saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) @@ -729,8 +732,6 @@ internal class DefaultKeysBackupService @Inject constructor( if (backUp) { maybeBackupKeys() } - // Save for next time and for gossiping - saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) result } }.foldToCallback(callback) From bce5bc8389dd4a6a09b8b7a50750fc136dcc7e15 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Sat, 5 Mar 2022 17:13:02 +0200 Subject: [PATCH 077/405] Fix wrong versioning regex pattern Add MSC3440 support using /version/ and /capabilities --- .../api/session/room/threads/ThreadsService.kt | 2 +- .../internal/auth/version/HomeServerVersion.kt | 3 ++- .../sdk/internal/auth/version/Versions.kt | 9 +++++++++ .../homeserver/GetHomeServerCapabilitiesTask.kt | 3 ++- .../room/threads/DefaultThreadsService.kt | 2 +- .../android/sdk/api/auth/data/VersionsKtTest.kt | 16 ++++++++++++++++ .../list/viewmodel/ThreadListViewModel.kt | 2 +- 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index 99c0dc7d0f..839cdff63b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -42,7 +42,7 @@ interface ThreadsService { * message edition for that thread * @return the enhanced [List] with edited updates */ - fun enhanceWithEditions(threads: List): List + fun enhanceThreadWithEditions(threads: List): List /** * Fetch all thread replies for the specified thread using the /relations api diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt index 0a9b8b73cc..815f8de2de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt @@ -38,7 +38,7 @@ internal data class HomeServerVersion( } companion object { - internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""") + internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""") internal fun parse(value: String): HomeServerVersion? { val result = pattern.matchEntire(value) ?: return null @@ -56,5 +56,6 @@ internal data class HomeServerVersion( val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0) val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0) val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0) + val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 74cb3de2ac..662348e505 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -51,6 +51,7 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members" private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server" private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" +private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" /** * Return true if the SDK supports this homeserver version @@ -68,6 +69,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { doesServerSeparatesAddAndBind() } +/** + * Indicate if the homeserver support MSC3440 for threads + */ +internal fun Versions.doesServerSupportThreads(): Boolean { + return getMaxVersion() >= HomeServerVersion.v1_3_0 || + unstableFeatures?.get(FEATURE_THREADS_MSC3440) ?: false +} + /** * Return true if the server support the lazy loading of room members * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index ae8e31c8a1..8be689f49d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -122,7 +123,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) } - homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() + homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse() } if (getMediaConfigResult != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 033c1c0ff9..b65991347d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -65,7 +65,7 @@ internal class DefaultThreadsService @AssistedInject constructor( ) } - override fun enhanceWithEditions(threads: List): List { + override fun enhanceThreadWithEditions(threads: List): List { return Realm.getInstance(monarchy.realmConfiguration).use { threads.enhanceWithEditions(it, roomId) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt index 96655b849d..7a38c8a0aa 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data import org.amshove.kluent.shouldBe import org.junit.Test import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk class VersionsKtTest { @@ -53,5 +54,20 @@ class VersionsKtTest { Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true + } + + @Test + fun doesServerSupportThreads() { + Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe true } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index d68e0a3248..7a22f75bce 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -80,7 +80,7 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState private fun observeThreadSummaries() { room?.flow() ?.liveThreadSummaries() - ?.map { room.enhanceWithEditions(it) } + ?.map { room.enhanceThreadWithEditions(it) } ?.flowOn(room.coroutineDispatchers.io) ?.execute { asyncThreads -> copy(threadSummaryList = asyncThreads) From d19dd91d671f2282b145c7e01152c41cde988387 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Sat, 5 Mar 2022 20:49:11 +0200 Subject: [PATCH 078/405] Format code --- .../session/homeserver/GetHomeServerCapabilitiesTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 8be689f49d..1f48082b6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -123,7 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) } - homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse() + homeServerCapabilitiesEntity.canUseThreading = + capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse() } if (getMediaConfigResult != null) { From 8732c6fe42fdb55922d5a57f25e58202465f6018 Mon Sep 17 00:00:00 2001 From: NIkita Fedrunov Date: Mon, 7 Mar 2022 08:32:31 +0100 Subject: [PATCH 079/405] Amend spaces menu to have consistent copy --- changelog.d/5270.misc | 1 + vector/src/main/res/drawable/ic_explore.xml | 9 +++++---- .../main/res/layout/bottom_sheet_space_settings.xml | 12 ++++++------ vector/src/main/res/values/strings.xml | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 changelog.d/5270.misc diff --git a/changelog.d/5270.misc b/changelog.d/5270.misc new file mode 100644 index 0000000000..9bbe41af59 --- /dev/null +++ b/changelog.d/5270.misc @@ -0,0 +1 @@ +Amend spaces menu to be consistent with iOS version \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_explore.xml b/vector/src/main/res/drawable/ic_explore.xml index 09a496170f..30a330a5d2 100644 --- a/vector/src/main/res/drawable/ic_explore.xml +++ b/vector/src/main/res/drawable/ic_explore.xml @@ -4,10 +4,11 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M18.4224,7.3784C18.4224,8.5075 17.5071,9.4229 16.3779,9.4229C15.2488,9.4229 14.3335,8.5075 14.3335,7.3784C14.3335,6.2493 15.2488,5.334 16.3779,5.334C17.5071,5.334 18.4224,6.2493 18.4224,7.3784ZM19.8496,9.4545C20.2133,8.8476 20.4224,8.1375 20.4224,7.3784C20.4224,5.1447 18.6116,3.334 16.3779,3.334C14.1443,3.334 12.3335,5.1447 12.3335,7.3784C12.3335,9.6121 14.1443,11.4229 16.3779,11.4229C17.1369,11.4229 17.8471,11.2138 18.454,10.8501C18.4853,10.8937 18.5205,10.9353 18.5597,10.9744L20.293,12.7078C20.6835,13.0983 21.3167,13.0983 21.7072,12.7078C22.0977,12.3172 22.0977,11.6841 21.7072,11.2936L19.9739,9.5602C19.9347,9.5211 19.8931,9.4858 19.8496,9.4545Z" + android:fillColor="#000000" + android:fillType="evenOdd"/> diff --git a/vector/src/main/res/layout/bottom_sheet_space_settings.xml b/vector/src/main/res/layout/bottom_sheet_space_settings.xml index 4ea71ca5e7..e764cdedc4 100644 --- a/vector/src/main/res/layout/bottom_sheet_space_settings.xml +++ b/vector/src/main/res/layout/bottom_sheet_space_settings.xml @@ -76,7 +76,7 @@ android:layout_height="wrap_content" app:actionTitle="@string/invite_people_menu" app:leftIcon="@drawable/ic_invite_people" - app:tint="?vctr_content_primary" + app:tint="?vctr_content_secondary" app:titleTextColor="?vctr_content_primary" /> @@ -95,7 +95,7 @@ android:layout_height="wrap_content" app:actionTitle="@string/settings" app:leftIcon="@drawable/ic_settings_root_general" - app:tint="?vctr_content_primary" + app:tint="?vctr_content_secondary" app:titleTextColor="?vctr_content_primary" /> @@ -114,7 +114,7 @@ android:layout_height="wrap_content" app:actionTitle="@string/space_add_child_title" app:leftIcon="@drawable/ic_fab_add" - app:tint="?vctr_content_primary" + app:tint="?vctr_content_secondary" app:titleTextColor="?vctr_content_primary" tools:actionDescription="" /> @@ -124,7 +124,7 @@ android:layout_height="wrap_content" app:actionTitle="@string/add_space" app:leftIcon="@drawable/ic_fab_add" - app:tint="?vctr_content_primary" + app:tint="?vctr_content_secondary" app:titleTextColor="?vctr_content_primary" app:betaAction="true" tools:actionDescription="" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index dc52d2cf5d..4ca1d1104f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -597,7 +597,7 @@ Continue - List members + Members Jump to unread @@ -2781,7 +2781,7 @@ Explore rooms Add rooms - Leave Space + Leave Are you sure you want to leave %s? You are the only person here. If you leave, no one will be able to join in the future, including you. You won\'t be able to rejoin unless you are re-invited. From a3dcee55e401bbaa98770fc5447de8b88bee3e9a Mon Sep 17 00:00:00 2001 From: NIkita Fedrunov Date: Mon, 7 Mar 2022 08:35:53 +0100 Subject: [PATCH 080/405] include dms number in space unread number badge --- changelog.d/5260.misc | 1 + .../sdk/internal/session/room/summary/RoomSummaryUpdater.kt | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/5260.misc diff --git a/changelog.d/5260.misc b/changelog.d/5260.misc new file mode 100644 index 0000000000..36812e2c83 --- /dev/null +++ b/changelog.d/5260.misc @@ -0,0 +1 @@ +Number of unread messages on space badge now include number of unread DMs \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 1c1d59fb3d..449fe60dca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -411,8 +411,6 @@ internal class RoomSummaryUpdater @Inject constructor( realm.where(RoomSummaryEntity::class.java) .process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN)) .notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) - // also we do not count DM in here, because home space will already show them - .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) .contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId) .findAll().forEach { highlightCount += it.highlightCount From 33246be2a53f482f731df1a7ca038a1e3406c542 Mon Sep 17 00:00:00 2001 From: NIkita Fedrunov Date: Mon, 7 Mar 2022 09:58:40 +0100 Subject: [PATCH 081/405] change selected space highlight --- changelog.d/5346.misc | 1 + vector/src/main/res/drawable/bg_space_item.xml | 9 +++++---- vector/src/main/res/layout/item_space.xml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 changelog.d/5346.misc diff --git a/changelog.d/5346.misc b/changelog.d/5346.misc new file mode 100644 index 0000000000..f979c180ef --- /dev/null +++ b/changelog.d/5346.misc @@ -0,0 +1 @@ +Selected space highlight changed in left panel \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_space_item.xml b/vector/src/main/res/drawable/bg_space_item.xml index 158a6769ba..f9608b3d35 100644 --- a/vector/src/main/res/drawable/bg_space_item.xml +++ b/vector/src/main/res/drawable/bg_space_item.xml @@ -8,11 +8,12 @@ - + - - - + + diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index a0fe050fdd..1eb5e6d74a 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -113,7 +113,7 @@ android:id="@+id/groupTmpLeave" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginEnd="4dp" + android:layout_marginEnd="10dp" android:background="?selectableItemBackground" android:clickable="true" android:importantForAccessibility="no" From 3b0a5658224c195e82028b8c223510bcf3c7e9c1 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 7 Mar 2022 16:51:40 +0100 Subject: [PATCH 082/405] Changes room hierarchy endpoint --- .../org/matrix/android/sdk/internal/session/space/SpaceApi.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt index edd10bc2ef..3d5fc153bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -31,7 +31,7 @@ internal interface SpaceApi { * @param from: Optional. Pagination token given to retrieve the next set of rooms. * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same. */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy") + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "rooms/{roomID}/hierarchy") suspend fun getSpaceHierarchy( @Path("roomId") spaceId: String, @Query("suggested_only") suggestedOnly: Boolean?, From e758f440c7c8a8887ee8e2ec437337c3c16abb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Mon, 7 Mar 2022 22:18:49 +0000 Subject: [PATCH 083/405] Translated using Weblate (Slovak) Currently translated at 99.2% (2140 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- vector/src/main/res/values-sk/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 6ec0827757..3ea0dc14a8 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -2439,4 +2439,10 @@ %1$s a %2$s Pokračovať pomocou jednotného prihlásenia SSO jednotné prihlásenie SSO + Zatvoriť výzvu zálohovanie kľúčov + Výťazná odpoveď + Nezaškrtnuté + Zaškrtnuté + Rozpísaná správa + Otvoriť navigačnú ponuku \ No newline at end of file From 9c18088128bb8b40bb91136474d89e8180c44117 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 23:05:24 +0000 Subject: [PATCH 084/405] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 4 ++-- .github/workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/nightly.yml | 12 ++++++------ .github/workflows/quality.yml | 8 ++++---- .github/workflows/sync-from-external-sources.yml | 6 +++--- .github/workflows/tests.yml | 2 +- .github/workflows/update-gradle-wrapper.yml | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ba71c1f61..4ff935fad1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | @@ -49,7 +49,7 @@ jobs: if: github.ref == 'refs/heads/main' # Only runs on main, no concurrency. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index ee4a87293f..4e701faa44 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -7,5 +7,5 @@ jobs: runs-on: ubuntu-latest # No concurrency required, this is a prerequisite to other actions and should run every time. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 52c63d595a..08e382c817 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,7 +21,7 @@ jobs: runs-on: macos-latest # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: distribution: 'adopt' @@ -43,7 +43,7 @@ jobs: runs-on: macos-latest # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: distribution: 'adopt' @@ -69,7 +69,7 @@ jobs: api-level: [ 28 ] # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - uses: actions/setup-java@v2 with: @@ -261,7 +261,7 @@ jobs: api-level: [ 28 ] # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.8 uses: actions/setup-python@v3 with: @@ -311,7 +311,7 @@ jobs: codecov-units: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: distribution: 'adopt' @@ -339,7 +339,7 @@ jobs: needs: - codecov-units steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: distribution: 'adopt' diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 02827e7f17..a588b91449 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -10,7 +10,7 @@ jobs: name: Project Check Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Run code quality check suite run: ./tools/check/check_code_quality.sh @@ -23,7 +23,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Run ktlint run: | ./gradlew ktlintCheck --continue @@ -96,7 +96,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | @@ -129,7 +129,7 @@ jobs: group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml index 55873c9112..d390c47696 100644 --- a/.github/workflows/sync-from-external-sources.yml +++ b/.github/workflows/sync-from-external-sources.yml @@ -11,7 +11,7 @@ jobs: if: github.repository == 'vector-im/element-android' # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.8 uses: actions/setup-python@v3 with: @@ -38,7 +38,7 @@ jobs: if: github.repository == 'vector-im/element-android' # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.8 uses: actions/setup-python@v3 with: @@ -64,7 +64,7 @@ jobs: if: github.repository == 'vector-im/element-android' # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Run analytics import script run: ./tools/import_analytic_plan.sh - name: Create Pull Request for analytics plan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d24108084..fcca6364b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/cache@v2 with: path: | diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml index 4a786a9339..1cbf29cc8d 100644 --- a/.github/workflows/update-gradle-wrapper.yml +++ b/.github/workflows/update-gradle-wrapper.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Update Gradle Wrapper uses: gradle-update/update-gradle-wrapper-action@v1 From 557fd7eacf87438a174dae60e6684fd8d2a365f7 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 8 Mar 2022 10:13:56 +0200 Subject: [PATCH 085/405] Replace thread timeline and thread summaries EventInsertType from INCREMENTAL_SYNC to PAGINATION --- .../android/sdk/internal/database/helper/ThreadSummaryHelper.kt | 2 +- .../session/room/relation/threads/FetchThreadTimelineTask.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 229e433a89..9ebe1203ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -260,7 +260,7 @@ private fun HashMap.addSenderState(realm: Realm, roo */ private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } - return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) + return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 54ebc620c9..a8f712c4fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -212,7 +212,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( */ private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } - return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) + return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } /** From 768262094c52c5078fd4f23cf5da8c2a02487553 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 8 Mar 2022 10:54:12 +0100 Subject: [PATCH 086/405] Fix missing messages when forward paging with chunks > 50 messages - offsets() was not limiting in the right direction when loading messages forwards - after fixing offsets(), more recent messages would not be loaded due to the isLastForward() check, so better prioritize the SUCCESS LoadMoreResult over the REACHED_END here --- changelog.d/5448.bugfix | 1 + .../session/room/timeline/TimelineChunk.kt | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 changelog.d/5448.bugfix diff --git a/changelog.d/5448.bugfix b/changelog.d/5448.bugfix new file mode 100644 index 0000000000..c4e8fb4a49 --- /dev/null +++ b/changelog.d/5448.bugfix @@ -0,0 +1 @@ +Fix missing messages when loading messages forwards diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index c0dc31fcf8..77f210aa9a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -144,14 +144,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, val offsetCount = count - loadFromStorage.numberOfEvents - return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) { + return if (offsetCount == 0) { + LoadMoreResult.SUCCESS + } else if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) { LoadMoreResult.REACHED_END } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) { LoadMoreResult.REACHED_END } else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) { LoadMoreResult.REACHED_END - } else if (offsetCount == 0) { - LoadMoreResult.SUCCESS } else { delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction) } @@ -508,13 +508,18 @@ private fun RealmQuery.offsets( count: Int, startDisplayIndex: Int ): RealmQuery { - sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - if (direction == Timeline.Direction.BACKWARDS) { + return if (direction == Timeline.Direction.BACKWARDS) { lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + limit(count.toLong()) } else { greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + // We need to sort ascending first so limit works in the right direction + sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + limit(count.toLong()) + // Result is expected to be sorted descending + sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } - return limit(count.toLong()) } private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { From bc3b8d0a16fa1e77d41716cbe1348df13da0aa9b Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 11:51:17 +0100 Subject: [PATCH 087/405] Adds testing for fallback api --- .../sdk/internal/network/NetworkConstants.kt | 1 + .../session/space/ResolveSpaceInfoTask.kt | 18 ++++++++++++------ .../sdk/internal/session/space/SpaceApi.kt | 13 ++++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 1ab1042129..2d567f273d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -21,6 +21,7 @@ internal object NetworkConstants { private const val URI_API_PREFIX_PATH = "_matrix/client" const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" + const val URI_API_PREFIX_PATH_V1 = "${URI_API_PREFIX_PATH}v1/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 2a396d6ee7..7c127a4ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { @@ -38,12 +39,17 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( ) : ResolveSpaceInfoTask { override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { return executeRequest(globalErrorReceiver) { - spaceApi.getSpaceHierarchy( - spaceId = params.spaceId, - suggestedOnly = params.suggestedOnly, - limit = params.limit, - maxDepth = params.maxDepth, - from = params.from) + try { + throw RuntimeException("Test space task exception") + } catch (e: Throwable) { + Timber.i("Test fall back api") + spaceApi.getSpaceHierarchy( + spaceId = params.spaceId, + suggestedOnly = params.suggestedOnly, + limit = params.limit, + maxDepth = params.maxDepth, + from = params.from) + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt index 3d5fc153bf..164df6f8b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -31,11 +31,22 @@ internal interface SpaceApi { * @param from: Optional. Pagination token given to retrieve the next set of rooms. * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same. */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "rooms/{roomID}/hierarchy") + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomID}/hierarchy") suspend fun getSpaceHierarchy( @Path("roomId") spaceId: String, @Query("suggested_only") suggestedOnly: Boolean?, @Query("limit") limit: Int?, @Query("max_depth") maxDepth: Int?, @Query("from") from: String?): SpacesResponse + + /** + * Unstable version of [getSpaceHierarchy] + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy") + suspend fun getSpaceHierarchyUnstable( + @Path("roomId") spaceId: String, + @Query("suggested_only") suggestedOnly: Boolean?, + @Query("limit") limit: Int?, + @Query("max_depth") maxDepth: Int?, + @Query("from") from: String?): SpacesResponse } From 9fa285e6ca8e0ed276a2b287b5160a418938019b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Mar 2022 14:06:28 +0300 Subject: [PATCH 088/405] Support showing push notifications for poll start events. --- .../app/features/notifications/NotifiableEventResolver.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index ec034173fc..0cf586afea 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -94,7 +94,7 @@ class NotifiableEventResolver @Inject constructor( } suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { - if (event.getClearType() != EventType.MESSAGE) return null + if (event.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return null // Ignore message edition if (event.isEdition()) return null @@ -153,7 +153,8 @@ class NotifiableEventResolver @Inject constructor( event.attemptToDecryptIfNeeded(session) // only convert encrypted messages to NotifiableMessageEvents when (event.root.getClearType()) { - EventType.MESSAGE -> { + EventType.MESSAGE, + EventType.POLL_START -> { val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName @@ -185,7 +186,7 @@ class NotifiableEventResolver @Inject constructor( soundName = null ) } - else -> null + else -> null } } } From 7a1d3aa3f27fb184bea9e082321e69cec13d1ca2 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Mar 2022 14:07:14 +0300 Subject: [PATCH 089/405] Filter poll response events in latest event query. --- .../sdk/internal/database/query/TimelineEventEntityQueries.kt | 1 + .../android/sdk/internal/database/query/TimelineEventFilter.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 63f41ebf2c..81d5ac835f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -97,6 +97,7 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent if (filters.filterEdits) { not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE) } if (filters.filterRedacted) { not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt index 10a0d1dcec..a7317506a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt @@ -26,6 +26,7 @@ internal object TimelineEventFilter { internal object Content { internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}""" + internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}""" } /** From 83ff898ce5399d5bbe301143068d4f77ef1d7be8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Mar 2022 14:14:45 +0300 Subject: [PATCH 090/405] Changelog added. --- changelog.d/4780.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4780.bugfix diff --git a/changelog.d/4780.bugfix b/changelog.d/4780.bugfix new file mode 100644 index 0000000000..51eb1e4ad7 --- /dev/null +++ b/changelog.d/4780.bugfix @@ -0,0 +1 @@ +Poll system notifications on Android are not user friendly \ No newline at end of file From 0af6ae6075d73d595db470392a09526d8f4b8b0a Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 13:10:18 +0100 Subject: [PATCH 091/405] Adds logic for using stable and unstable hierarchy endpoints --- .../session/space/ResolveSpaceInfoTask.kt | 41 ++++++++++----- .../space/DefaultResolveSpaceInfoTaskTest.kt | 51 +++++++++++++++++++ .../android/sdk/test/fakes/FakeSpaceApi.kt | 47 +++++++++++++++++ .../ResolveSpaceInfoTaskParamsFixture.kt | 35 +++++++++++++ .../test/fixtures/SpacesResponseFixture.kt | 30 +++++++++++ 5 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 7c127a4ac3..6c9a5c7101 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.space import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task -import timber.log.Timber +import retrofit2.HttpException import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { @@ -29,7 +29,6 @@ internal interface ResolveSpaceInfoTask : Task(500, "".toResponseBody()) + coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws HttpException(errorResponse) + } + + fun givenUnstableEndpointWorks() { + coEvery { instance.getSpaceHierarchyUnstable(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt new file mode 100644 index 0000000000..183d26caef --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fixtures + +import org.matrix.android.sdk.internal.session.space.ResolveSpaceInfoTask + +internal object ResolveSpaceInfoTaskParamsFixture { + fun aResolveSpaceInfoTaskParams( + spaceId: String = "", + limit: Int? = null, + maxDepth: Int? = null, + from: String? = null, + suggestedOnly: Boolean? = null, + ) = ResolveSpaceInfoTask.Params( + spaceId, + limit, + maxDepth, + from, + suggestedOnly, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt new file mode 100644 index 0000000000..3149d13e7d --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fixtures + +import org.matrix.android.sdk.internal.session.space.SpaceChildSummaryResponse +import org.matrix.android.sdk.internal.session.space.SpacesResponse + +internal object SpacesResponseFixture { + fun aSpacesResponse( + nextBatch: String? = null, + rooms: List? = null, + ) = SpacesResponse( + nextBatch, + rooms, + ) +} From e5299d716c8d78231bbfce4bb02904f5f58a3c4d Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 13:15:26 +0100 Subject: [PATCH 092/405] Fixes legal comments --- .../internal/session/space/DefaultResolveSpaceInfoTaskTest.kt | 2 +- .../test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt | 2 +- .../sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt | 2 +- .../matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt index c92b9a5ed2..d23280121c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2021 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. diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt index 81f486dbb3..26ee295236 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2021 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. diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt index 183d26caef..28f8c3637d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2021 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. diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt index 3149d13e7d..0a08331114 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2021 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. From 8c6902aa23d6532cbd7485e6dbd604097bfce313 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 8 Mar 2022 14:50:27 +0200 Subject: [PATCH 093/405] Fix reply within thread edition --- .../android/sdk/internal/session/filter/RoomEventFilter.kt | 4 ++-- .../internal/session/room/relation/DefaultRelationService.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt index c93f6a10db..aac987f3f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt @@ -52,13 +52,13 @@ data class RoomEventFilter( * A list of relation types which must be exist pointing to the event being filtered. * If this list is absent then no filtering is done on relation types. */ -// @Json(name = "relation_types") val relationTypes: List? = null, +// @Json(name = "related_by_rel_types") val relationTypes: List? = null, @Json(name = "io.element.relation_types") val relationTypes: List? = null, // To be replaced with the above line after the release /** * A list of senders of relations which must exist pointing to the event being filtered. * If this list is absent then no filtering is done on relation types. */ -// @Json(name = "relation_senders") val relationSenders: List? = null, +// @Json(name = "related_by_senders") val relationSenders: List? = null, @Json(name = "io.element.relation_senders") val relationSenders: List? = null, // To be replaced with the above line after the release /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index ab514d31c8..bb2acd8438 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -172,7 +172,7 @@ internal class DefaultRelationService @AssistedInject constructor( replyText = replyInThreadText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId, - showInThread = false + showInThread = true ) ?.also { saveLocalEcho(it) From a657bc6976e6da2f74de8437b0856d8f904e5784 Mon Sep 17 00:00:00 2001 From: Zet Date: Sat, 5 Mar 2022 14:53:44 +0000 Subject: [PATCH 094/405] Translated using Weblate (Arabic) Currently translated at 30.5% (659 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ar/ --- vector/src/main/res/values-ar/strings.xml | 44 +++++++---------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml index 0f1004ad7b..f0cc1fbceb 100644 --- a/vector/src/main/res/values-ar/strings.xml +++ b/vector/src/main/res/values-ar/strings.xml @@ -38,7 +38,6 @@ غُرفة فارِغة %1$s وَ %2$s دَعوة لغُرفة - دعوتك أنشأ ⁨%1$s⁩ الغرفة أنشأتَ الغرفة @@ -156,7 +155,7 @@ المُزامنة الأولية: \nيستورد الغُرف المُزامنة الأولية: -\nيجري إستيرد التَعمية +\nيستورد التَعمية غُرفة فارِغة (كانت %s) %1$s، %2$s و%3$s @@ -192,7 +191,6 @@ احذف غيّر الاسم أبلِغ عن المحتوى - اخرج مكالمة صوتية مكالمة صورية @@ -209,7 +207,6 @@ متراسلو «ماترِكس» فقط لا نتائج الغرف - أرسِل السجلات أرسِل سجلات الانهيار أرسِل لقطة شاشة @@ -234,7 +231,6 @@ أنسيت كلمة السر؟ يجب إدخال عنوان البريد الإلكتروني المرتبط بحسابك. فشل تأكيد عنوان البريد: تحقق من نقر الرابط في البريد - أدخِل مسارا صالحا لم يحتوي JSON صالح أُرسلت الكثير من الطلبات @@ -261,7 +257,6 @@ اخرج البصمة (⁨%s⁩): تعذّر التحقق من معرّف الخادوم البعيد. - لا نتائج صورة اللاحة اسم العرض @@ -325,7 +320,6 @@ استخدم الكمرة الأصيلة اضبطه كعنوان رئيسي ألغِ ضبطه كعنوان رئيسي - السمة خطأ في فكّ التعمية اسم الجهاز @@ -335,7 +329,6 @@ صدّر المفاتيح إلى ملف محلي أدخِل عبارة المرور أكّد عبارة المرور - استورد مفاتيح الطرفين لغرفة استورد مفاتيح الغرفة استورد المفاتيح من ملف محلي @@ -363,9 +356,7 @@ بلاغ علة يبدو أنك تهزّ الهاتف وأنت مُحبط. أتريد إرسال بلاغ علة؟ التقدم (%s٪) - يحمّل… - لا أعضاء عضو واحد @@ -384,7 +375,6 @@ %d رسالة جديدة %d رسالة جديدة - أولوية منخفضة المجتمعات هزّ الجهاز بجنون يُرسل بلاغًا بعلة @@ -396,7 +386,6 @@ أمتأكد من بدء محادثة صوتية؟ أمتأكد من بدء محادثة صورية؟ عنوان البريد مُعرّف بالفعل. - يريد خادوم المنزل هذا التأكد من أنك لست أحد الآليين JSON‏ معطوب @@ -407,19 +396,14 @@ %d تغييرا على العضوية %d تغيير على العضوية - يحتاج Element تصريحا منك للوصول إلى المِكرفون لإجراء المكالمات الصوتية. - يحتاج Element تصريحا منك للوصول إلى الكمرة والمِكرفون لإجراء المكالمات الصورية. رجاءً اسمح بالوصول في المنبثقة التالية لتقدر على إرسال إجراء المكالمات الصورية. - - ادعُ انضم إلى الغرفة الأصلي لم يُجب الطرف البعيد. - الدردشات المباشرة أشِر إليه لن تستطيع العودة عن هذا التغيير إذ أنك تمنح المستخدم نفس مستوى السلطة الذي لك. @@ -482,7 +466,6 @@ هُزّ الجهاز عند الإشارة إليّ التحاليل ‏⁨%1$s⁩ في ⁨%2$s⁩ - تخويل والج كَ‍ خادوم المنزل @@ -493,7 +476,6 @@ الميل معطّل مزعج - لا ترسل من هذا الجهاز الرسائل المعمّاة إلى الأجهزة غير المؤكّدة عمِّ إلى الأجهزة المؤكّدة فقط <b>غير<b/> مؤكّدة @@ -504,7 +486,6 @@ قد يعني هذا بأن أحدهم يعترض الاتصال بعدوانية، أو أن هاتفك لا يثق بالشهادة التي قدّمها الخادوم البعيد. إن قال مدير الخادوم بأن هذا متوقع، فتأكد من أن البصمة أدناه تطابق البصمة التي وفّرها. تغيّرت الشهادة من شهادة كنت تثق بها إلى شهادة لا تثق بها. لربما جدّد الخادوم شهادته. راسل إدارة الخادوم واسألهم عن البصمة المتوقعة. - أضِف اختصارا إلى الشاشة الرئيسية شاشة معلومات التطبيق في النظام دعوات المكالمات @@ -545,10 +526,6 @@ رجاءً اكتب كلمة السر. رجاءً اكتب الوصف بالإنجليزية إن أمكن. تنبيهات النظام - - - - لا شيء محدّد واحدة محدّدة @@ -566,14 +543,11 @@ %d رسالة إخطار غير مقروءة %d رسالة إخطار غير مقروءة - يمنع المستخدم حسب المعرّف المعطى يُلغي المنع عن المستخدم حسب المعرّف المعطى يُحدّد مستوى قدرة المستخدم يدعو المستخدم حسب المعرّف المعطى إلى الغرفة الحالية شغّل/عطّل مارك‌داون - - ألغِ تفعيل الحساب استُبدلت هذه الغرفة ولم تعد نشطة بعد الآن هذه الغرفة هي استمرار لمحادثة أخرى @@ -610,8 +584,8 @@ طرد الإعدادات المتقدمة للإشعارات عند تسجيل الخروج الآن ستخسر مفاتيحك - النسخ الاحتياطي المفاتيح ما زال جاريا. في حال خروجك الآن لن تتمكن لاحقا من قراءة الرسائل المشفرة. - تأكد من تفعيل النسخ الاحتياطي للمفاتيح على كل أجهزتك كي لا تخسر رسائلك المشفرة + النسخ الاحتياطي المفاتيح ما زال جاريا. في حال خروجك الآن لن تتمكن لاحقا من قراءة الرسائل المعماة. + تأكد من تفعيل النسخ الاحتياطي للمفاتيح على كل أجهزتك كي لا تخسر رسائلك المعماة ينسخ احتياطيا المفاتيح… ليس لديك تصريح لبدء إجتماع ليس لديك تصريح لبدء إجتماع في هذه الغرفة @@ -622,10 +596,10 @@ إفصل أبّطل لاشيء - ستفقد الوصول إلى رسائلك المشفرة إلا إذا أخذت نسخة إحتياطية من مفاتيحك قبل تسجيلك للخروج. + ستفقد الوصول إلى رسائلك المعماة إلا إذا أخذت نسخة إحتياطية من مفاتيحك قبل تسجيلك للخروج. نسخة إحتياطية هل أنت متأكد؟ - لا أريد رسائلي المشفرة + لا أريد رسائلي المعماة نسخ إحتياطي للمفتاح إفتراضي النظام غير %1$s عنوان الغرفة الى %2$s. @@ -840,4 +814,12 @@ يستورد المفاتيح… ينزّل المفاتيح… لا يسمح لك بالانضمام لهذه الغرفة + فشلت مكالمة ${app_name} + انسخ رابط النقاش + اعرضه في الغرفة + أقبل + اعرض النقاشات + فشلت إزالة الودجة + فشلت إضافة الودجة + يستمع للإشعارات \ No newline at end of file From 2e5bd0dda6d4fa594f612423630b7bf74d9ae102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 6 Mar 2022 10:20:03 +0000 Subject: [PATCH 095/405] Translated using Weblate (Estonian) Currently translated at 99.9% (2155 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- vector/src/main/res/values-et/strings.xml | 43 ++--------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 21d4aac1ec..44025005d3 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -38,7 +38,6 @@ Telefoninumber Kutse jututuppa %1$s ja %2$s - Tühi jututuba Esmane laadimine: \nImpordin kontot… @@ -265,7 +264,6 @@ Tunnista kehtetuks Katkesta ühendus Teata kahtlasest sisust - või Kutsu Võta vastu @@ -297,7 +295,6 @@ Vaid need, kellel on Matrixi konto Tulemusi ei ole Jututoad - Kogukonnad Saada logikirjed Saada kokkujooksmise logikirjed @@ -323,7 +320,6 @@ Edasta häält Kas oled kindel, et soovid algatada häälkõnet\? Kas oled kindel, et soovid algatada videokõnet\? - Saada faile Saada kleepse Tee foto või video @@ -339,11 +335,9 @@ See ei tundu olema e-posti aadressi moodi Selline e-posti aadress on juba kasutusel. Kas unustasid oma salasõna\? - See koduserver soovib olla kindel, et sa ei ole robot Sinu kontoga seotud e-posti aadress peab olema sisestatud. E-posti aadressi verifitseerimine ei õnnestunud. Palun kontrolli, et sa avasid kirjas leidunud lingi - Palun loe läbi ja nõustu koduserveri kasutusjuhendiga: Palun sisesta korrektne URL See ei ole toimiv Matrix\'i serveri aadress @@ -394,10 +388,7 @@ Videokõne on pooleli… Teine osapool ei võtnud kõnet vastu. Lisateave õiguste kohta - - Kõnede tegemiseks vajab ${app_name} õigusi kasutada sinu mikrofoni. - ${app_name} vajab videokõnedeks õigusi sinu kaamera ja mikrofoni kasutamiseks. \n \nKõnede tegemiseks palun anna järgmisel lehel vajalikud õigused. @@ -408,7 +399,6 @@ Liitu Hülga Mine lugemata sõnumite juurde - Isiklikud sõnumid See võib tähendada, et keegi on suuteline pahatahtlikult sinu veebiliiklust pealtkuulama või sinu telefon ei usalda serveri kasutatavat sertifikaati. Kui serveri haldaja on sind teavitanud, et nii võib juhtuda, siis kontrolli, et sertifikaadi sõrmejälg vastab sellele, mille haldaja sulle on andnud. @@ -455,7 +445,6 @@ Kui sa soovid oma PIN-koodi lähtestada, siis klõpsi nuppu „Unustasin PIN-koodi“. Määra põhiaadressiks Eemalda põhiaadressiks määramine - Teema Fontide suurus Pisike @@ -513,8 +502,6 @@ Palun sisesta taastevõti Verifitseeritud! Selge lugu - - Verifitseerimispäring %s soovib verifitseerida sinu sessiooni Sinu jututoad kuvatakse siin. Olemasolevate jututubade leidmiseks või uute tegemiseks klõpsi all paremal nurgas asuvat + nuppu. @@ -578,8 +565,6 @@ Kuna sina oled selle sessiooni verifitseerinud, siis see sessioon on krüptitud sõnumite saatmiseks usaldusväärne: See isikutuvastusserver kasutab vana API\'t. ${app_name} toetab aga vaid API versiooni 2. See tegevus ei ole võimalik. Koduserveri versioon on liiga vana. - - Kõik sõnumid Lisa avalehele Profiilipilt @@ -698,15 +683,10 @@ Väldi juhuslikke kõnesid Enne kõne algatamist küsi kinnitust Osalejate loend - %d osaleja %d osalejat - - - - Lahku jututoast Kas oled kindel, et soovid lahkuda jututoast\? Kutsu @@ -715,7 +695,6 @@ Taasta ligipääs Müksa välja Maini - Taastevõti on salvestatud. Varukoopia on juba olemas sinu koduserveris Asenda @@ -871,8 +850,6 @@ Lisa Matrix\'i rakendusi Luba süsteemi poolt hallatud kaamera kasutamine Käivita kohandatud vaate asemel süsteemne kaamera vaade. - - Taustapiirangud on Elemendi jaoks keelatud. Seda testi tuleks läbi viia mobiilse andmeside abil (WIFI puudub). \n%1$s Elemendi jaoks on taustpiirangud lubatud. @@ -909,7 +886,6 @@ Kui rakendus töötab taustal, siis sa ei saa saabunud sõnumite kohta teavitusi. Käivita teenus seadme käivitamisel Sünkroniseerimispäring aegus - Viivitus sünkroonimiste vahel Versioon olm-teegi versioon @@ -964,7 +940,6 @@ Eemalda minu konto kasutusest Leia kasutajaid Halda kasutajate otsingu seadistusi. - Analüütika Saada arendajatele analüütikat Võimaldamaks meil rakendust parandada kogub ${app_name} anonüümset teavet rakenduse kasutuse kohta. @@ -973,7 +948,6 @@ Uuenda avalikku nime Viimati nähtud %1$s @ %2$s - Autentimine Sisselogitud kui Koduserver @@ -1000,7 +974,6 @@ \nPane tähele, et antud toiming taaskäivitab rakenduse ja see võib võtta veidi aega. Vali riik Eksporditavate võtmete krüptimiseks palun sisesta paroolifraas. Võtmete importimisel pead kasutama sama paroolifraasi. - Võtmete eksportimine õnnestus Krüptitud sõnumite taastamine Halda võtmete varundust @@ -1016,7 +989,6 @@ Verifitseeri Kinnita seda võrreldes järgnevaid andmeid oma teise sessiooni kasutajaseadetes: Kui nad omavahel ei klapi, siis teie suhtluse turvalisus võib olla ohus. - Vali jututubade loend Serveri aadress Kõik jututoad %s serveris @@ -1025,7 +997,6 @@ %d lugemata teavitatud sõnum %d lugemata teavitatud sõnumit - Jätkamaks pead nõustuma kasutustingimustega. Sa oled lisanud uue sessiooni \'%s\', mis soovib saada krüptimisvõtmeid. Uus sessioon soovib krüptovõtmeid. @@ -1066,8 +1037,6 @@ Avaleht Jututoad Kutsutud - - %2$s müksas sind välja %1$s jututoast Tunnuspilt Selleks et jätkata koduserveri %1$s kasutamist sa pead üle vaatama ja nõustuma meie kasutustingimustega. @@ -1156,7 +1125,6 @@ Kontrollin varukoopia olekut Kustuta varukoopia Kas kustutame krüptovõtmete varukoopia serverist\? Sellisel juhul sa ei saa kasutada ka taastevõtit krüptitud sõnumite ajaloo loetavaks muutmseks. - Turvaline varundus Hoia ära, et kaotad ligipääsu krüptitud sõnumitele ja andmetele Ära kunagi kaota krüptitud sõnumeid @@ -1173,7 +1141,6 @@ Versioon Algoritm Allkiri - Teadmata viga Sa ei kasuta ühtegi isikutuvastusserverit Tundub, et sa üritad luua ühendust teise koduserveriga. Kas sa soovid välja logida\? @@ -1448,7 +1415,6 @@ Trüki ta välja ja hoia turvalises kohas Salvesta ta mälupulgale või varunduskettale Kopeeri ta isiklikku andmehoidlasse mis asub pilves - Kui sa tühistad nüüd, siis sa võid peale viimasest seadmest välja logimist kaotada ligipääsu oma krüptitud sõnumitele ja andmetele. \n \nAga sa võid seadistustes võtta kasutusele turvalise varunduse ning hallata oma krüptovõtmeid. @@ -1527,7 +1493,6 @@ Haldaja sai nüüd teate, et see sisu ei ole sobilik. \n \nKui sa ei soovi enam näha selle kasutaja sisu, siis sa võid tema sõnumite peitmiseks kasutajat eirata. - Eira kasutajat Kõik sõnumid (lärmakas) Kõik sõnumid @@ -1708,7 +1673,6 @@ Halda on Matrix\'i kontoga seotud e-posti aadresse ja telefoninumbreid Kood Palun kasuta rahvusvahelist vormingut (telefoninumbri alguses peaks olema „+“) - Kinnita oma isikusamasust verifitseerides seda sisselogimissessiooni. Sellega tagad ligipääsu krüptitud sõnumitele. Ei ole võimalik avada sellise jututoa vaadet, kus sulle on seatud suhtluskeeld. Ei leia sellist jututuba. Palun kontrolli, et ta ikka olemas on. @@ -1791,7 +1755,6 @@ Otsevestlus Lisa kaasa võtmevahetusega seotud päringute ajalugu Rohkem otsingutulemusi pole - 🔐️ Liitu minuga vestlusrakenduses ${app_name} Hei, palun suhtle minuga vestlusrakenduses ${app_name}: %s Kutsu sõpru @@ -1914,8 +1877,6 @@ Suuna Ühenda Pea esmalt nõu - - Kõne on pooleli (%1$s) Telefoninumbri otsimisel tekkis viga Numbriklahvistik @@ -2112,7 +2073,6 @@ Sisesta serveri nimi, mille sisu sa soovid uurida. Lisa uus server Sinu server - Vabandust, liitumisel tekkis viga: %s Kogukonnakeskuse aadressid Selle kogukonnakeskuse hallatud ja nähtavad aadressid. @@ -2315,7 +2275,6 @@ Küsitluse küsimus või teema Koosta üks küsitlus Küsitlus - Saada e-posti aadressid ja telefoninumbrid %s serverisse Sinu kontaktid on vaid sinu teada. Kui tahad nende hulgast leida Matrix\'i kasutajaid, siis me vajame sinu luba nende andmete saatmiseks räsitud kujul isikutuvastusserverisse. Selleks et leida tuttavaid, sa peaksid saatma oma kontaktteavet (telefoninumbreid ja/või e-posti aadresse) siin rakenduses seadistatud isikutuvastusserverile. Parema andmeturvalisuse nimel me ei saada teavet mitte loetava tekstina, vaid räsina. @@ -2458,4 +2417,6 @@ %d serveri kasutusõiguste muudatus %d serveri kasutusõiguste muudatust + %1$s, %2$s ning teised kasutajad + %1$s ja %2$s \ No newline at end of file From cf6a71702302ce4bf939e0999c4bcd02e3791aa6 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Fri, 4 Mar 2022 23:28:50 +0000 Subject: [PATCH 096/405] Translated using Weblate (Finnish) Currently translated at 80.2% (1730 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fi/ --- vector/src/main/res/values-fi/strings.xml | 105 ++++++++++++++-------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 931075ce8d..7e22ce42a8 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -39,7 +39,6 @@ Huonekutsu %1$s ja %2$s Tyhjä huone - Alkusynkronointi: \nTuodaan tiliä… Alkusynkronointi: @@ -190,7 +189,6 @@ Poista Nimeä uudelleen Ilmoita epäilyttävästä sisällöstä - tai Kutsu Kirjaudu ulos @@ -213,7 +211,6 @@ Ainoastaan Matrix-yhteyshenkilöt Ei tuloksia Huoneet - Lähetä lokit Lähetä kaatumislokit Lähetä näytönkaappauskuva @@ -259,13 +256,10 @@ Puhelu käynnissä… Toinen puoli ei vastannut. Huomio - ${app_name} tarvitsee käyttöluvan mikrofoniin suorittakseen puheluita. - ${app_name} tarvitsee käyttöluvan kameraan ja mikrofoniin suorittakseen videopuheluita. \n \nSalli mikrofonin ja kameran käyttö seuraavilla näytöillä aloittaaksesi tämän puhelun. - KYLLÄ EI Jatka @@ -273,7 +267,6 @@ Liity Hylkää Siirry ensimmäiseen lukemattomaan viestiin. - Poistu huoneesta Haluatko varmasti poistua huoneesta? YKSITYISKESKUSTELUT @@ -299,7 +292,6 @@ Sertifikaatti johon laitteesi luotti aikaisemmin on vaihtunut. Tämä on HYVIN EPÄTAVALLISTA. On suositeltavaa, että ET hyväksy tätä uutta sertifikaattia. Sertifikaatti on vaihtunut ennestään luotetusta ei-luotettuun. Palvelin on voinut uusia sertifikaattinsa. Kysy palvelimen ylläpitäjältä, mikä sormenjäljen pitäisi olla. Hyväksy sertifikaatti vain, jos palvelimen ylläpitäjä on julkaissut sormenjäljen, joka täsmää yllä olevan kanssa. - Etsi Etsi huoneen jäsenistä Ei tuloksia @@ -344,7 +336,6 @@ Päivitä julkinen nimi Viimeksi käytetty %1$s @ %2$s - Tunnistautuminen Kirjautuneena nimellä Kotipalvelin @@ -375,7 +366,6 @@ Nämä ovat kokeellisia ominaisuuksia, jotka voivat mennä rikki. Käytä varoen. Aseta pääosoitteeksi Poista pääosoite - Salauksenpurkuvirhe Julkinen nimi Istunnon tunnus @@ -386,7 +376,6 @@ Vie Anna salasana Vahvista salasana - Tuo salatun huoneen avaimet Tuo huoneen avaimet Tuo avaimet paikallisesta tiedostosta @@ -527,15 +516,10 @@ Saapuvien puheluiden soittoääni Videopuhelu menossa… Käyttäjälista - yksi jäsen %d jäsentä - - - - Poista huoneesta yksi uusi viesti @@ -545,8 +529,6 @@ yksi valittu %d valittu - - Edistyneet ilmoitusasetukset Ilmoituksen tärkeys tapahtumakohtaisesti Ratkaise ilmoituksien ongelmia @@ -621,7 +603,6 @@ Paina lukukuittauksesta nähdäksesi tarkemman listan. Ei vaikuta kutsuihin, poistamisiin ja porttikieltoihin. Sisältää hahmokuvat ja näyttönimien vaihdot. - ${app_name} kerää anonyymiä analytiikkaa sovelluksen parantamiseksi. Luo salalause salataksesi viedyt avaimet. Tarvitset saman salalauseen avainten tuomiseen. Salattujen viestien palautus @@ -630,7 +611,6 @@ yksi lukematon ilmoitettu viesti %d lukematonta ilmoitettua viestiä - yksi huone %d huonetta @@ -656,8 +636,6 @@ Markdown päällä/pois Matrix-sovellusten hallinnan korjaamiseen Hiljainen - - Hahmokuva Jatkaaksesi kotipalvelimen %1$s käyttöä, sinun täytyy hyväksyä palvelun käyttöehdot. Näytä ehdot @@ -715,7 +693,6 @@ Tallenna palautusavain Jaa Tallenna tiedostona - Teethän kopion Jaa palautusavain kohteelle… Luodaan palautusavainta käyttäen salalausetta. Tässä saattaa kestää useampi sekunti. @@ -782,8 +759,6 @@ Näppäimistön enter-näppäin lähettää viestin sen sijaan, että se lisäisi rivinvaihdon Salasana ei ole kelvollinen %1$s -> %2$s - - Media Oletuksena oleva pakkauksen määrä Valitse @@ -887,8 +862,6 @@ Näytä muokkaushistoria Avaimen jakopyyntö Vahvistettu! - - Vahvistuspyyntö %s haluaa vahvistaa laitteesi Viesti-ilmoitusten säännöt @@ -898,7 +871,6 @@ Taustasynkronointitila Ei taustasynkronointia Et saa ilmoituksia saapuvista viesteistä, kun sovellus on taustalla. - Jatkaaksesi sinun täytyy hyväksyä palvelun käyttöehdot. Et käytä mitään identiteettipalvelinta Näyttää, että yrität yhdistää toiseen kotipalvelimeen. Haluatko kirjautua ulos\? @@ -942,7 +914,6 @@ Se on roskapostia Se on sopimaton Ei mitään - Optimoitu akunkestoa varten ${app_name} synkronoi taustalla laitteen rajallisia resursseja (akkua) säästäen. \nLaitteesi resurssien tilasta riippuen käyttöjärjestelmä saattaa lykätä synkronointia. @@ -1025,7 +996,6 @@ Tämä sisältö on ilmiannettu epäsopivana. \n \nJos et halua nähdä enempää sisältöä tältä käyttäjältä, voit estää hänet piilottaaksesi hänen viestit. - Tämä ei ole kelvollinen Matrix-palvelimen osoite Jätä käyttäjä huomiotta Kaikki viestit (äänekäs) @@ -1222,7 +1192,6 @@ Varmenna %s Varmennettu %s Odotetaan käyttäjää %s… - Huoneessa olevat viesti eivät ole salattu osapuolten välisellä salauksella. Huoneen viestit ovat salattu osapuolten välisellä salauksella. \n @@ -1674,8 +1643,6 @@ Aktiivinen puhelu · %1$d aktiivista puhelua · - - Aktiivinen puhelu (%1$s) Numeronäppäimistö Ei vastausta @@ -1926,14 +1893,14 @@ Puuttuvat oikeudet Avaruudet - Vähintään %1$s valinta vaaditaan - Vähintään %1$s valintaa vaaditaan + Vähintään yksi vaihtoehto vaaditaan + Vähintään %1$s vaihtoehtoa vaaditaan Kysymys ei voi olla tyhjä LUO KYSELY - LISÄÄ VALINTA - Valinta %1$d - Luo valinnat + LISÄÄ VAIHTOEHTO + Vaihtoehto %1$d + Luo vaihtoehdot Kysymys tai aihe Kyselyn kysymys tai aihe Luo kysely @@ -1954,4 +1921,66 @@ Lähetä m.room.server_acl-tapahtumia Valitse kotipalvelin Ei nyt + Palauta salaus + lähettää lumisadetta ❄️ + lähettää konfettia 🎉 + 🔐️ Liity seuraani ${app_name}-sovelluksessa + Hei, juttele minulle ${app_name}-sovelluksessa: %s + Kotipalvelimesi (%1$s) ehdottaa, että käytät palvelinta %2$s identiteettipalvelimenasi + Odotetaan salaushistoriaa + Sinulla ei ole pääsyä tähän viestiin, koska lähettäjä jätti avaimet tarkoituksella lähettämättä + Sinulla ei ole pääsyä tähän viestiin, koska lähettäjä ei luota istuntoosi + Sinulla ei ole pääsyä tähän viestiin, koska lähettäjä esti sinut + Ei saatavilla + Tapahtuma lähetetty! + Odotetaan tätä viestiä, tässä voi kestää jonkin aikaa + Sinulla ei ole pääsyä tähän viestiin + %1$s antoi porttikiellon + Äänestäjät näkevät tulokset heti äänestettyään + + Lopullinen tulos yhden äänen perusteella + Lopullinen tulos %1$d äänen perusteella + + + Yksi ääni annettu. Äänestä nähdäksesi tulokset + %1$d ääntä annettu. Äänestä nähdäksesi tulokset + + Ääniä ei annettu + + Perustuen yhteen ääneen + Perustuen %1$d ääneen + + + yksi ääni + %1$d ääntä + + Ääni annettu + Kartta + Jaa sijainti + Sijainti + Jaa sijainti + Suljettu kysely + Avoin kysely + Kyselyn tyyppi + Muokkaa kyselyä + Poista kysely + Lähetä tarra + Lähetä kuvia ja videoita + Avaa kamera + Näytä viestikuplat + Kartan lataaminen epäonnistui + Jaa sijainti + Luo kysely + Jaa sijainti + Näytä vähemmän + Läpisalattu, puhelinnumeroa ei vaadita. Ei mainoksia tai tiedonlouhintaa. + Valitse missä keskustelujasi säilytetään – sinä päätät ja olet riippumaton. Yhdistäjänä Matrix. + Sinä päätät. + Turvallista ja riippumatonta viestintää, joka on yhtä yksityistä kuin keskustelisit kasvokkain kotonasi. + Viestintää tiimillesi. + Turvallista viestintää. + Pidä keskustelusi hallussasi. + Yhdistä palvelimeen + Minulla on jo tili + Luo tili \ No newline at end of file From 9e828822a7d72cf15cf976e4f9d8a5866668b7ad Mon Sep 17 00:00:00 2001 From: Glandos Date: Sun, 6 Mar 2022 21:45:13 +0000 Subject: [PATCH 097/405] Translated using Weblate (French) Currently translated at 99.9% (2156 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- vector/src/main/res/values-fr/strings.xml | 86 ++++++++++++----------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index f56efb4ec3..6bae118e59 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -39,7 +39,6 @@ Invitation au salon Salon vide %1$s et %2$s - Synchronisation initiale : \nImportation du compte… Synchronisation initiale : @@ -217,7 +216,6 @@ Supprimer Renommer Signaler le contenu - ou Inviter Se déconnecter @@ -368,7 +366,6 @@ Nom du serveur d’accueil Tous les salons sur le serveur %s Tous les salons natifs sur %s - Veuillez décrire l’erreur. Qu’avez-vous fait \? Quel était le comportement attendu \? Que s’est-il réellement passé \? Afin de diagnostiquer les problèmes, les journaux de ce client seront envoyés avec ce rapport d’erreur. Ce rapport d’erreur, y compris les journaux et la capture d’écran, ne seront pas visibles publiquement. Si vous préférez envoyer le texte ci-dessus uniquement, veuillez décocher : Vous semblez secouer le téléphone avec agacement. Souhaitez-vous ouvrir soumettre un rapport d’anomalie \? @@ -377,26 +374,19 @@ L’envoi du rapport d’anomalie a échoué (%s) Ceci ne ressemble pas à une adresse e-mail valide Cette adresse e-mail est déjà utilisée. - Ce serveur d’accueil souhaite s’assurer que vous n’êtes pas un robot L’adresse e-mail liée à votre compte doit être saisie. Impossible de vérifier l’adresse e-mail : assurez-vous d’avoir cliqué sur le lien dans l’e-mail - Trop de requêtes ont été envoyées Quitter Citer Le correspondant n’a pas décroché. Information - - ${app_name} a besoin d’accéder à votre microphone pour passer des appels audio. - ${app_name} a besoin d’accéder à votre appareil photo et à votre microphone pour passer des appels vidéo. \n \nVeuillez autoriser l’accès dans les prochaines fenêtres pour pouvoir effectuer l’appel. - Aller au premier message non lu - Voulez-vous vraiment quitter le salon \? Ignorer Afficher tous les messages de cet utilisateur @@ -413,11 +403,9 @@ Le certificat n’est plus celui qui a été approuvé par votre téléphone. Ce comportement est INATTENDU. Il est recommandé de ne PAS ACCEPTER ce nouveau certificat. Le certificat n’est plus celui qui a été approuvé par votre téléphone. Le serveur a peut-être renouvelé son certificat. Contactez l’administrateur du serveur pour lui demander l’empreinte de son certificat. Acceptez le certificat uniquement si l’administrateur du serveur a publié une empreinte correspondant à celle ci-dessus. - Quand je suis invité sur un salon Paramètres utilisateur %1$s @ %2$s - Vérifiez votre e-mail et cliquez sur le lien qu’il contient. Une fois cela fait, cliquez sur continuer. Afficher tous les messages de %s \? \n @@ -426,14 +414,11 @@ Uniquement les membres (à partir de l’activation de cette option) Uniquement les membres (depuis leur invitation) Ce sont des fonctionnalités expérimentales qui peuvent se comporter de façon inattendue. À utiliser avec précaution. - Exporter les clés vers un fichier local - Importer les clés à partir d’un fichier local Ne jamais envoyer de messages chiffrés aux sessions non vérifiées depuis cette session. Confirmez en comparant les informations suivantes avec les paramètres utilisateur dans votre autre session : Si elles ne correspondent pas, la sécurité de votre communication est peut-être compromise. - Interface utilisateur Langue Choisissez une langue @@ -508,7 +493,6 @@ Motif : %1$s Badge Secouer avec agacement pour signaler une anomalie - %d membre %d membres @@ -518,13 +502,10 @@ %d nouveaux messages Liste les membres - - %d message notifié non lu %d messages notifiés non lus - %d salon %d salons @@ -584,16 +565,10 @@ La conversation continue ici Ce salon est la suite d’une autre conversation Cliquer ici pour voir les anciens messages - - - - %d sélectionné %d sélectionnés - - Alertes système contacter l’administrateur de votre service Ce serveur d’accueil a dépassé une de ses limites de ressources donc certains utilisateurs ne pourront pas se connecter. @@ -685,7 +660,6 @@ ${app_name} n’est pas affecté par l’optimisation de la batterie. Si un utilisateur laisse un appareil débranché et immobile pour une longue durée, avec l’écran éteint, l’appareil entre en mode veille.. Cela empêche les applications d’accéder au réseau et reporte leurs tâches, synchronisations et alarmes standard. Ignorer l’optimisation - Aucun APK des services Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement. Appel vidéo en cours… Sauvegarde de clé @@ -722,7 +696,6 @@ Terminé Sauvegarder la clé de récupération Enregistrer dans un fichier - Veuillez en faire une copie Partager la clé de récupération avec… Génération de la clé de récupération utilisant la phrase secrète. Cette opération peut prendre plusieurs secondes. @@ -808,7 +781,6 @@ Algorithme Signature Pour utiliser la sauvegarde de clés sur cette session, faites une restauration avec votre phrase secrète ou votre clé de récupération. - Traitement de la clé de récupération… Téléchargement des clés… Importation des clés… @@ -817,7 +789,6 @@ Envoyer le message avec Entrée Le bouton Entrée sur le clavier logiciel enverra le message au lieu d’aller à la ligne Le mot de passe n’est pas valide - Média Compression par défaut Choisir @@ -854,8 +825,6 @@ Ignorer Vérifié ! Compris - - Demande de vérification %s veut vérifier votre session Erreur inconnue @@ -952,7 +921,6 @@ Aucun Révoquer Déconnecter - Impossible de joindre le serveur d’accueil à cette URL, veuillez la vérifier Mode de synchronisation en arrière-plan Optimisé pour préserver la batterie @@ -963,7 +931,6 @@ \nCela aura un impact sur l’utilisation des données mobiles et de la batterie, une notification permanente sera affichée indiquant que ${app_name} est à l’écoute des évènements. Aucune synchronisation en arrière-plan Vous ne serez pas notifié des messages entrants quand l’application est en arrière-plan. - Découverte Gérer vos paramètres de découverte. Vous n’utilisez aucun serveur d’identité @@ -1033,7 +1000,6 @@ Ce contenu a été signalé comme inapproprié. \n \nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez l’ignorer pour masquer ses messages. - Intégrations Utilisez un gestionnaire d’intégrations pour gérer les robots, les passerelles, les widgets et les jeux d’autocollants. \nLes gestionnaires d’intégrations reçoivent des données de configuration et peuvent modifier des widgets, envoyer des invitations de salon et définir des rangs à votre place. @@ -1249,7 +1215,6 @@ Vérifier %s %s a été vérifié Nous attendons %s… - Les messages dans ce salon ne sont pas chiffrés de bout en bout. Les messages dans ce salon sont chiffrés de bout en bout. \n @@ -1395,7 +1360,6 @@ Imprimez-le et conservez-le en lieu sûr Sauvegardez-le sur une clé USB ou un disque de sauvegarde Copiez-le sur votre stockage dans le cloud personnel - Chiffrement activé Les messages de ce salon sont chiffrés de bout en bout. Apprenez-en plus et vérifiez les utilisateurs sur leur profil. Chiffrement désactivé @@ -1779,7 +1743,6 @@ Veuillez fournir une adresse de salon Cette adresse est déjà utilisée %1$d de %2$d - Autoriser Révoquer mon autorisation Vous avez donné votre autorisation pour envoyer des e-mails et des numéros de téléphone à ce serveur d’identité pour découvrir d\'autres utilisateurs à partir de vos contacts. @@ -1858,8 +1821,6 @@ Transférer Rejoindre Consulter d’abord - - Appel en cours (%1$s) Il y a eu une erreur lors de la recherche du numéro de téléphone Pavé de numérotation @@ -2060,7 +2021,7 @@ Moi et mon équipe Un espace privé pour organiser vos salons Seulement moi - Assurez-vous que les accès à %s sont accordés aux bonnes personnes. Vous pourrez changer ceci plus tard. + Assurez-vous que les bonnes personnes ont accès à %s. Avec qui travaillez-vous \? Pour rejoindre un espace existant, il vous faut une invitation. Le fichier est trop volumineux pour être envoyé. @@ -2112,7 +2073,6 @@ Ajouter un nouveau serveur Votre serveur Consultation de %1$s - Désolé, une erreur est survenue en essayant de rejoindre %s Adresse de l’espace Afficher et gérer les adresses de cet espace. @@ -2313,7 +2273,6 @@ Question ou sujet du sondage Créer un sondage Sondage - Envoyer des courriels et des numéros de téléphone à %s Vos contacts sont personnels et privés. Pour découvrir des utilisateurs à partir de vos contacts, nous avons besoin de votre permission pour envoyer les informations des contacts à votre serveur d’identité. La session a été déconnectée ! @@ -2413,4 +2372,47 @@ Communication indépendante et sécurisée qui vous donne le même niveau d\'intimité qu\'une discussion face-à-face dans votre maison. Localisation Le chiffrement a été mal configuré ce qui vous empêche d\'envoyer des messages. Cliquez pour ouvrir les paramètres. + Notification du salon + Utilisateurs + Notifier tout le salon + + %1$d de plus + %1$d de plus + + Réduire + Afficher les messages en bulles + Impossible de charger la carte + Carte + Note : l’application sera redémarrée + Activer les messages en fils de discussion + Se connecter au serveur + Vous cherchez à joindre un serveur existant \? + passer cette question + Pas encore sûr \? Vous pouvez %s + Communautés + Équipes + Famille et amis + Nous allons vous aider à vous connecter. + À qui allez-vous le plus parler \? + Vous êtes déjà en train de voir ce fil de discussion ! + Voir dans le salon + Répondre dans le fil de discussion + La commande « %s » est connue mais non supportée dans les fils de discussion. + Depuis un fil de discussion + Indice : Appui long sur un message puis « %s ». + Les fils de discussion vous permettent de centrer vos conversations sur un sujet, et de les suivre facilement. + Gardez vos conversations organisées avec les fils de discussion + Affiche tous les fils de discussion auxquels vous avez participé + Mes fils de discussion + Affiche tous les fils de discussion du salon actuel + Tous les fils de discussion + Filtrer + Fils de discussion + Fil de discussion + Filtrer les fils de discussion du salon + %1$s, %2$s et d’autres + %1$s et %2$s + Copier le lien du fil de discussion + Voir dans le salon + Voir les fils de discussions \ No newline at end of file From 8b7606bd203ec1aa988ca862bf71f1aa92260faf Mon Sep 17 00:00:00 2001 From: notramo Date: Mon, 7 Mar 2022 20:27:43 +0000 Subject: [PATCH 098/405] Translated using Weblate (Hungarian) Currently translated at 99.8% (2154 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/ --- vector/src/main/res/values-hu/strings.xml | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 000da846b8..f189a4198f 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1562,7 +1562,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Szoba beállításainak módosítása sikeres Nem érheted el ezt az üzenetet Várakozás erre az üzenetre, ez eltarthat egy darabig - A végpontok közötti titkosítás miatt lehet hogy várnod kell, hogy valaki üzenet megérkezzen, mert a titkosítási kulcsok nem lettek megfelelően elküldve neked. + A végpontok közötti titkosítás miatt lehet hogy várnod kell, hogy valaki üzenetét el tudd olvasni, mert a titkosítási kulcsok nem lettek megfelelően elküldve neked. Nem érheted el ezt az üzenetet, mert a küldő letiltott Nem érheted el ezt az üzenetet, mert a feladó nem bízik a munkamenetedben Nem érheted el ezt az üzenetet, mert a feladó szándékosan nem küldte el a kulcsokat @@ -1829,7 +1829,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Én és a csoporttársaim Privát tér a szobáid csoportosításához Csak én - Ellenőrizd, hogy a megfelelő személyeknek van hozzáférése ide: %s. + Ellenőrizd, hogy a megfelelő személyeknek van hozzáférésük ehhez: %s. Csak az olvasatlan üzenetek számát mutassa az egyszerű értesítésekben. Kivel dolgozol együtt\? Létező térbe való belépéshez meghívó szükséges. @@ -2376,25 +2376,25 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Térkép betöltése sikertelen Térkép Figyelem: az alkalmazás újraindul - Üzenetszálak engedélyezése + Üzenetszálak bekapcsolása Szerverhez csatlakozás - Csatlakoznál egy már meglévő szerverhez\? - kérdés kihagyása - Még nem vagy biztos\? Tudhatsz ilyent: %s + Csatlakoznál egy meglévő szerverhez\? + Kihagyhatod ezt a kérdést. + Még nem tudod\? %s Közösségek Csoportok Barátok és család Segítünk a kapcsolatteremtésben. - Kivel beszélgetnék leginkább\? - Már nézed ezt az üzenetszálat! + Kikkel fogsz legtöbbet beszélgetni\? + Jelenleg ezt az üzenetszálat olvasod! Megjelenítés szobában - Válasz az üzenetszálban + Válasz üzenetszálban „%s” parancs ismert, de üzenetszálban nem támogatott. - Az üzenetszálból - Tipp: Koppints hosszan az üzenetre és használd ezt: %s. + Egy üzenetszálból + Tipp: Koppints hosszan az üzenetre és használd a \"%s\" opciót. Az üzenetszálak segítenek a különböző témájú beszélgetések figyelemmel kísérésében. - Beszélgetések üzenetszálakba való rendezése - Minden üzenetszál megjelenítése ahol szerepel + Beszélgetések üzenetszálakba rendezése + Minden üzenetszál megjelenítése, ahová üzenetet küldtél Üzenetszálaim A szobában lévő összes szál mutatása Minden üzenetszál @@ -2417,6 +2417,6 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze %d szerver jogosultság változott %d szerver jogosultság változott - %1$s, %2$s és még mások + %1$s, %2$s és mások %1$s és %2$s \ No newline at end of file From e6d81b3a7712e6ad27c673cd4700ec3746344d6c Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 7 Mar 2022 06:14:01 +0000 Subject: [PATCH 099/405] Translated using Weblate (Japanese) Currently translated at 98.6% (2128 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- vector/src/main/res/values-ja/strings.xml | 114 +++++++++++----------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index c1f3304fb8..e87e5653ca 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -139,8 +139,8 @@ メールアドレスを追加 電話番号を追加 通知音 - このアカウントで通知を許可 - このセッションで通知を許可 + このアカウントでは通知を有効にする + このセッションでは通知を有効にする 1対1のチャットでのメッセージ グループチャットでのメッセージ ルームへ招待されたとき @@ -219,7 +219,7 @@ 送信 このホームサーバーは、あなたがロボットではないことの確認を求めています アカウントに登録されたメールアドレスの入力が必要です。 - メールアドレスの確認に失敗しました:電子メールのリンクをクリックしたことを確認してください + メールアドレスの認証に失敗しました:電子メールのリンクをクリックしたことを確認してください 不正な形式のJSON 有効なJSONを含んでいませんでした ログイン要求が多すぎます @@ -273,14 +273,14 @@ 発言を通報 写真を撮影 動画を撮影 - 検証を開始 + 認証を開始 リクエストにuser_idがありません。 リクエストにroom_idがありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 ウィジェットをこのルームから削除してもよろしいですか? - 一致していない場合は、あなたのコミュニケーションの安全性が損なわれている可能性があります。 - このセッションでは、未検証のセッションに対して暗号化されたメッセージを送信しない。 + 一致していない場合は、コミュニケーションのセキュリティーが破られている可能性があります。 + このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 認証済のセッションに対してのみ暗号化 インポート ローカルファイルから鍵をインポート @@ -300,7 +300,7 @@ 信用する 信用しない フィンガープリント(%s): - リモートサーバーのIDを確認できませんでした。 + リモートサーバーのIDを認証できませんでした。 誰かが不当にあなたの通信を傍受しているか、あなたの電話がリモートサーバーの証明書を信用していない可能性があります。 サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 @@ -420,7 +420,7 @@ アカウントを停止するときに、自分の送信した全てのメッセージの履歴を消去してください(警告: この操作により、今後のユーザーは会話を不完全な形で見ることになります) アカウントを停止 パスワードを入力してください。 - このルームは交換されており、使用されていません。 + このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです 以前のメッセージを見るには、ここをクリックしてください @@ -438,7 +438,7 @@ %1$s:%2$s %d+ 展開 - 畳む + 折りたたむ 承諾 このホームサーバーの方針を確認し承諾してください: 通話設定画面 @@ -569,7 +569,7 @@ %d件のアクティブなセッション - このログインを検証 + このログインを認証 QRコード はい いいえ @@ -580,7 +580,7 @@ 削除の確認 このイベントを削除してよろしいですか?ルーム名や説明の変更を削除すると、変更が取り消されますのでご注意ください。 暗号化は有効です - このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や検証はユーザーのプロフィールをご確認ください。 + このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 暗号化が有効になっていません 通知設定 切断 @@ -657,7 +657,7 @@ 招待されています %sからの招待 概ね完了しました。%sの画面にも同じシールドアイコンが表示されていますか? - 相手ユーザーの端末のコードをスキャンし、相互に安全性を検証 + 相手ユーザーの端末のコードをスキャンし、相互に安全性を認証 相手のコードをスキャン スキャンできません 拒否 @@ -1147,10 +1147,10 @@ \nセッション名:%1$s \n最後のオンライン日時:%2$s \n新たにログインして新しいセッションを開始しなかった場合、この要求を無視してください。 - 未認証のセッションが暗号鍵を要請しています。 + 未認証のセッションが暗号鍵を要求しています。 \nセッション名:%1$s \n最後のオンライン日時:%2$s -\n新たにログインして新しいセッションを開始しなかった場合、この要求を無視してください。 +\n新しいセッションにログインしていない場合、この要求を無視してください。 鍵の共有リクエスト カスタムカメラ画面の代わりにシステムカメラを使用します。 使用中のウィジェットがありません @@ -1326,7 +1326,7 @@ 会話 未読メッセージはありません 未読はありません! - %sがセッションの検証を要求しています + %sがセッションの認証を要求しています リトライ 他のホームサーバーに接続しようとしているようですね。サインアウトしますか? IDサーバーを使用していません @@ -1334,7 +1334,7 @@ このルームを含む参加済のスペース このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 了解 - 完了しました! + 完了しました! メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ @@ -1477,7 +1477,7 @@ スパムとして報告済 このルームにファイルはありません このルームにメディアはありません - 公開されたルームをアップグレード + 公開ルームをアップグレード 非公開スペース 公開スペース 送信済 @@ -1566,7 +1566,7 @@ 有効にする あなたのIDサーバーの運営方針 新しいルームを作成 - 認証コードが正しくありません。 + 確認コードが正しくありません。 IDサーバーのURLを入力してください 名前、ID、メールアドレスで検索 システムの設定 @@ -1667,7 +1667,7 @@ イベントを送信しました! 送信に失敗した全てのメッセージを削除 失敗しました - 公開されたルーム + 公開ルーム アバターを削除 アバターを変更 かけ直す @@ -1705,7 +1705,7 @@ ユーザーを招待しています… パスワードを選択してください。 メンバーを追加 - ログインを検証 + ログインを認証 メッセージを送る… このファイルは大きすぎてアップロードできません。 この情報の送信に同意しますか? @@ -1838,7 +1838,7 @@ 一般 転送 信頼済 - 検証済 + 認証済 未送信のメッセージを削除 カスタムイベントを送信 ルームの状態を探索 @@ -1857,7 +1857,7 @@ %1$d個の投票があります。結果を見るには投票してください - 未検証の端末で暗号化 + 未認証の端末で暗号化 メッセージを紙吹雪と共に送る メッセージを降雪と共に送る 紙吹雪🎉を送る @@ -1954,7 +1954,7 @@ パスワードはまだ変更されていません。 \n \n変更作業を中止しますか? - %1$sに認証メールを送信しました。 + %1$sに確認メールを送信しました。 メールボックスを確認してください サインインに戻る 元の大きさのままメディアファイルを送信 @@ -1968,18 +1968,18 @@ 認証の要求 %sがキャンセルしました 既読 - 検証 + 認証 メールアドレスが正しくないようです 国際電話番号の形式を使用してください。 国際電話番号は「+」から始まる必要があります コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 - パスワードの再設定を確認するために認証メールを送信します。 - このメールアドレスはどのアカウントにも属していません。 - このアプリではこのホームサーバーにアカウントを作成できません。 + パスワードの再設定を確認するために確認メールを送信します。 + このメールアドレスはどのアカウントにも登録されていません。 + このアプリでは、このホームサーバーにアカウントを作成できません。 \n -\nウェブクライエントを使用してアカウント登録しますか? +\nウェブクライアントを使用してアカウントを作成しますか? 申し訳ありませんが、このサーバーはアカウントの新規登録を受け入れていません。 このアプリではこのホームサーバーにサインインできません。このホームサーバーは次のサインインの方法に対応しています:%1$s \n @@ -2037,11 +2037,11 @@ セキュリティーキーを保存 位置情報を共有しました %sでリアクションしました - 検証終了 + 認証が完了しました 次のいずれかのセキュリティーが破られている可能性があります。 \n \n - あなたのホームサーバー -\n - 検証している相手のホームサーバー +\n - 認証している相手が接続しているホームサーバー \n - あなたか相手のインターネット接続 \n - あなたか相手の端末 セキュアではない @@ -2130,23 +2130,23 @@ ナビゲーションのメニューを開く 承諾しました %sが承諾しました - %sが検証済み - %sを検証する - 絵文字を比較して検証 - 絵文字を比較して検証 + %sが認証済 + %sを認証する + 絵文字を比較して認証 + 絵文字を比較して認証 対面でない場合は、代わりに絵文字を比較してください あなたのホームサーバーで許容されている添付ファイルの最大サイズは%sです。 新しいパスワードを確認するには下記のリンクを開いてください。リンクにアクセスしてから、以下をクリックしてください。 PINコードを再設定するには「PINコードを忘れた」をタップしてログアウトし、その後再設定してください。 - いま検証できる%d個の端末を表示 + いま認証できる%d個の端末を表示 この操作を実行するには ${app_name}に認証情報を入力する必要があります。 あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。 - 暗号化されたメッセージにアクセスするには、ログインを検証し、本人確認を行う必要があります。 - 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを検証し、本人確認を行う必要があります。 + 暗号化されたメッセージにアクセスするには、ログインを認証し、本人確認を行う必要があります。 + 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを認証し、本人確認を行う必要があります。 詳しく知る - セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを検証しましょう。 + セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを認証しましょう。 暗号化の設定が正しくありません。 暗号化を復元 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 @@ -2163,12 +2163,12 @@ 完了! アカウントパスワードと違うものにしてください。 続行するには%sを入力してください。 - 検証を中止しました - 今中止すると、%1$s(%2$s)を検証しません。検証は相手のユーザープロフィール画面からもう一度開始できます。 + 認証を中止しました + 今中止すると、%1$s(%2$s)を認証しません。認証は相手のユーザープロフィール画面からもう一度開始できます。 中止すると、新しい端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません 中止すると、この端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません 自分ではない - 新しいセッションを検証して、暗号化されたメッセージにアクセスできるようにしましょう。 + 新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 新しいログイン。あなたですか? ${app_name} Android ルームの管理者によって削除されています。理由:%1$s @@ -2180,9 +2180,9 @@ \n \n予期しないトラブルを起こす可能性があるので注意してください。 %1$s(%2$s)が新しいセッションでサインインしました: - このセッションは%1$s(%2$s)によって検証されているので、メッセージのセキュリティは信頼できます。 - 既存のセッションでこのセッションを検証して、暗号化されたメッセージへアクセスできるようにしましょう。 - あなたはこのセッションを検証しているので、メッセージのセキュリティは信頼できます。 + このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティは信頼できます。 + 既存のセッションでこのセッションを認証して、暗号化されたメッセージへアクセスできるようにしましょう。 + あなたはこのセッションを認証しているので、メッセージのセキュリティは信頼できます。 利用可能な暗号情報がありません 既定のバージョン 非公開のルームとダイレクトメッセージにおけるエンドツーエンド暗号化は、あなたのサーバーの管理者により既定として無効にされています。 @@ -2198,7 +2198,7 @@ あと少しです!確認を待機しています… あと少しです!もう一方のデバイスは同じマークを表示していますか? %sを待機しています… - このユーザーがこのセッションを検証するまで、送受信されるメッセージには警告マークが付きます。手動で検証することも可能です。 + このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 セッションの取得に失敗しました 誰がチームの仲間ですか? %sを探索できるようになります @@ -2228,7 +2228,7 @@ 再認証が必要です 全てリセット 連絡先 - 検証をキャンセルしました。あらためて開始してください。 + 認証をキャンセルしました。あらためて開始してください。 押し続けて録音し、離すと送信 PINコードを設定してください @@ -2237,9 +2237,9 @@ 置き換えられたルームに参加 このルームが発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 - 絵文字で検証 - テキストで検証 - すべてのセッションを検証し、アカウントとメッセージが安全であることを確認してください + 絵文字で認証 + テキストで認証 + すべてのセッションを認証し、アカウントとメッセージが安全であることを確認してください ログインしている場所を確認 復旧用の手段を全て無くしてしまいましたか?全てリセットする クロス署名に対応した他のMatrixのクライアントでも使用できます。 @@ -2282,12 +2282,12 @@ \n \n続行してよろしいですか? このリンクを再確認してください - ログインを検証してください:%1$s + ログインを認証してください:%1$s 機密ストレージのアクセスに失敗しました この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 このアカウントは無効化されています。 - 個人のクラウドストレージにコピーしましょう - 印刷して安全な場所に保管しましょう + 個人用のクラウドストレージにコピー + 印刷して安全な場所に保管 %2$sと%1$sが設定されました。 \n \n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 @@ -2300,7 +2300,7 @@ PINコードでしか${app_name}のロックを解除することはできません。 ${app_name}を開く際には、毎回PINコードの入力が必要です。 あなたがブロックされているルームを開くことはできません。 - PINコードの検証に失敗しました。新しいコードを入力してください。 + PINコードの認証に失敗しました。新しいコードを入力してください。 端末の連絡先がありません 暗号化の履歴を待機しています 送信者があなたのセッションを信頼していないため、このメッセージにアクセスすることができません @@ -2318,8 +2318,8 @@ 絵文字の一覧を開く 認証に失敗しました 復旧を設定しています。 - このセッションは、他のセッションと検証を共有することができません。 -\n検証は端末に保存され、新しいバージョンのアプリで共有されます。 + このセッションは、他のセッションと認証を共有することができません。 +\n認証は端末に保存され、新しいバージョンのアプリで共有されます。 %1$s、%2$s他 %1$sと%2$s 自分と相手を認証して、チャットを安全に保ちましょう @@ -2331,7 +2331,7 @@ 音声通話が拒否されました %1$sは通話を拒否しました このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 - このセッションを信頼済として検証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: + このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 最新の${app_name}は他のデバイスでも使用できます: @@ -2340,4 +2340,6 @@ メールアドレスで招待したり、連絡先を検索したりできます… 鍵のバックアップの機密情報をSSSSに保存しています あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。 + 詳細を非表示 + 他の参加者はいません。%sに招待しましょう。 \ No newline at end of file From e1c61e3e309566e643a3e20240ad3fc4c8ae2188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADcolas=20F=2E=20R=2E=20A=2E=20Prado?= Date: Fri, 4 Mar 2022 02:24:21 +0000 Subject: [PATCH 100/405] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2157 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- vector/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index b9e2fa3791..cd237407d2 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -1414,7 +1414,7 @@ Quase lá! %s está mostrando um tick (✓)\? Sim Não - Conectividade ao servidor tem sido perdida + A conexão com o servidor foi perdida Modo avião está ligado Ferramentas Dev Dados de Conta @@ -2107,7 +2107,7 @@ %d chamadas de vídeo perdidas - Chamadad de áudio perdida + Chamada de áudio perdida %d chamadas de áudio perdidas Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão permanecer nesta sala arquivada. From 37bbaa983f0687616298071e7e4a7f9879ac1a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Mon, 7 Mar 2022 22:22:00 +0000 Subject: [PATCH 101/405] Translated using Weblate (Slovak) Currently translated at 99.3% (2143 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- vector/src/main/res/values-sk/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 3ea0dc14a8..410939ef62 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -2423,7 +2423,7 @@ Udalosť vymazaná používateľom, dôvod: %1$s Dôvod úpravy ${app_name} sa stretol s problémom pri vykresľovaní obsahu udalosti s id \'%1$s\' - Reagoval/a s: %s + Reagoval/a: %s ${app_name} môže padať častejšie, keď sa vyskytne neočakávaná chyba Zadajte adresu servera, ktorý chcete použiť Vlastné hlásenie… @@ -2445,4 +2445,11 @@ Zaškrtnuté Rozpísaná správa Otvoriť navigačnú ponuku + Videli + + %1$d ďalšia + %1$d ďalšie + %1$d ďalších + + Zadajte URL adresu servera Modular Element alebo adresu servera, ktorý si želáte použiť \ No newline at end of file From 4d0cde6fb9413c90137b60d032e365e5a79fda58 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 7 Mar 2022 22:18:58 +0000 Subject: [PATCH 102/405] Translated using Weblate (Slovak) Currently translated at 99.3% (2143 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- vector/src/main/res/values-sk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 410939ef62..8d97ff7266 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -2439,7 +2439,7 @@ %1$s a %2$s Pokračovať pomocou jednotného prihlásenia SSO jednotné prihlásenie SSO - Zatvoriť výzvu zálohovanie kľúčov + Zatvoriť výzvu na zálohovanie kľúčov Výťazná odpoveď Nezaškrtnuté Zaškrtnuté From d8f8d4a40c084c7deb373b772246ac3411459d70 Mon Sep 17 00:00:00 2001 From: notramo Date: Mon, 7 Mar 2022 20:38:25 +0000 Subject: [PATCH 103/405] Translated using Weblate (Hungarian) Currently translated at 98.0% (50 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/hu/ --- .../android/hu-HU/full_description.txt | 61 +++++++++---------- .../android/hu-HU/short_description.txt | 2 +- fastlane/metadata/android/hu-HU/title.txt | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt index 899b4cd978..0791eed7ba 100644 --- a/fastlane/metadata/android/hu-HU/full_description.txt +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -1,43 +1,42 @@ -Element egy biztonságos üzenetküldő és csapatmunka támogató alkalmazás ami ideális távoli munkavégzés közben csoportos csevegéshez. Az alkalmazás végpontok közötti titkosítást használ videó konferenciához, fájl megosztáshoz és videó hivásokhoz. +Az Element egy biztonságos üzenetküldő, és egy csapatmunka app, amely távoli munkavégzéshez is alkalmas lehet. Az alkalmazás végponti titkosítás használatával biztosít videó konferencia, fájlmegosztás, és audio hívás lehetőségeket. -Element tulajdonságai: -- Fejlett online kommunikációs eszköz -- Teljesen titkosított üzenetküldés biztonságos céges kommunikációt kínál még a távdolgozóknak is -- Elosztott csevegés a Matrix nyílt forráskódú keretrendszer felhasználásával -- Bizontságos fájl megosztás titkosítottan projektek kezeléséhez -- Videó hívás VoIP-pal és képernyőmegosztással -- Könnyen integrálható a kedvenc online kollaborációs eszközöddel, projekt menedzsment eszközzel, VoIP szolgáltatással vagy más csoport üzenetküldő alkalmazással +Az Element funkciói többek között: +- Fejlett online kommunikációs eszközök +- Titkosított üzenetek a biztonságos céges kommunikációhoz, otthonról dolgozóknak is +- Decentralizált chat a nyílt forráskódú Matrix protokoll használatával +- Biztonságos fájlmegosztáss a projektek kezeléséhez +- Videochat, VoIP, és képernyőmegosztási lehetőséggel +- Egyszerű integráció a kedvenc online kollaborációs eszközeiddel, projektkezelési eszközökkel, VoIP szolgáltatásokkal, és más csoportos üzenetküldő alkalmazásokkal -Element teljesen más mint a többi üzenetküldő alkalmazás. Matrixot használ, egy nyílt hálózatot a decentralizált biztonságos kommunikációhoz. Lehetőséget ad saját szerver üzemeltetésére ami maximális tulajdont és kontrollt biztosít az adatok fölött. +Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages. -Magánélet védelme és titkosított üzenetküldés -Element megóv a kéretlen hirdetésektől, adatbányászattól és a különböző szigetszerű megoldásoktól. Minden adatot biztonságba helyez, egy az egybe videó és hang kommunikáció végpontok között titkosítva ahol az eszközök hitelesítve vannak. +Privacy and encrypted messaging +Element protects you from unwanted ads, data mining and walled gardens. It also secures all your data, one-to-one video and voice communication through end-to-end encryption and cross-signed device verification. -Element a kezedbe adja az adatvédelmi irányítást miközben bárkivel kommunikálhatsz a Matrix hálózatban vagy más üzleti kollaborációs eszközzel ami integrálva van, mint amilyen a Slack. +Element gives you control over your privacy while allowing you to communicate securely with anyone on the Matrix network, or other business collaboration tools by integrating with apps such as Slack. -Element futtatható saját szerveren +Element can be self-hosted +To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility. -Azért, hogy az érzékeny adatok és beszélgetések minnél inkább az irányításod alatt lehessen az Elementet saját magadnak üzemeltetheted vagy választhatsz bármely Matrixon alapuló - szabványos nyílt forráskódú és decentralizált kommunikáció - szoláltató közül. Element adatvédelmet, biztonságot és rugalmas integrációkat biztosít. +Own your data +You decide where to keep your data and messages. Without the risk of data mining or access from third parties. -A te adatod a tiéd -Te döntöd el, hogy hol tárolod az adataidat és üzeneteidet. Adatbányászat vagy harmadik fél hozzáférésének kockázata nélkül. +Element puts you in control in different ways: +1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers +2. Self-host your account by running a server on your own IT infrastructure +3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform -Element többféle képpen adja vissza az irányítást: -1. Szerezz egy ingyenes hozzáférést a matrix.org nyilvános szerverre amit a Matrix fejlesztők üzemeltetnek vagy válassz a több ezer önkéntesek által üzemeltetett nyilvános szerverből -2. Üzemeltess szerver magadnak a saját infrastruktúrádon -3. Iratkozz fel egy egyedi szerverre az Element Matrix Services platformon +Open messaging and collaboration +You can chat with anyone on the Matrix network, whether they’re using Element, another Matrix app or even if they are using a different messaging app. -Nyílt üzenetküldés és kollaboráció -Bárkivel beszélgethetsz a Matrix hálózaton, akár az Elementet használja akár egy másik Matrix alkalmazást használ vagy akár egy eltérő üzenetküldőt. +Super secure +Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signed device verification. -Fantasztikusan biztonságos -Igazi végpontok között titkosítás (csak a beszélgetésben résztvevők tudják visszafejteni) és hitelesítés eszközök közötti aláírásokkal. +Complete communication and integration +Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done. -Teljes kommunikáció és integráció -Üzenetküldés, hang és videóhívás, fájl megosztás, képernyő megosztás és egy csomó integráció, botok és kisalkalmazások. Építs szobákat, közösségeket, maradj kapcsolatban és végezz el dolgokat. +Pick up where you left off +Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io -Vedd fel a fonalat -Maradj kapcsolatban bárhol minden eszközödön a szinkronizált üzenetekkel és a weben a https://app.element.io oldallal - -Nyílt forráskód -Element Android egy nyílt forráskódú projekt a GitHubon. Küldj hibajegyet és/vagy vegyél részt a fejlesztésében itt: https://github.com/vector-im/element-android +Open source +Element Android is an open source project, hosted by GitHub. Please report bugs and/or contribute to its development at https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt index 2dfe14c516..51be689331 100644 --- a/fastlane/metadata/android/hu-HU/short_description.txt +++ b/fastlane/metadata/android/hu-HU/short_description.txt @@ -1 +1 @@ -Csoportos üzenetküldő - titkosított üzenetek, videó hívások +Csoportos üzenetküldő - titkosított üzenetek és videó hívások diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt index 907f907f99..c463dea393 100644 --- a/fastlane/metadata/android/hu-HU/title.txt +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -1 +1 @@ -Element +Element - Biztonságos üzenetküldő From bf619204e1a5d6aa459a78e8ed9ebfe86a182d1c Mon Sep 17 00:00:00 2001 From: Edward Gera Date: Mon, 7 Mar 2022 14:54:14 +0000 Subject: [PATCH 104/405] Translated using Weblate (Hebrew) Currently translated at 85.5% (1846 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/he/ --- vector/src/main/res/values-iw/strings.xml | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/vector/src/main/res/values-iw/strings.xml b/vector/src/main/res/values-iw/strings.xml index 57a8cf6630..3d9cfd1154 100644 --- a/vector/src/main/res/values-iw/strings.xml +++ b/vector/src/main/res/values-iw/strings.xml @@ -2067,4 +2067,73 @@ יש לי כבר חשבון צור חשבון תקשורת מאובטחת. + סגור את קוֹטֵף האמוג\'י + פתח את קוֹטֵף האמוג\'י + רמת האמון אמינה + אזהרה רמת האמון + ברירת המחדל של רמת האמון + נבחר + וידאו + יש טיוטה שלא נשלחה + חלק מההודעות לא נשלחו + מחיקת אווטאר + החלף אווטאר + תמונה + ייבוא מפתחות מהקובץ + פתח יישומונים + צילום מסך + האימות נכשלה + ${app_name} כדי לבצע פעולה זו נדורש להזין את פרטי התחברות שלך. + יש צורך באימות מחדש + החלק כדי לסיים את השיחה + אדם לא ידוע + העבר אל %1$s + התייעצות עם %1$s + משתמשים + אירעה שגיאה בהעברת השיחה + הַעֲבִר + התחבר + התייעץ קודם + %1$s הקש לחזרה + שיחה פעילה (%1$s) · + + שיחה פעילה · + %1$d שיחות פעילות · + + + + שיחה פעילה (%1$s) + אירעה שגיאה בחיפוש מספר הטלפון + לוח חיוג + אין מענה + שיחה וידאו שלא נענתה + שיחה קולית שלא נענתה + השיחה קולית נדחתה + שיחת וידאו נדחתה + שיחת הווידאו הסתיימה • %1$s + השיחה הקולית הסתיימה • %1$s + שיחת קולית פעילה + שיחת וידאו פעילה + שיחה וידאו נכנסת + שיחה קולית נכנסת + שיחה חוזרת + השיחה הזו הסתיימה + %1$s דחה את השיחה הזו + דחית את השיחה הזו + התראת חדר + להודיע לכל החדר + + %1$d יותר + + + + + %1$s, %2$s ואחרים + %1$s ו %2$s + + %d שינוי ברשימות ACL בשרתים + + + + \ No newline at end of file From a7e44c81f2e1914806c306421a74e2bba43e5a53 Mon Sep 17 00:00:00 2001 From: Ilan Feler Date: Sun, 6 Mar 2022 14:48:01 +0000 Subject: [PATCH 105/405] Translated using Weblate (Hebrew) Currently translated at 85.5% (1846 of 2157 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/he/ --- vector/src/main/res/values-iw/strings.xml | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/vector/src/main/res/values-iw/strings.xml b/vector/src/main/res/values-iw/strings.xml index 3d9cfd1154..73a57823eb 100644 --- a/vector/src/main/res/values-iw/strings.xml +++ b/vector/src/main/res/values-iw/strings.xml @@ -2136,4 +2136,43 @@ + נהל חדרים + החלט מי יכול לראות ולהצטרף לחדר זה. + הקש כדי לערוך מרחבים + חפש שם + חפש באמצאות שם, כינוי או מייל + דוחס וידאו %d%% + דוחס תמונה … + תן משוב + שליחת המשוב נכשלה (%s) + תודה, המשוב שלך נשלח בהצלחה + אתה יכול לצור איתי קשר במידה ויהיו לך שאלות המשך + הינך משתמש בגירסת נסיון של מרחבים. המשוב שלך ישמש לעידכון גהגרסאות הבאות. פרטי המערכת ושם המשתמש ירשמו כדי שנוכל להשתמש במשוב שלך בצורה מיטבית. + משוב + סוף המשאל + פעולה זו תעצור את האפשרות להצביע ותציג את תוצאות המשאל. + סוף המשאל\? + אפשרות הזוכה + סוף המשאל + שאלה לא יכולה להיות ריקה + צור משאל + הוסף אפשרות + אפשרות %1$d + צור אפשריות + שאלה או נושא + שאלה או נושא המשאל + צור משאל + אתחל את הישומון כדי שהשינויים יכנסו לתוקף. + הצטרף בכל מקרה + הצטרף למרחב + צור מרחב + דלג כרגע + הצטרף למרחב שלי %1$s%2$s + הודעות קבוצה מוצפנות + הודעות ישירות + שידרוגי חדר + השם המוצג שלי + שם המשתמש שלי + הזמנות לחדר + מילות מפתח \ No newline at end of file From 2c0d281a7bde658ec9ca50e27b543d7ca1f6be13 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 7 Mar 2022 07:33:01 +0000 Subject: [PATCH 106/405] Translated using Weblate (Japanese) Currently translated at 62.7% (32 of 51 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/ja/ --- .../android/ja-JP/changelogs/40100100.txt | 2 +- .../android/ja-JP/changelogs/40100110.txt | 2 +- .../android/ja-JP/changelogs/40100120.txt | 4 +- .../android/ja-JP/changelogs/40100130.txt | 4 +- .../android/ja-JP/changelogs/40100140.txt | 4 +- .../android/ja-JP/changelogs/40100150.txt | 4 +- .../android/ja-JP/changelogs/40100160.txt | 4 +- .../android/ja-JP/changelogs/40100170.txt | 4 +- .../android/ja-JP/changelogs/40101000.txt | 4 +- .../android/ja-JP/changelogs/40101010.txt | 4 +- .../android/ja-JP/changelogs/40101020.txt | 4 +- .../android/ja-JP/changelogs/40101030.txt | 4 +- .../android/ja-JP/changelogs/40101160.txt | 4 +- .../android/ja-JP/changelogs/40103090.txt | 2 + .../android/ja-JP/changelogs/40103100.txt | 2 +- .../android/ja-JP/changelogs/40103130.txt | 2 + .../android/ja-JP/changelogs/40103140.txt | 2 + .../android/ja-JP/changelogs/40103150.txt | 2 + .../android/ja-JP/full_description.txt | 40 +++++++++---------- 19 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103090.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103130.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103140.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/40103150.txt diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt index 48af96d216..0f9fc720a9 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt @@ -1,2 +1,2 @@ 今回の新バージョンでは、主にバグの修正と改善が行われています。メッセージの送信がより速くなりました。 -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.10 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100110.txt b/fastlane/metadata/android/ja-JP/changelogs/40100110.txt index b8b9798fcd..d67486a147 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100110.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100110.txt @@ -1,2 +1,2 @@ 今回の新バージョンでは、主にUI(ユーザーインターフェース)とUX(ユーザーエクスペリエンス)の向上が図られています。友達を招待したり、QRコードを読み取って素早くDMを作成できるようになりました。 -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.11 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100120.txt b/fastlane/metadata/android/ja-JP/changelogs/40100120.txt index 01c33c5d52..1e10e5f2e3 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100120.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100120.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +このバージョンの主な変更点:URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100130.txt b/fastlane/metadata/android/ja-JP/changelogs/40100130.txt index 941a052239..0e5ef9b8eb 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100130.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100130.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.13 +このバージョンの主な変更点:URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt index 6dc536cdcf..8fa9848d0b 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: 部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。 -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.14 +このバージョンの主な変更点:部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100150.txt b/fastlane/metadata/android/ja-JP/changelogs/40100150.txt index caded1b8ed..c94330b70b 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100150.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100150.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: ソーシャルログインに対応しました。 -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15 +このバージョンの主な変更点:ソーシャルログインに対応しました。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt index 1b1a2092b0..ae947f1781 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: パフォーマンスの向上とバグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 +このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt index a0cc7b107d..01b742a9a2 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: バグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.17 +このバージョンの主な変更点:バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt index d0900f38c2..0c09cee3dd 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: パフォーマンスの向上とバグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.0 +このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt index cb204e5696..25ac73b449 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: パフォーマンスの向上とバグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.1 +このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt index bb6ab66525..762879a281 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: パフォーマンスの向上とバグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.2 +このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt index e7ecc05a0f..3c641c09ac 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点: パフォーマンスの向上とバグの修正! -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.3 +このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました! +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101160.txt b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt index 985ea10510..3e37e353d7 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40101160.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。 -全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.16 +このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103090.txt b/fastlane/metadata/android/ja-JP/changelogs/40103090.txt new file mode 100644 index 0000000000..580b49e6d9 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103090.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:音声メッセージの下書き機能の追加。不具合の修正。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103100.txt b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt index 76c28cdd90..0527756005 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/40103100.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt @@ -1,2 +1,2 @@ -このバージョンの主な変更点:投票機能のサポート(実験的)。URL プレビューの新規デザイン。 +このバージョンの主な変更点:アンケート機能のサポート(実験的)。URL プレビューの新規デザイン。 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103130.txt b/fastlane/metadata/android/ja-JP/changelogs/40103130.txt new file mode 100644 index 0000000000..19d04a9b99 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103130.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.13 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103140.txt b/fastlane/metadata/android/ja-JP/changelogs/40103140.txt new file mode 100644 index 0000000000..c9f5062c5b --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103140.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.14 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103150.txt b/fastlane/metadata/android/ja-JP/changelogs/40103150.txt new file mode 100644 index 0000000000..89c3117cf5 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40103150.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。 +更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.15 diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt index 6014938cce..ce1550acb0 100644 --- a/fastlane/metadata/android/ja-JP/full_description.txt +++ b/fastlane/metadata/android/ja-JP/full_description.txt @@ -1,42 +1,42 @@ -Elementは、安全なメッセンジャー、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンドツーエンドの暗号化を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 +Elementは、安全なメッセージングアプリ、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンド・ツー・エンドの暗号化技術を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 Elementの特徴 - 高度なオンラインコミュニケーションツール -- 完全に暗号化されたメッセージにより、リモートワーカーでも、より安全な企業コミュニケーションが可能 -- Matrixオープンソースフレームワークをベースにした分散型のチャット -- プロジェクトを管理しながら、暗号化されたデータで安全にファイル共有 +- メッセージの完全な暗号化。リモートワーカーでも、より安全な企業コミュニケーションが可能 +- Matrixオープンソースフレームワークに基づく、分散型のチャット +- プロジェクトの管理と並行して、データの暗号化によりファイルを安全に共有することが可能 - Voice over IPによるビデオチャットと画面共有 -- お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能 +- お気に入りのオンラインコラボレーションツールや、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能 -Elementは他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権)コミュニケーションのためのオープンネットワークであるMatrixで動作します。ユーザーが自分のデータやメッセージを最大限にコントロールできるように、セルフホスティングも可能です。 +Elementは、他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権型)コミュニケーションのためのオープンネットワークであるMatrixで動作します。自分のデータやメッセージを最大限にコントロールするために、あなた自身がサーバーを運営することもできます。 プライバシーと暗号化されたコミュニケーション -Elementは、望ましくない広告、データマイニング、ウォールドガーデンからユーザーを保護します。また、エンド・ツー・エンドの暗号化と相互署名された端末の検証により、全てのデータ、1対1のビデオおよび音声通信を保護します。 +Elementは、望ましくない広告、データマイニング、囲い込みからユーザーを守ります。また、エンド・ツー・エンドの暗号化と、相互署名による端末の認証に基づき、全てのデータ、ビデオ会議、音声通信を保護します。 -Elementは、Slackなどのアプリと統合することで、Matrixネットワーク上の誰とでも安全にコミュニケーションを取ることができると同時に、プライバシーをコントロールすることができます。 +Elementでは、Matrixネットワークにいる誰とでもコミュニケーションが行えるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うとともに、プライバシーをコントロールすることができます。 -Elementはセルフホスティングが可能 -機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixベースのホストを選択することもできます。Elementは、プライバシー、セキュリティーコンプライアンス、および統合の柔軟性を提供します。 +セルフホスティングが可能 +機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixに基づくサーバーを選ぶこともできます。Elementは、プライバシー、セキュリティーコンプライアンス、および柔軟な機能統合を提供します。 自分のデータを所有する -データやメッセージをどこに保管するかは、ユーザー自身が決めることができます。データマイニングやサードパーティからのアクセスのリスクはありません。 +データやメッセージを保管する場所を自分で決めることができます。データマイニングや第三者へのデータ流出のリスクはありません。 -Elementでは、どのサーバーを使うかを、ご自身で決めることができます。 -1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得するか、ボランティアがホストしているパブリックサーバーから選択する。 +Elementでは、どのサーバーを使うかをご自身で決めることができます。 +1. 開発者が運営する matrix.org の公開サーバーで無料アカウントを取得するか、ボランティアが管理している運営サーバーから選ぶ。 2. あなた自身がサーバーを運営し、アカウントを管理する。 -3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。 +3. Element Matrix Servicesの運営プラットフォームに加入し、カスタムサーバー上でアカウントを作る。 オープンなメッセージングとコラボレーション -Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらには他のメッセージングアプリを使っているかに関わらず、チャットをすることができます。 +相手がElement、他のMatrixアプリ、さらには他のメッセージングアプリを使っているかに関わらず、Matrixネットワーク上の誰とでもチャットをすることができます。 非常に安全 -本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できる)と、相互署名された端末の検証を行います。 +本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、クロス署名による端末の認証が可能です。 包括的なコミュニケーションと統合 -メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くのインテグレーション、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます。 +メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。 -中断からの再開 -メッセージの履歴は全ての端末とウェブ(https://app.element.io)で完全に同期されるので、どこからでも連絡を取り合うことができます。 +いつでも、どこにいても +メッセージの履歴は、全ての端末とウェブ(https://app.element.io)で完全に同期されるので、どこからでも連絡を取り合うことができます。 オープンソース -Element AndroidはGitHubで開発されているオープンソースのプロジェクトです。 バグの報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。 +Element Androidは、GitHubで開発されているオープンソースのプロジェクトです。 不具合の報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。 From 67c9584215f1a7cbc2228f2ee2d2438e10fc4ef9 Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:05:49 +0000 Subject: [PATCH 107/405] Ignore flaky VerificationTest --- .../sdk/internal/crypto/verification/qrcode/VerificationTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 35c5a4dab9..2c96568102 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -39,6 +40,7 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@Ignore("This test is flaky ; see issue #5449") class VerificationTest : InstrumentedTest { data class ExpectedResult( From eb46067c0836ed0e6b322eb9ee3c8ca486e2bc66 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 15:40:13 +0100 Subject: [PATCH 108/405] Changes caught exception type to Throwable --- .../android/sdk/internal/session/space/ResolveSpaceInfoTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 6c9a5c7101..305bc5db04 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -48,7 +48,7 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( private suspend fun getSpaceHierarchy() = try { getStableSpaceHierarchy() - } catch (e: HttpException) { + } catch (e: Throwable) { getUnstableSpaceHierarchy() } From a53d5bdba2986debbbe7bdeb59fbdb05784b079e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 8 Mar 2022 16:41:38 +0200 Subject: [PATCH 109/405] Remove eventType from /relations api for threads --- .../android/sdk/internal/session/room/RoomAPI.kt | 15 ++++++++++++++- .../relation/threads/FetchThreadTimelineTask.kt | 6 +----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 71838ab5a2..ceed846608 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room 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.RelationType import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams @@ -218,7 +219,6 @@ internal interface RoomAPI { /** * Paginate relations for event based in normal topological order - * * @param relationType filter for this relation type * @param eventType filter for this event type */ @@ -232,6 +232,19 @@ internal interface RoomAPI { @Query("limit") limit: Int? = null ): RelationsResponse + /** + * Paginate relations for thread events based in normal topological order + * @param relationType filter for this relation type + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") + suspend fun getThreadsRelations(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Path("relationType") relationType: String = RelationType.IO_THREAD, + @Query("from") from: String? = null, + @Query("to") to: String? = null, + @Query("limit") limit: Int? = null + ): RelationsResponse + /** * Join the given room. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index a8f712c4fa..227558cc75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -20,7 +20,6 @@ import io.realm.Realm 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.RelationType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider @@ -99,14 +98,11 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( } override suspend fun execute(params: FetchThreadTimelineTask.Params): Result { - val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val response = executeRequest(globalErrorReceiver) { - roomAPI.getRelations( + roomAPI.getThreadsRelations( roomId = params.roomId, eventId = params.rootThreadEventId, - relationType = RelationType.IO_THREAD, from = params.from, - eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE, limit = params.limit ) } From 82b5fc9557cbd775a5eefe20c9dcfb46304b7e52 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 16:50:49 +0100 Subject: [PATCH 110/405] Removes unused imports --- .../sdk/internal/session/space/ResolveSpaceInfoTask.kt | 1 - .../java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 305bc5db04..16a3511e32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.space import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task -import retrofit2.HttpException import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt index 26ee295236..b851e15575 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt @@ -18,13 +18,9 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk -import okhttp3.ResponseBody.Companion.toResponseBody import org.matrix.android.sdk.internal.session.space.SpaceApi -import org.matrix.android.sdk.internal.session.space.SpacesResponse import org.matrix.android.sdk.test.fixtures.ResolveSpaceInfoTaskParamsFixture import org.matrix.android.sdk.test.fixtures.SpacesResponseFixture -import retrofit2.HttpException -import retrofit2.Response internal class FakeSpaceApi { @@ -37,8 +33,7 @@ internal class FakeSpaceApi { } fun givenStableEndpointFails() { - val errorResponse = Response.error(500, "".toResponseBody()) - coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws HttpException(errorResponse) + coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws Exception() } fun givenUnstableEndpointWorks() { From 510206aa8af8fd4bde6994eac4a408588412cba9 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 16:54:05 +0100 Subject: [PATCH 111/405] Adds changelog file --- changelog.d/5443.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5443.misc diff --git a/changelog.d/5443.misc b/changelog.d/5443.misc new file mode 100644 index 0000000000..f9fd715403 --- /dev/null +++ b/changelog.d/5443.misc @@ -0,0 +1 @@ +Adds stable room hierarchy endpoint with a fallback to the unstable one From 0892525c84045a2df912949c7bbc4df6b61f5468 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 17:19:11 +0100 Subject: [PATCH 112/405] Adds debug logs --- .../internal/session/space/ResolveSpaceInfoTask.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 16a3511e32..9b0adad54b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { @@ -39,10 +40,12 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( private lateinit var params: ResolveSpaceInfoTask.Params override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse { - return executeRequest(globalErrorReceiver) { + val result = executeRequest(globalErrorReceiver) { this.params = params getSpaceHierarchy() } + Timber.w("Space Info result: $result") + return result } private suspend fun getSpaceHierarchy() = try { @@ -57,7 +60,9 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( suggestedOnly = params.suggestedOnly, limit = params.limit, maxDepth = params.maxDepth, - from = params.from) + from = params.from).also { + Timber.w("Spaces response stable: $it") + } private suspend fun getUnstableSpaceHierarchy() = spaceApi.getSpaceHierarchyUnstable( @@ -65,5 +70,7 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( suggestedOnly = params.suggestedOnly, limit = params.limit, maxDepth = params.maxDepth, - from = params.from) + from = params.from).also { + Timber.w("Spaces response unstable: $it") + } } From 54828f76cf900ba5bc9064b8db13939ded06c1c2 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 8 Mar 2022 17:26:01 +0100 Subject: [PATCH 113/405] Adds slash to v1 prefix path --- .../org/matrix/android/sdk/internal/network/NetworkConstants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 2d567f273d..21bba0f535 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -21,7 +21,7 @@ internal object NetworkConstants { private const val URI_API_PREFIX_PATH = "_matrix/client" const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" - const val URI_API_PREFIX_PATH_V1 = "${URI_API_PREFIX_PATH}v1/" + const val URI_API_PREFIX_PATH_V1 = "${URI_API_PREFIX_PATH}/v1/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media From 17d58f24d209f0cf85c83fb7a31f9b056e248565 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 1 Mar 2022 00:26:26 +0100 Subject: [PATCH 114/405] Add padding before our first message Signed-off-by: sim --- changelog.d/5384.misc | 1 + .../home/room/detail/timeline/item/AbsMessageItem.kt | 4 ++++ .../room/detail/timeline/style/TimelineMessageLayout.kt | 3 +++ .../detail/timeline/style/TimelineMessageLayoutFactory.kt | 1 + vector/src/main/res/layout/view_message_bubble.xml | 8 +++++++- 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5384.misc diff --git a/changelog.d/5384.misc b/changelog.d/5384.misc new file mode 100644 index 0000000000..dca87422bb --- /dev/null +++ b/changelog.d/5384.misc @@ -0,0 +1 @@ +Add top margin before our first message diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 9e8f86c26e..ac5d943cf6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -105,6 +105,9 @@ abstract class AbsMessageItem : AbsBaseMessageItem } else { holder.timeView.isVisible = false } + + holder.additionalTopSpace.isVisible = attributes.informationData.messageLayout.addTopMargin + // Render send state indicator holder.sendStateImageView.render(attributes.informationData.sendStateDecoration) holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA @@ -154,6 +157,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { + val additionalTopSpace by bind(R.id.additionalTopSpace) val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index c87680de0a..202d8b05da 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -24,12 +24,14 @@ sealed interface TimelineMessageLayout : Parcelable { val layoutRes: Int val showAvatar: Boolean val showDisplayName: Boolean + val addTopMargin: Boolean val showTimestamp: Boolean @Parcelize data class Default(override val showAvatar: Boolean, override val showDisplayName: Boolean, override val showTimestamp: Boolean, + override val addTopMargin: Boolean = false, // Keep defaultLayout generated on epoxy items override val layoutRes: Int = 0) : TimelineMessageLayout @@ -38,6 +40,7 @@ sealed interface TimelineMessageLayout : Parcelable { override val showAvatar: Boolean, override val showDisplayName: Boolean, override val showTimestamp: Boolean = true, + override val addTopMargin: Boolean = false, val isIncoming: Boolean, val isPseudoBubble: Boolean, val cornersRadius: CornersRadius, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 8c1c308bb5..a614015f46 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -118,6 +118,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess TimelineMessageLayout.Bubble( showAvatar = showInformation && !isSentByMe, showDisplayName = showInformation && !isSentByMe, + addTopMargin = isFirstFromThisSender && isSentByMe, isIncoming = !isSentByMe, cornersRadius = cornersRadius, isPseudoBubble = messageContent.isPseudoBubble(), diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 5ae5afc329..d56bdb659d 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -26,12 +26,18 @@ android:padding="2dp" tools:src="@sample/user_round_avatars" /> + + Date: Tue, 8 Mar 2022 21:32:13 +0100 Subject: [PATCH 115/405] Adds error print stack trace --- .../android/sdk/internal/session/space/ResolveSpaceInfoTask.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 9b0adad54b..723f640edc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -51,6 +51,8 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor( private suspend fun getSpaceHierarchy() = try { getStableSpaceHierarchy() } catch (e: Throwable) { + Timber.w("Stable space hierarchy failed: ${e.message}") + e.printStackTrace() getUnstableSpaceHierarchy() } From 96b51744b6738d7a3514742c214a2f6b44be5045 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 8 Mar 2022 23:19:21 +0100 Subject: [PATCH 116/405] Fix ktlint --- .../sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 3e0e8eb00a..954c2dbe43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -679,7 +679,7 @@ internal class DefaultKeysBackupService @Inject constructor( Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") throw InvalidParameterException("Invalid recovery key") } - + // Save for next time and for gossiping // Save now as it's valid, don't wait for the import as it could take long. saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) From a98f502c01c0571f172c801962c2a521aad19d62 Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Tue, 1 Mar 2022 17:30:21 +0100 Subject: [PATCH 117/405] Adding custom view for options picker --- .../location/LocationSharingOption.kt | 23 +++++ .../view/LocationSharingOptionPickerView.kt | 43 +++++++++ .../view/LocationSharingOptionView.kt | 95 +++++++++++++++++++ .../res/layout/fragment_location_sharing.xml | 32 +------ .../layout/view_location_sharing_option.xml | 42 ++++++++ .../view_location_sharing_option_picker.xml | 22 +++++ vector/src/main/res/values/attrs.xml | 10 ++ 7 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/LocationSharingOption.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt create mode 100644 vector/src/main/res/layout/view_location_sharing_option.xml create mode 100644 vector/src/main/res/layout/view_location_sharing_option_picker.xml create mode 100644 vector/src/main/res/values/attrs.xml diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingOption.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingOption.kt new file mode 100644 index 0000000000..38f4bff16e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingOption.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +enum class LocationSharingOption { + USER_CURRENT, // current user's location + USER_LIVE, // user's location during a certain amount of time + PINNED // static location pinned by the user +} diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt new file mode 100644 index 0000000000..896edb5558 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import im.vector.app.databinding.ViewLocationSharingOptionPickerBinding + +/** + * Custom view to display the location sharing option picker. + */ +class LocationSharingOptionPickerView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + /*private val binding = */ + init { + ViewLocationSharingOptionPickerBinding.inflate( + LayoutInflater.from(context), + this + ) + } + + /*fun setOptions(vararg options: LocationSharingOption) { + // TODO show only the options passed in argument + }*/ +} diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt new file mode 100644 index 0000000000..b9cb6cf3d0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.view + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import im.vector.app.R +import im.vector.app.databinding.ViewLocationSharingOptionBinding + +/** + * Custom view to display a location sharing option. + */ +class LocationSharingOptionView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewLocationSharingOptionBinding.inflate( + LayoutInflater.from(context), + this + ) + + init { + applyRipple() + context.theme.obtainStyledAttributes( + attrs, + R.styleable.LocationSharingOptionView, + 0, + 0 + ).run { + try { + setIcon(this) + setTitle(this) + } finally { + recycle() + } + } + } + + private fun applyRipple() { + val outValue = TypedValue() + context.theme.resolveAttribute( + android.R.attr.selectableItemBackground, + outValue, + true + ) + binding.root.background = ContextCompat.getDrawable( + context, + outValue.resourceId + ) + } + + private fun setIcon(typedArray: TypedArray) { + val icon = typedArray.getDrawable(R.styleable.LocationSharingOptionView_icon) + val background = typedArray.getDrawable(R.styleable.LocationSharingOptionView_iconBackground) + val backgroundTint = typedArray.getColor( + R.styleable.LocationSharingOptionView_iconBackgroundTint, + ContextCompat.getColor(context, android.R.color.transparent) + ) + val description = typedArray.getString(R.styleable.LocationSharingOptionView_iconDescription) + + binding.shareLocationOptionIcon.setImageDrawable(icon) + val bkg = background?.let { + val backgroundDrawable = DrawableCompat.wrap(it) + DrawableCompat.setTint(backgroundDrawable, backgroundTint) + backgroundDrawable + } ?: background + binding.shareLocationOptionIcon.background = bkg + binding.shareLocationOptionIcon.contentDescription = description + } + + private fun setTitle(typedArray: TypedArray) { + val title = typedArray.getString(R.styleable.LocationSharingOptionView_title) + binding.shareLocationOptionTitle.text = title + } +} diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml index ad418f3e1c..1d64a56fdc 100644 --- a/vector/src/main/res/layout/fragment_location_sharing.xml +++ b/vector/src/main/res/layout/fragment_location_sharing.xml @@ -14,40 +14,14 @@ app:mapbox_renderTextureMode="true" tools:background="#4F00" /> - - - - - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/view_location_sharing_option.xml b/vector/src/main/res/layout/view_location_sharing_option.xml new file mode 100644 index 0000000000..a399c438dd --- /dev/null +++ b/vector/src/main/res/layout/view_location_sharing_option.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_location_sharing_option_picker.xml b/vector/src/main/res/layout/view_location_sharing_option_picker.xml new file mode 100644 index 0000000000..13c93611d0 --- /dev/null +++ b/vector/src/main/res/layout/view_location_sharing_option_picker.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..e35e111794 --- /dev/null +++ b/vector/src/main/res/values/attrs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + From 1c6b31001f09fd7a7aacf3ae0fbe2656dccc29be Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Tue, 1 Mar 2022 17:32:03 +0100 Subject: [PATCH 118/405] Adding changelog entry --- changelog.d/5395.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5395.feature diff --git a/changelog.d/5395.feature b/changelog.d/5395.feature new file mode 100644 index 0000000000..eb16c6cd81 --- /dev/null +++ b/changelog.d/5395.feature @@ -0,0 +1 @@ +Add a custom view to display a picker for share location options From 55c6383074fa76f42e916b4773bb5e04f13944eb Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Tue, 1 Mar 2022 17:43:34 +0100 Subject: [PATCH 119/405] Set options method --- .../location/LocationSharingFragment.kt | 2 ++ .../view/LocationSharingOptionPickerView.kt | 19 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index a7e93a3f6c..9333c579b4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -59,6 +59,8 @@ class LocationSharingFragment @Inject constructor( views.mapView.initialize(urlMapProvider.getMapUrl()) } + // TODO change the options dynamically depending on the current chosen location + views.shareLocationContainer.setOptions(LocationSharingOption.PINNED) views.shareLocationContainer.debouncedClicks { viewModel.handle(LocationSharingAction.OnShareLocation) } diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt index 896edb5558..aa72bb17f2 100644 --- a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt @@ -20,7 +20,9 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible import im.vector.app.databinding.ViewLocationSharingOptionPickerBinding +import im.vector.app.features.location.LocationSharingOption /** * Custom view to display the location sharing option picker. @@ -29,15 +31,12 @@ class LocationSharingOptionPickerView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { - /*private val binding = */ - init { - ViewLocationSharingOptionPickerBinding.inflate( - LayoutInflater.from(context), - this - ) - } + private val binding = ViewLocationSharingOptionPickerBinding.inflate( + LayoutInflater.from(context), + this + ) - /*fun setOptions(vararg options: LocationSharingOption) { - // TODO show only the options passed in argument - }*/ + fun setOptions(vararg options: LocationSharingOption) { + binding.locationSharingOptionPinned.isVisible = options.contains(LocationSharingOption.PINNED) + } } From 4586426958d0dd25bce76669fe12bc482d094aad Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Tue, 1 Mar 2022 18:02:38 +0100 Subject: [PATCH 120/405] Adding other options --- .../location/LocationSharingFragment.kt | 5 +-- .../view/LocationSharingOptionPickerView.kt | 7 +++- .../res/layout/fragment_location_sharing.xml | 10 +++--- .../view_location_sharing_option_picker.xml | 34 ++++++++++++++++++- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 9333c579b4..368b4651dc 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -60,8 +60,9 @@ class LocationSharingFragment @Inject constructor( } // TODO change the options dynamically depending on the current chosen location - views.shareLocationContainer.setOptions(LocationSharingOption.PINNED) - views.shareLocationContainer.debouncedClicks { + // set correct click listener on each option + views.shareLocationOptionsPicker.setOptions(LocationSharingOption.PINNED) + views.shareLocationOptionsPicker.debouncedClicks { viewModel.handle(LocationSharingAction.OnShareLocation) } diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt index aa72bb17f2..1795bad197 100644 --- a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt @@ -37,6 +37,11 @@ class LocationSharingOptionPickerView @JvmOverloads constructor( ) fun setOptions(vararg options: LocationSharingOption) { - binding.locationSharingOptionPinned.isVisible = options.contains(LocationSharingOption.PINNED) + binding.locationSharingOptionUserCurrentLocation.isVisible = + options.contains(LocationSharingOption.USER_CURRENT) + binding.locationSharingOptionUserLiveLocation.isVisible = + options.contains(LocationSharingOption.USER_LIVE) + binding.locationSharingOptionPinned.isVisible = + options.contains(LocationSharingOption.PINNED) } } diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml index 1d64a56fdc..b2279ed146 100644 --- a/vector/src/main/res/layout/fragment_location_sharing.xml +++ b/vector/src/main/res/layout/fragment_location_sharing.xml @@ -9,13 +9,13 @@ android:id="@+id/mapView" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@id/shareLocationContainer" + app:layout_constraintBottom_toTopOf="@id/shareLocationOptionsPicker" app:layout_constraintTop_toTopOf="parent" app:mapbox_renderTextureMode="true" tools:background="#4F00" /> + android:layout_marginBottom="@dimen/layout_vertical_margin" + app:layout_constraintBottom_toBottomOf="@id/shareLocationOptionsPicker" + app:layout_constraintEnd_toEndOf="@id/shareLocationOptionsPicker" /> diff --git a/vector/src/main/res/layout/view_location_sharing_option_picker.xml b/vector/src/main/res/layout/view_location_sharing_option_picker.xml index 13c93611d0..359c9331f4 100644 --- a/vector/src/main/res/layout/view_location_sharing_option_picker.xml +++ b/vector/src/main/res/layout/view_location_sharing_option_picker.xml @@ -6,17 +6,49 @@ android:layout_height="wrap_content" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + + + + + + From 3bbb7167f1f6b404d205add8e7f8f3872ce8de0f Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Wed, 2 Mar 2022 10:44:02 +0100 Subject: [PATCH 121/405] Adding icon for live location --- library/ui-styles/src/main/res/values/dimens.xml | 5 ++++- .../location/view/LocationSharingOptionView.kt | 6 ++++++ .../res/drawable/ic_attachment_location_live_white.xml | 10 ++++++++++ .../main/res/layout/view_location_sharing_option.xml | 2 +- .../res/layout/view_location_sharing_option_picker.xml | 3 ++- vector/src/main/res/values/attrs.xml | 1 + 6 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_attachment_location_live_white.xml diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index db42cfa12c..4719d77fbe 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -70,4 +70,7 @@ 0.15 0.05 - \ No newline at end of file + + + 10dp + diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt index b9cb6cf3d0..a967537592 100644 --- a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt @@ -24,6 +24,7 @@ import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.setPadding import im.vector.app.R import im.vector.app.databinding.ViewLocationSharingOptionBinding @@ -76,6 +77,10 @@ class LocationSharingOptionView @JvmOverloads constructor( R.styleable.LocationSharingOptionView_iconBackgroundTint, ContextCompat.getColor(context, android.R.color.transparent) ) + val padding = typedArray.getDimensionPixelOffset( + R.styleable.LocationSharingOptionView_iconPadding, + context.resources.getDimensionPixelOffset(R.dimen.location_sharing_option_default_padding) + ) val description = typedArray.getString(R.styleable.LocationSharingOptionView_iconDescription) binding.shareLocationOptionIcon.setImageDrawable(icon) @@ -85,6 +90,7 @@ class LocationSharingOptionView @JvmOverloads constructor( backgroundDrawable } ?: background binding.shareLocationOptionIcon.background = bkg + binding.shareLocationOptionIcon.setPadding(padding) binding.shareLocationOptionIcon.contentDescription = description } diff --git a/vector/src/main/res/drawable/ic_attachment_location_live_white.xml b/vector/src/main/res/drawable/ic_attachment_location_live_white.xml new file mode 100644 index 0000000000..da6a37232d --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_location_live_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/view_location_sharing_option.xml b/vector/src/main/res/layout/view_location_sharing_option.xml index a399c438dd..e7aa4aa6a9 100644 --- a/vector/src/main/res/layout/view_location_sharing_option.xml +++ b/vector/src/main/res/layout/view_location_sharing_option.xml @@ -26,9 +26,9 @@ android:layout_width="40dp" android:layout_height="40dp" android:contentDescription="@string/a11y_location_share_icon" - android:padding="10dp" tools:background="@drawable/circle" tools:backgroundTint="?colorPrimary" + tools:padding="10dp" tools:src="@drawable/ic_attachment_location_white" /> + From 0707877b3c925ba447e405a5cd2bc63245cbe409 Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Wed, 2 Mar 2022 11:07:33 +0100 Subject: [PATCH 122/405] Using correct string resources for options --- .../layout/view_location_sharing_option_picker.xml | 12 ++++++------ vector/src/main/res/values/strings.xml | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/vector/src/main/res/layout/view_location_sharing_option_picker.xml b/vector/src/main/res/layout/view_location_sharing_option_picker.xml index 277f0f0604..ecb96fc1e4 100644 --- a/vector/src/main/res/layout/view_location_sharing_option_picker.xml +++ b/vector/src/main/res/layout/view_location_sharing_option_picker.xml @@ -16,12 +16,12 @@ app:icon="@drawable/ic_attachment_location_white" app:iconBackground="@drawable/circle" app:iconBackgroundTint="?colorPrimary" - app:iconDescription="@string/a11y_location_share_icon" + app:iconDescription="@string/a11y_location_share_option_pinned_icon" app:layout_constraintBottom_toTopOf="@id/locationSharingOptionUserCurrentLocation" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:title="@string/location_share" /> + app:title="@string/location_share_option_pinned" /> + app:title="@string/location_share_option_user_current" /> + app:title="@string/location_share_option_user_live" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index dc52d2cf5d..8cdbe122f3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2924,9 +2924,17 @@ Share location Location + Share location Map + Share location + Share my current location + Share my current location + Share live location + Share live location + Share this location + Share this location ${app_name} could not access your location ${app_name} could not access your location. Please try again later. Open with From fb764028c9e66a83da95b6a4a3e4fd50a483dbbb Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Wed, 2 Mar 2022 12:15:04 +0100 Subject: [PATCH 123/405] Adding dividers --- .../view/LocationSharingOptionPickerView.kt | 16 ++++++++----- .../view/LocationSharingOptionView.kt | 9 +++++++ .../view_location_sharing_option_picker.xml | 24 ++++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt index 1795bad197..de8c6dc8f3 100644 --- a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionPickerView.kt @@ -37,11 +37,15 @@ class LocationSharingOptionPickerView @JvmOverloads constructor( ) fun setOptions(vararg options: LocationSharingOption) { - binding.locationSharingOptionUserCurrentLocation.isVisible = - options.contains(LocationSharingOption.USER_CURRENT) - binding.locationSharingOptionUserLiveLocation.isVisible = - options.contains(LocationSharingOption.USER_LIVE) - binding.locationSharingOptionPinned.isVisible = - options.contains(LocationSharingOption.PINNED) + val optionsNumber = options.toSet().size + val isPinnedVisible = options.contains(LocationSharingOption.PINNED) + val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT) + val isUserLiveVisible = options.contains(LocationSharingOption.USER_LIVE) + + binding.locationSharingOptionPinned.isVisible = isPinnedVisible + binding.locationSharingOptionsDivider1.isVisible = isPinnedVisible && optionsNumber > 1 + binding.locationSharingOptionUserCurrentLocation.isVisible = isUserCurrentVisible + binding.locationSharingOptionsDivider2.isVisible = isUserCurrentVisible && isUserLiveVisible + binding.locationSharingOptionUserLiveLocation.isVisible = isUserLiveVisible } } diff --git a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt index a967537592..a5d97ca955 100644 --- a/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/location/view/LocationSharingOptionView.kt @@ -18,6 +18,7 @@ package im.vector.app.features.location.view import android.content.Context import android.content.res.TypedArray +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater @@ -57,6 +58,14 @@ class LocationSharingOptionView @JvmOverloads constructor( } } + fun setIcon(icon: Drawable) { + binding.shareLocationOptionIcon.setImageDrawable(icon) + } + + fun setIconBackground(bkg: Drawable) { + binding.shareLocationOptionIcon.background = bkg + } + private fun applyRipple() { val outValue = TypedValue() context.theme.resolveAttribute( diff --git a/vector/src/main/res/layout/view_location_sharing_option_picker.xml b/vector/src/main/res/layout/view_location_sharing_option_picker.xml index ecb96fc1e4..fdabae7ca2 100644 --- a/vector/src/main/res/layout/view_location_sharing_option_picker.xml +++ b/vector/src/main/res/layout/view_location_sharing_option_picker.xml @@ -23,20 +23,42 @@ app:layout_constraintTop_toTopOf="parent" app:title="@string/location_share_option_pinned" /> + + + + Date: Wed, 2 Mar 2022 15:00:16 +0100 Subject: [PATCH 124/405] Changing color of the live location icon --- library/ui-styles/src/main/res/values/colors.xml | 4 ++++ library/ui-styles/src/main/res/values/theme_dark.xml | 2 ++ library/ui-styles/src/main/res/values/theme_light.xml | 2 ++ .../main/res/layout/view_location_sharing_option_picker.xml | 5 +---- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 6610c0f45d..e4e8b1d900 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -139,4 +139,8 @@ @color/palette_gray_100 @color/palette_gray_450 + + + @color/palette_prune + @color/palette_prune diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 607f008453..8cd4dc2399 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -145,6 +145,8 @@ @style/Widget.Vector.ActionButton + + @color/vctr_live_location_dark - - - - - - - - \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 6610c0f45d..461dacdf68 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -9,10 +9,6 @@ #2f9edb ?colorError - - #14368BD6 - @color/palette_azure - @color/palette_azure @color/palette_melon @@ -22,7 +18,6 @@ #99000000 #27303A - #FF61708B #1E61708B @@ -83,16 +78,6 @@ #BF000000 #BF000000 - - #FFFFFFFF - #FF22262E - #FF090A0C - - - #FFE9EDF1 - #FF22262E - #FF090A0C - #EBEFF5 #27303A @@ -107,9 +92,7 @@ #AAAAAAAA #55555555 - #EEEEEE - #61708B #FFF3F8FD diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index db42cfa12c..8307915474 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -9,20 +9,11 @@ 32dp 50dp - 16dp - 196dp - 44dp - 72dp 16dp 32dp - 40dp - 60dp - - 4dp 8dp - 8dp 0.75 diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml index e37fd8a7c6..625745f709 100644 --- a/library/ui-styles/src/main/res/values/palette.xml +++ b/library/ui-styles/src/main/res/values/palette.xml @@ -1,5 +1,7 @@ - + + + @@ -15,6 +17,7 @@ #0DBD8B #FFFFFF #FF5B55 + #7E69FF #2DC2C5 #5C56F5 @@ -27,6 +30,7 @@ #8D97A5 #737D8C #17191C + #F4F9FD diff --git a/library/ui-styles/src/main/res/values/palette_mobile.xml b/library/ui-styles/src/main/res/values/palette_mobile.xml index c22b9705c7..ec2f1d0814 100644 --- a/library/ui-styles/src/main/res/values/palette_mobile.xml +++ b/library/ui-styles/src/main/res/values/palette_mobile.xml @@ -35,7 +35,6 @@ @color/palette_gray_25 @color/palette_black_950 - @color/palette_black_950 @color/palette_white @color/palette_black_800 diff --git a/library/ui-styles/src/main/res/values/styles_attachments.xml b/library/ui-styles/src/main/res/values/styles_attachments.xml deleted file mode 100644 index 18c2e3f95f..0000000000 --- a/library/ui-styles/src/main/res/values/styles_attachments.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml index d09d0a399d..004aca5aaa 100644 --- a/library/ui-styles/src/main/res/values/styles_buttons.xml +++ b/library/ui-styles/src/main/res/values/styles_buttons.xml @@ -33,24 +33,6 @@ - - - - - - - - \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/styles_dial_pad.xml b/library/ui-styles/src/main/res/values/styles_dial_pad.xml index 34e128c56d..77dc0b3081 100644 --- a/library/ui-styles/src/main/res/values/styles_dial_pad.xml +++ b/library/ui-styles/src/main/res/values/styles_dial_pad.xml @@ -1,5 +1,8 @@ - + + + - - diff --git a/library/ui-styles/src/main/res/values/theme_black.xml b/library/ui-styles/src/main/res/values/theme_black.xml index 44d4206d43..6e5ce80c19 100644 --- a/library/ui-styles/src/main/res/values/theme_black.xml +++ b/library/ui-styles/src/main/res/values/theme_black.xml @@ -11,8 +11,6 @@ @color/vctr_fab_label_stroke_black @color/vctr_fab_label_color_black @color/vctr_touch_guard_bg_black - @color/vctr_attachment_selector_background_black - @color/vctr_attachment_selector_border_black @color/vctr_room_active_widgets_banner_bg_black @color/vctr_room_active_widgets_banner_text_black @color/vctr_reaction_background_off_black diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 607f008453..23cdaea5e2 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -21,8 +21,6 @@ @color/vctr_fab_label_color_dark @color/vctr_touch_guard_bg_dark @color/vctr_keys_backup_banner_accent_color_dark - @color/vctr_attachment_selector_background_dark - @color/vctr_attachment_selector_border_dark @color/vctr_room_active_widgets_banner_bg_dark @color/vctr_room_active_widgets_banner_text_dark @color/vctr_reaction_background_off_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index efc18b9f32..6189a1f399 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -21,8 +21,6 @@ @color/vctr_fab_label_color_light @color/vctr_touch_guard_bg_light @color/vctr_keys_backup_banner_accent_color_light - @color/vctr_attachment_selector_background_light - @color/vctr_attachment_selector_border_light @color/vctr_room_active_widgets_banner_bg_light @color/vctr_room_active_widgets_banner_text_light @color/vctr_reaction_background_off_light diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 3e301eebb9..ff623760e0 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -34,9 +34,8 @@ android { buildConfigField "String", "SDK_VERSION", "\"1.4.4\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" - resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" - resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" - resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" + buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" + buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\"" defaultConfig { consumerProguardFiles 'proguard-rules.pro' diff --git a/vector/lint.xml b/vector/lint.xml index 6bdd2ae585..e219ac1eed 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -20,24 +20,9 @@ - - - - - - - - - - - - - - - - + + diff --git a/vector/src/main/res/anim/anim_slide_in_bottom.xml b/vector/src/main/res/anim/anim_slide_in_bottom.xml deleted file mode 100644 index d696a09e13..0000000000 --- a/vector/src/main/res/anim/anim_slide_in_bottom.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/vector/src/main/res/anim/anim_slide_nothing.xml b/vector/src/main/res/anim/anim_slide_nothing.xml deleted file mode 100644 index c0bf1b82bc..0000000000 --- a/vector/src/main/res/anim/anim_slide_nothing.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/vector/src/main/res/anim/anim_slide_out_bottom.xml b/vector/src/main/res/anim/anim_slide_out_bottom.xml deleted file mode 100644 index e9fd4e9a7f..0000000000 --- a/vector/src/main/res/anim/anim_slide_out_bottom.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/vector/src/main/res/anim/enter_from_left.xml b/vector/src/main/res/anim/enter_from_left.xml deleted file mode 100644 index e20f4f589a..0000000000 --- a/vector/src/main/res/anim/enter_from_left.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/anim/enter_from_right.xml b/vector/src/main/res/anim/enter_from_right.xml deleted file mode 100644 index e749a77191..0000000000 --- a/vector/src/main/res/anim/enter_from_right.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/anim/exit_to_left.xml b/vector/src/main/res/anim/exit_to_left.xml deleted file mode 100644 index ef67949730..0000000000 --- a/vector/src/main/res/anim/exit_to_left.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/anim/exit_to_right.xml b/vector/src/main/res/anim/exit_to_right.xml deleted file mode 100644 index 9c7dca9435..0000000000 --- a/vector/src/main/res/anim/exit_to_right.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/anim/unread_marker_anim.xml b/vector/src/main/res/anim/unread_marker_anim.xml deleted file mode 100644 index 9e61c80c9d..0000000000 --- a/vector/src/main/res/anim/unread_marker_anim.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable-xxhdpi/ic_material_call_end_grey.png b/vector/src/main/res/drawable-xxhdpi/ic_material_call_end_grey.png deleted file mode 100644 index 638811519461737521d210512b68d47202fe356b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4424 zcmbVO^;;8;^WQ)i4bmx~C?QT@AmJ$K4r!#Md(>z_N~EQvLt-G^jT4m|ASEz17$6~C zV<-wA-haaP`?}|K_dKt=`{91L=kBe(t{OE3I|Tp$pw>`VHoS>x|B;OJ#&4;vbpiks zun;3Ne?z-K9+IyLht98`|?TCC|?IBNE1n>;e0wHo&2Ju zub?r_#`ou@=jKi!PKB2})p>Em%F$!%3O7pQ?)E8*pT=rj!M-1k_wjJ; zCb{BC_ul@sYufoQmn^wjvG&229gDkHBLTlp6dD?Kch~XX3GSTX)S1hmSJvO0&rj#W z|FBp6?AWb-WieH2D>hp3betFhi^m7J(BByiJ_~FcPlh(%N`d;tK z;K6bk0ljqK+6Ii5`(I1#*c^5Xe`{K>0y!+(Wd2acHx!@A)7S|;!op{?9``54uaA$F>P z$z_PXUv#m_)gO2qxO_;A-EGnl+zG8UGlgrnP)!y@__B~|5SX3^uU+?2cqey7@clYQnVOO92s%4GMaHO2)p9Oi^@2vZWJ=LxescXhg} zsB-IC(dBJF^8IG_D(1m`_~o`#xi%N$sY)y3HxuvFUJp35PJQ_1N8~LRSb#@S*S03zf1}HdBn$H@! zpQC~&)`l5l9h$ShX85H$CsCPbnR|syV&9|wiF+&V*o<0U8Ei%&4kF%l;i^L&50-kv z`Q4r65)6xO1v7~NL@XoIhs_s)+6S9Xj^+H(k8%+x}+lu>gS5s z@OMj`7gH|^1@`oZxIH!1uLFcJCYTXC+It+uDTu&kZEW|qu^Vn$seD8 zz(@ZXj}GaCXbOLxjMF$@?*jRyYwKnym@7(1FXvujB~xGRqjNUc&wS{I3p9H>W7_xJ-lU%INPUy_^qiYx6qLE; zxZ?VcExt_w;(>vsfbQ~CH9Ksz5Rk{lj5Q=)TDK(OidcijXm8}wGTh&^t);w~V|Y)f zUtH{Oh78sGkbS)T#oCuC(pLYn)?8cGzuygVd&rd+pNzC6)<|@wRM)o6g~&>Rqi5ru zgi-w!KPIBMo~VNz)~VFhF>q`T+nY>{W*~(y*69TmW$b^5HHvxZx3mqhkGp}wJDXkG zz^Dr}e5UrOdj#yRx8;3@(u94rpwH{{_SoxCl54aU8=Yq~h9%J=?I-i1JSO}?Bj`lR zX|k%@*COA*F7$#)(1oLlnGWaY&&IrS*B`VoEN0#(Nm{q;X74deoa{rl`Q)33u;ETh zhVeP|^qQtl1{IZEvq&gfTd2@?6wRkMz#|oiO15MU_*)+0OS*QUdfutrQfTFl5b5aG zVM+m2j@$VZj zZI#o_UU(-#P<1h6Mp{X4;BjPlVP|rGbfQ8E;ed z>uANWBpixTzqo?Xyfp4R2;-|X=_>v1Wu={KVVzcOH~6Gwn&opjojc*3KeWw6cf@%g z%-YZ*hSa*ZRmz&HxY7}}6F}uCZAza7Wq>NC&l_$NIh&_0TDxMhq{i=%BwFSxnkrdr@1q~58o#Y3bk@bqRINNxGc#>fXHaS%@bTro zzG&UFkAKOn^P{xnK;`F~&gT-q?-o{{mpffKDa6`7nBo{2H&3d8xr)f*XDXTc$*%=g zC|meu-8(0;*X12*;q)&tF7pt+rOEqTCWoi^be#6pJ+~lvc-Cpbkbt~YoF0oh>Y~Nq zvm6ykTfKF`- z^mb003mSWZW~7>SNe0{tIUWooq5bq>L640~_lU@eZDKC7=Ycmr)%*4_ZQ!@GVnp(Z zqn}waMXMy(c?PGSN2cVM1*%hh9%V_DR*TH^@ZToC;~vTjp469*j92wBiry{i7YL=JIm0!35m5v4pICU-liujhJIy3 z>ABCCAWc?$S5weGYasm11lzvOj9(`Z8zC3B8Oy9KzhvjstcR%Y_~g>B~_cjP;f zqru>BZZ06G89hZJvUZ5~*{wLr0&(Ve=O~3tS4gr?WIqF)?PeK4^7FPCRRk!>hWkr{ z`fvm1HQRRa>#LpUGg<;wEh+*iX}lJm#N<ob!eTa-3g&d}C?gs#X z@BK$$P=!4FMg;k5=%|3!$mnRnByRxizit}MHIx;NZr)RDXuOdl>(KF`c4d|k{B!GF zt=L8}dhvZTMmyC~4b$oc1)%{ks;~sHsDy!ZkaEIR71<+Uf$0zVW_Fc2jvJ3!9#%5t zO^;_`0{ZYnmzw!?JuQCHewX1m5uSsC0>aI*N%TGZZ!17xMmG>Ugt|H_uSiN0a6rstYA|j5A%hRvyv4sM^v&4lcKPu zv&xaEJtJI`8K>!J^|6f4o#N5i~G<&ykNv9xKq_mR#>X}7UB z3Fr@%E%-wPCpd$hQ%3{3aTT8`J4J0pD!@EO7|KH+(7GS2kx)p_WeG5Wu!kbOV-Px` zh!TgSn*WwmIDWPb#p$-a^S5Ic;a~PqNDZ2qr$h$0ag02Fj+#g|$7#l@QNUzHcquXU z?P)|h6ZFh{bb?>2gjes_ynA*wLGLBPBKS2<_$UsoNO2azoI~!>P%7D1mO`uUPTe>E z2{1Xx&-#GAsHL97eMd^6#_eZ#JHWL2a#J|6=aZ6IA_A?{N_6otg}Akw^w}&?QXql0 z1luMO7|}AS)XI^QzBvGNO}vDwVdmXvCND3qc|(yjM(*)fr1b%W)ZIRz00JjWhv`Sf zH6^Y8(O)?D{Uul<-Png;=wBG&jx7zWuqOg7fOyjN;$VqSqlh0hRj713)h4U4^V%k; zBm+b_?!jrN(WZ%IX^lL}8Rs=OR#>xmUh;<)`cXZb<_(b;~aI>?*DVGhdy{93K6|5@%KO|6$MBKCls@ zSHY_~10FLJ3F$~cg9!?b+i$IJsR}^v|4ROG``}jy@BWbj@D!>t!LWZHlNqij3!X9* z?0Vs4n$Fj7yT8Lj7+WmqMpD1+71S*o!z5kc$*)ZYp5=YKn+6v=qWB#lIDU21YdO5~ z5FXncrqs&Td9#MQbO`znEw2M38OGR(yyg`^)o<%}*0ga?ZCeL2+^kyT8gsTJt&+&r z6Up@yCGlL7;^13+Ro4W(h MP|;Pce`Xu?KMbFL-2eap diff --git a/vector/src/main/res/drawable-xxhdpi/ic_material_done_white.png b/vector/src/main/res/drawable-xxhdpi/ic_material_done_white.png deleted file mode 100755 index 0ebb55559b55f99df3270796763745a71f113078..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255 zcmV=}0*5fKp)HP1G&I6Iqc zjKz5rgEVtukiIy2Qo_-K;J|?c2M!!K@i?}(lWik$uJ;4aeo|e|;mI*SsV?U@bDX5g zSJ{Te;g>S0!OODIPipkgGo)TTcRZxVWAGnIFqKOr(UM3=(~BhBSvit;g=R?Vjh>M7 zAB+M?!6}eboF615Cqq(m7NiKA4^kA)n-qydL_|cirW>R3em?os7vTT^002ovPDHLk FV1hF8Y9jyu diff --git a/vector/src/main/res/drawable-xxhdpi/incoming_call_notification_transparent.png b/vector/src/main/res/drawable-xxhdpi/incoming_call_notification_transparent.png deleted file mode 100755 index b11532ef5a34edaf915afa9f64be727aa763d796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 684 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$a>cu_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv`A9L$_1f@*Sm+Zh;`EInNuLn>~)y>l?|u!DfB<4MVh ze2oGujT873Z@TME(G*{N_pkL`#sCi6^B$W%76J`{f|w(s)GsQw<=ByArcQTKst0xI`x+`cV*&Wu+R z!s^&UOV@1V7g|38p00i_>zopr0B$`3wEzGB diff --git a/vector/src/main/res/drawable/bg_attachment_type_selector.xml b/vector/src/main/res/drawable/bg_attachment_type_selector.xml deleted file mode 100644 index 2cbad6a855..0000000000 --- a/vector/src/main/res/drawable/bg_attachment_type_selector.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_button_secondary.xml b/vector/src/main/res/drawable/bg_button_secondary.xml deleted file mode 100644 index 65db98b8d7..0000000000 --- a/vector/src/main/res/drawable/bg_button_secondary.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_group_item.xml b/vector/src/main/res/drawable/bg_group_item.xml deleted file mode 100644 index 88d93ddee3..0000000000 --- a/vector/src/main/res/drawable/bg_group_item.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_tombstone_predecessor.xml b/vector/src/main/res/drawable/bg_tombstone_predecessor.xml deleted file mode 100644 index 4b7a8d2d4f..0000000000 --- a/vector/src/main/res/drawable/bg_tombstone_predecessor.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/call_header_transparent_bg.xml b/vector/src/main/res/drawable/call_header_transparent_bg.xml deleted file mode 100644 index 17408c8b8f..0000000000 --- a/vector/src/main/res/drawable/call_header_transparent_bg.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml deleted file mode 100644 index 3de9a237e8..0000000000 --- a/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_bell.xml b/vector/src/main/res/drawable/ic_bell.xml deleted file mode 100644 index cb648585ce..0000000000 --- a/vector/src/main/res/drawable/ic_bell.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_book.xml b/vector/src/main/res/drawable/ic_book.xml deleted file mode 100644 index 3cd7357248..0000000000 --- a/vector/src/main/res/drawable/ic_book.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_call.xml b/vector/src/main/res/drawable/ic_call.xml deleted file mode 100644 index 430c438577..0000000000 --- a/vector/src/main/res/drawable/ic_call.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_call_conference_small.xml b/vector/src/main/res/drawable/ic_call_conference_small.xml deleted file mode 100644 index 1ba596d4a9..0000000000 --- a/vector/src/main/res/drawable/ic_call_conference_small.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_call_end.xml b/vector/src/main/res/drawable/ic_call_end.xml deleted file mode 100644 index 0e80a323d8..0000000000 --- a/vector/src/main/res/drawable/ic_call_end.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_call_flip_camera_active.xml b/vector/src/main/res/drawable/ic_call_flip_camera_active.xml deleted file mode 100644 index 25590cc753..0000000000 --- a/vector/src/main/res/drawable/ic_call_flip_camera_active.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_call_flip_camera_default.xml b/vector/src/main/res/drawable/ic_call_flip_camera_default.xml deleted file mode 100644 index 75ad0133f8..0000000000 --- a/vector/src/main/res/drawable/ic_call_flip_camera_default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_call_mute_active.xml b/vector/src/main/res/drawable/ic_call_mute_active.xml deleted file mode 100644 index 757f9cfa17..0000000000 --- a/vector/src/main/res/drawable/ic_call_mute_active.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_call_mute_default.xml b/vector/src/main/res/drawable/ic_call_mute_default.xml deleted file mode 100644 index 37a0c83fec..0000000000 --- a/vector/src/main/res/drawable/ic_call_mute_default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_call_pip.xml b/vector/src/main/res/drawable/ic_call_pip.xml deleted file mode 100644 index aaad2d09de..0000000000 --- a/vector/src/main/res/drawable/ic_call_pip.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml b/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml deleted file mode 100644 index 50768871ab..0000000000 --- a/vector/src/main/res/drawable/ic_fab_add_by_mxid.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - diff --git a/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml b/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml deleted file mode 100644 index 50768871ab..0000000000 --- a/vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - diff --git a/vector/src/main/res/drawable/ic_flair.xml b/vector/src/main/res/drawable/ic_flair.xml deleted file mode 100644 index 8d30334432..0000000000 --- a/vector/src/main/res/drawable/ic_flair.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_home_bottom_catchup.xml b/vector/src/main/res/drawable/ic_home_bottom_catchup.xml deleted file mode 100644 index 1091bb04c7..0000000000 --- a/vector/src/main/res/drawable/ic_home_bottom_catchup.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_lock.xml b/vector/src/main/res/drawable/ic_lock.xml deleted file mode 100644 index 2929f41225..0000000000 --- a/vector/src/main/res/drawable/ic_lock.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_material_add.xml b/vector/src/main/res/drawable/ic_material_add.xml deleted file mode 100644 index 757f450331..0000000000 --- a/vector/src/main/res/drawable/ic_material_add.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_more_horizontal_2.xml b/vector/src/main/res/drawable/ic_more_horizontal_2.xml deleted file mode 100644 index 3aab2fada5..0000000000 --- a/vector/src/main/res/drawable/ic_more_horizontal_2.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/vector/src/main/res/drawable/ic_poll.xml b/vector/src/main/res/drawable/ic_poll.xml deleted file mode 100644 index 581343eb2b..0000000000 --- a/vector/src/main/res/drawable/ic_poll.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/vector/src/main/res/drawable/ic_radio_button_off.xml b/vector/src/main/res/drawable/ic_radio_button_off.xml deleted file mode 100644 index f4f3f0d268..0000000000 --- a/vector/src/main/res/drawable/ic_radio_button_off.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/vector/src/main/res/drawable/red_dot.xml b/vector/src/main/res/drawable/red_dot.xml deleted file mode 100644 index b7fccb7cab..0000000000 --- a/vector/src/main/res/drawable/red_dot.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/searches_cursor_background.xml b/vector/src/main/res/drawable/searches_cursor_background.xml deleted file mode 100644 index c9d1d88498..0000000000 --- a/vector/src/main/res/drawable/searches_cursor_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/vector_medias_picker_button_background.xml b/vector/src/main/res/drawable/vector_medias_picker_button_background.xml deleted file mode 100644 index 8adc855a90..0000000000 --- a/vector/src/main/res/drawable/vector_medias_picker_button_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_incoming_share.xml b/vector/src/main/res/layout/activity_incoming_share.xml deleted file mode 100644 index 0de417f57b..0000000000 --- a/vector/src/main/res/layout/activity_incoming_share.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml b/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml deleted file mode 100644 index 0539c648b4..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/vector/src/main/res/layout/bottom_sheet_call_sound_device_chooser.xml b/vector/src/main/res/layout/bottom_sheet_call_sound_device_chooser.xml deleted file mode 100644 index e751ac412a..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_call_sound_device_chooser.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml b/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml deleted file mode 100644 index b8eefc52ec..0000000000 --- a/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - -